feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')
This commit is contained in:
Generated
+249
@@ -8,6 +8,8 @@
|
|||||||
"name": "mediusa-clinic-os",
|
"name": "mediusa-clinic-os",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
|
"lucide-react": "^1.41.0",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
@@ -228,6 +230,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@@ -1718,6 +1729,19 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.18",
|
"version": "19.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
@@ -1738,6 +1762,13 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.69.0",
|
"version": "8.69.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz",
|
||||||
@@ -2680,6 +2711,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.11.21",
|
"version": "2.11.21",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
|
||||||
@@ -2830,6 +2871,26 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -2887,6 +2948,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.50.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
|
||||||
|
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2902,6 +2978,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -3054,6 +3140,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz",
|
||||||
|
"integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optional": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -3777,6 +3873,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-png": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
|
"iobuffer": "^5.3.2",
|
||||||
|
"pako": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
|
||||||
@@ -3787,6 +3894,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -4175,6 +4288,20 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4227,6 +4354,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iobuffer": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -4777,6 +4910,23 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-png": "^6.2.0",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||||
@@ -5156,6 +5306,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "1.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.41.0.tgz",
|
||||||
|
"integrity": "sha512-6lksP35l6KszDKUeRTi4LV7i6DEe0Yzl2ALJm9j4c5xEYN91GdW1xGsawGMOg2mgjF5GHBVX8pKX9kP+cWsP3Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -5573,6 +5732,22 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -5613,6 +5788,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5724,6 +5906,16 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.8",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
@@ -5775,6 +5967,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@@ -5851,6 +6050,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -6173,6 +6382,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -6373,6 +6592,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||||
@@ -6394,6 +6623,16 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -6734,6 +6973,16 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
|
"lucide-react": "^1.41.0",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
|
|||||||
+19
-12
@@ -1,29 +1,36 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import "./globals.css";
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: 'Mediusa Clinic OS — Next-Gen Practice Management Platform',
|
||||||
description: "Generated by create next app",
|
description:
|
||||||
|
'High-speed, proprietary white-label medical practice management platform for chiropractors and wellness clinics. Scheduling, paperless intake, ambient AI clinical notes, and billing.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased dark`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col bg-slate-950 text-slate-100 font-sans selection:bg-teal-500 selection:text-slate-950">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+388
-59
@@ -1,69 +1,398 @@
|
|||||||
import Image from "next/image";
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
INITIAL_TENANTS,
|
||||||
|
INITIAL_PROVIDERS,
|
||||||
|
INITIAL_PATIENTS,
|
||||||
|
INITIAL_APPOINTMENTS,
|
||||||
|
INITIAL_SOAP_NOTES,
|
||||||
|
INITIAL_SUPERBILLS,
|
||||||
|
} from '@/lib/mock-data';
|
||||||
|
import {
|
||||||
|
ClinicTenant,
|
||||||
|
Provider,
|
||||||
|
Patient,
|
||||||
|
Appointment,
|
||||||
|
SoapNote,
|
||||||
|
Superbill,
|
||||||
|
} from '@/types/clinical';
|
||||||
|
import { CalendarView } from '@/components/calendar/CalendarView';
|
||||||
|
import { SoapChartEditor } from '@/components/charting/SoapChartEditor';
|
||||||
|
import { SuperbillView } from '@/components/billing/SuperbillView';
|
||||||
|
import { RetentionView } from '@/components/retention/RetentionView';
|
||||||
|
import { PatientPortalView } from '@/components/intake/PatientPortalView';
|
||||||
|
import { SuperAdminView } from '@/components/admin/SuperAdminView';
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
FileText,
|
||||||
|
DollarSign,
|
||||||
|
Users,
|
||||||
|
ShieldCheck,
|
||||||
|
Building2,
|
||||||
|
Globe,
|
||||||
|
Sparkles,
|
||||||
|
ChevronDown,
|
||||||
|
Stethoscope,
|
||||||
|
Laptop,
|
||||||
|
CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
// Global Multi-Tenant State
|
||||||
|
const [tenants, setTenants] = useState<ClinicTenant[]>(INITIAL_TENANTS);
|
||||||
|
const [activeTenantId, setActiveTenantId] = useState<string>(INITIAL_TENANTS[0].id);
|
||||||
|
|
||||||
|
// Active Navigation Mode: 'clinic' (EHR Practice OS) | 'patient' (Booking/Intake) | 'superadmin' (Mediusa Command)
|
||||||
|
const [portalMode, setPortalMode] = useState<'clinic' | 'patient' | 'superadmin'>('clinic');
|
||||||
|
|
||||||
|
// Clinic Portal Sub-Tabs: 'calendar' | 'charting' | 'billing' | 'retention'
|
||||||
|
const [clinicTab, setClinicTab] = useState<'calendar' | 'charting' | 'billing' | 'retention'>('calendar');
|
||||||
|
|
||||||
|
// Active Entities
|
||||||
|
const [appointments, setAppointments] = useState<Appointment[]>(INITIAL_APPOINTMENTS);
|
||||||
|
const [patients, setPatients] = useState<Patient[]>(INITIAL_PATIENTS);
|
||||||
|
const [soapNotes, setSoapNotes] = useState<SoapNote[]>(INITIAL_SOAP_NOTES);
|
||||||
|
const [superbills, setSuperbills] = useState<Superbill[]>(INITIAL_SUPERBILLS);
|
||||||
|
|
||||||
|
const [activePatient, setActivePatient] = useState<Patient>(INITIAL_PATIENTS[0]); // Marcus Chen
|
||||||
|
const [activeSoapNote, setActiveSoapNote] = useState<SoapNote | undefined>(INITIAL_SOAP_NOTES[0]);
|
||||||
|
const [activeSuperbill, setActiveSuperbill] = useState<Superbill>(INITIAL_SUPERBILLS[0]);
|
||||||
|
|
||||||
|
const activeTenant = tenants.find((t) => t.id === activeTenantId) || tenants[0];
|
||||||
|
const activeProviders = INITIAL_PROVIDERS.filter((p) => p.tenantId === activeTenant.id);
|
||||||
|
const activeProvider = activeProviders[0] || INITIAL_PROVIDERS[0];
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleSelectAppointment = (apt: Appointment) => {
|
||||||
|
const patientMatch = patients.find((p) => p.id === apt.patientId) || patients[0];
|
||||||
|
setActivePatient(patientMatch);
|
||||||
|
|
||||||
|
const existingSoap = soapNotes.find((s) => s.patientId === apt.patientId && s.appointmentId === apt.id);
|
||||||
|
setActiveSoapNote(existingSoap);
|
||||||
|
setClinicTab('charting');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
|
||||||
|
setAppointments((prev) =>
|
||||||
|
prev.map((a) => (a.id === aptId ? { ...a, status } : a))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSoapNote = (note: SoapNote) => {
|
||||||
|
setSoapNotes((prev) => {
|
||||||
|
const idx = prev.findIndex((s) => s.id === note.id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const copy = [...prev];
|
||||||
|
copy[idx] = note;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
return [...prev, note];
|
||||||
|
});
|
||||||
|
setActiveSoapNote(note);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateSuperbillFromNote = (note: SoapNote) => {
|
||||||
|
const newSb: Superbill = {
|
||||||
|
id: `sb-${Date.now()}`,
|
||||||
|
tenantId: activeTenant.id,
|
||||||
|
invoiceNumber: `SB-2026-${Math.floor(1000 + Math.random() * 9000)}`,
|
||||||
|
patientId: activePatient.id,
|
||||||
|
patientName: `${activePatient.firstName} ${activePatient.lastName}`,
|
||||||
|
patientDob: activePatient.dob,
|
||||||
|
patientAddress: activePatient.address,
|
||||||
|
providerName: note.providerName,
|
||||||
|
providerNpi: activeProvider.npi,
|
||||||
|
clinicName: activeTenant.name,
|
||||||
|
clinicAddress: activeTenant.address,
|
||||||
|
clinicTaxId: activeTenant.taxId,
|
||||||
|
dateOfService: note.date,
|
||||||
|
posCode: '11 (Office)',
|
||||||
|
icd10Codes: note.icd10Codes,
|
||||||
|
items: note.cptCodes.map((cpt) => ({
|
||||||
|
cptCode: cpt.code,
|
||||||
|
description: cpt.description,
|
||||||
|
units: 1,
|
||||||
|
rate: cpt.fee,
|
||||||
|
total: cpt.fee,
|
||||||
|
})),
|
||||||
|
totalAmount: note.cptCodes.reduce((sum, c) => sum + c.fee, 0),
|
||||||
|
patientPaid: note.cptCodes.reduce((sum, c) => sum + c.fee, 0),
|
||||||
|
balanceDue: 0,
|
||||||
|
paymentMethod: 'Stripe Card',
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setSuperbills((prev) => [newSb, ...prev]);
|
||||||
|
setActiveSuperbill(newSb);
|
||||||
|
setClinicTab('billing');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkPaid = (superbillId: string, method: Superbill['paymentMethod']) => {
|
||||||
|
setSuperbills((prev) =>
|
||||||
|
prev.map((sb) =>
|
||||||
|
sb.id === superbillId
|
||||||
|
? { ...sb, balanceDue: 0, patientPaid: sb.totalAmount, paymentMethod: method }
|
||||||
|
: sb
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (activeSuperbill.id === superbillId) {
|
||||||
|
setActiveSuperbill((prev) => ({
|
||||||
|
...prev,
|
||||||
|
balanceDue: 0,
|
||||||
|
patientPaid: prev.totalAmount,
|
||||||
|
paymentMethod: method,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePatientBookingComplete = (newAptData: any) => {
|
||||||
|
const newApt: Appointment = {
|
||||||
|
id: `apt-${Date.now()}`,
|
||||||
|
tenantId: activeTenant.id,
|
||||||
|
patientId: `pat-${Date.now()}`,
|
||||||
|
...newAptData,
|
||||||
|
};
|
||||||
|
setAppointments((prev) => [newApt, ...prev]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTenant = (newTenant: ClinicTenant) => {
|
||||||
|
setTenants((prev) => [...prev, newTenant]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchTenant = (tenantId: string) => {
|
||||||
|
setActiveTenantId(tenantId);
|
||||||
|
setPortalMode('clinic');
|
||||||
|
setClinicTab('calendar');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col">
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
{/* Top Global Command Bar */}
|
||||||
<Image
|
<header className="sticky top-0 z-40 bg-slate-950/90 backdrop-blur-md border-b border-slate-800/80 px-4 lg:px-8 py-3">
|
||||||
className="dark:invert h-5 w-[100px]"
|
<div className="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
src="/next.svg"
|
{/* Brand & Clinic Domain Selector */}
|
||||||
alt="Next.js logo"
|
<div className="flex items-center gap-4">
|
||||||
width={100}
|
<div className="flex items-center gap-2">
|
||||||
height={20}
|
<span
|
||||||
priority
|
className="w-8 h-8 rounded-xl flex items-center justify-center font-black text-xs text-slate-950 shadow-md"
|
||||||
/>
|
style={{ backgroundColor: activeTenant.brandColor }}
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the{" "}
|
|
||||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
|
||||||
page.tsx
|
|
||||||
</code>{" "}
|
|
||||||
file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
>
|
||||||
Templates
|
M+
|
||||||
</a>{" "}
|
</span>
|
||||||
or the{" "}
|
<div>
|
||||||
<a
|
<div className="text-xs font-black tracking-tight text-white flex items-center gap-1.5">
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<span>MEDIUSA CLINIC OS</span>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<span className="text-[10px] px-1.5 py-0.2 rounded bg-teal-500/20 text-teal-300 font-mono">
|
||||||
>
|
v2.4 PRO
|
||||||
Learning
|
</span>
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
<div className="text-[11px] text-slate-400 font-mono flex items-center gap-1">
|
||||||
<a
|
<Globe className="w-3 h-3 text-teal-400" />
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
https://{activeTenant.domain}
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-[14px] w-4"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tenant Switcher Dropdown */}
|
||||||
|
<div className="relative group">
|
||||||
|
<select
|
||||||
|
value={activeTenantId}
|
||||||
|
onChange={(e) => setActiveTenantId(e.target.value)}
|
||||||
|
className="appearance-none bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-200 text-xs font-semibold rounded-xl px-3 py-1.5 pr-8 cursor-pointer focus:outline-none focus:border-teal-500 transition"
|
||||||
|
>
|
||||||
|
{tenants.map((t) => (
|
||||||
|
<option key={t.id} value={t.id}>
|
||||||
|
🏥 {t.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Core Portal Mode Switcher */}
|
||||||
|
<div className="flex items-center gap-1 bg-slate-900 p-1 rounded-xl border border-slate-800 text-xs font-semibold">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPortalMode('clinic')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||||
|
portalMode === 'clinic'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Stethoscope className="w-3.5 h-3.5" />
|
||||||
|
Doctor & Practice OS
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPortalMode('patient')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||||
|
portalMode === 'patient'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Laptop className="w-3.5 h-3.5" />
|
||||||
|
Patient Booking & Intake
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPortalMode('superadmin')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||||
|
portalMode === 'superadmin'
|
||||||
|
? 'bg-indigo-600 text-white font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Building2 className="w-3.5 h-3.5 text-indigo-300" />
|
||||||
|
Mediusa Super-Admin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-Tabs for Doctor Clinic OS */}
|
||||||
|
{portalMode === 'clinic' && (
|
||||||
|
<div className="max-w-7xl mx-auto flex items-center gap-2 mt-3 pt-2 border-t border-slate-800/80 overflow-x-auto text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('calendar')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'calendar'
|
||||||
|
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
Fast Calendar & Schedule
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('charting')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'charting'
|
||||||
|
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
SOAP Chart & 2D Spine Map ({activePatient.firstName} {activePatient.lastName})
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('billing')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'billing'
|
||||||
|
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<DollarSign className="w-3.5 h-3.5" />
|
||||||
|
Superbills & Stripe Billing
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('retention')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'retention'
|
||||||
|
? 'bg-slate-800 text-amber-300 border border-amber-500/40 font-bold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Users className="w-3.5 h-3.5 text-amber-400" />
|
||||||
|
Care Plan Retention Radar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main App Container */}
|
||||||
|
<main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
{/* PORTAL MODE 1: Clinic Practice OS */}
|
||||||
|
{portalMode === 'clinic' && (
|
||||||
|
<div>
|
||||||
|
{clinicTab === 'calendar' && (
|
||||||
|
<CalendarView
|
||||||
|
appointments={appointments}
|
||||||
|
providers={activeProviders}
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
onSelectAppointment={handleSelectAppointment}
|
||||||
|
onUpdateStatus={handleUpdateAppointmentStatus}
|
||||||
|
onNewAppointmentClick={() => setPortalMode('patient')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'charting' && (
|
||||||
|
<SoapChartEditor
|
||||||
|
patient={activePatient}
|
||||||
|
provider={activeProvider}
|
||||||
|
initialSoapNote={activeSoapNote}
|
||||||
|
onSaveSoapNote={handleSaveSoapNote}
|
||||||
|
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
||||||
|
onBackToCalendar={() => setClinicTab('calendar')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'billing' && (
|
||||||
|
<SuperbillView
|
||||||
|
superbill={activeSuperbill}
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
onBack={() => setClinicTab('charting')}
|
||||||
|
onMarkPaid={handleMarkPaid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'retention' && (
|
||||||
|
<RetentionView
|
||||||
|
patients={patients}
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
onSelectPatientChart={(patient) => {
|
||||||
|
setActivePatient(patient);
|
||||||
|
const existing = soapNotes.find((s) => s.patientId === patient.id);
|
||||||
|
setActiveSoapNote(existing);
|
||||||
|
setClinicTab('charting');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PORTAL MODE 2: White-Labeled Patient Booking & Intake */}
|
||||||
|
{portalMode === 'patient' && (
|
||||||
|
<PatientPortalView
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
providers={activeProviders}
|
||||||
|
onBookingComplete={handlePatientBookingComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PORTAL MODE 3: Mediusa Super-Admin Command Center */}
|
||||||
|
{portalMode === 'superadmin' && (
|
||||||
|
<SuperAdminView
|
||||||
|
tenants={tenants}
|
||||||
|
onAddTenant={handleAddTenant}
|
||||||
|
onSwitchTenant={handleSwitchTenant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Footer Branding (Mediusa / AI Pilots Ecosystem) */}
|
||||||
|
<footer className="bg-slate-950 border-t border-slate-800/80 py-4 px-6 text-center text-xs text-slate-500">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between max-w-7xl mx-auto gap-2">
|
||||||
|
<span>
|
||||||
|
Mediusa Clinic OS • Proprietary Practice Management Engine for AI Pilots / Mediusa
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-slate-400">
|
||||||
|
hello@aipilots.site • HIPAA Certified • Sub-30s Clinical Charting Standard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ClinicTenant } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Building2,
|
||||||
|
TrendingUp,
|
||||||
|
DollarSign,
|
||||||
|
Users,
|
||||||
|
Plus,
|
||||||
|
ExternalLink,
|
||||||
|
ShieldCheck,
|
||||||
|
CheckCircle2,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
|
Globe,
|
||||||
|
Settings,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface SuperAdminViewProps {
|
||||||
|
tenants: ClinicTenant[];
|
||||||
|
onAddTenant: (tenant: ClinicTenant) => void;
|
||||||
|
onSwitchTenant: (tenantId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuperAdminView: React.FC<SuperAdminViewProps> = ({
|
||||||
|
tenants,
|
||||||
|
onAddTenant,
|
||||||
|
onSwitchTenant,
|
||||||
|
}) => {
|
||||||
|
const [showProvisionModal, setShowProvisionModal] = useState(false);
|
||||||
|
const [newClinicName, setNewClinicName] = useState('');
|
||||||
|
const [newSlug, setNewSlug] = useState('');
|
||||||
|
const [newDoctorName, setNewDoctorName] = useState('');
|
||||||
|
const [newNpi, setNewNpi] = useState('');
|
||||||
|
const [newPlan, setNewPlan] = useState<'Starter' | 'Pro Clinic' | 'Multi-Doc Enterprise'>('Pro Clinic');
|
||||||
|
const [isProvisioning, setIsProvisioning] = useState(false);
|
||||||
|
const [provisionSuccess, setProvisionSuccess] = useState(false);
|
||||||
|
|
||||||
|
const totalMRR = tenants.reduce((sum, t) => sum + t.mrr, 0) + 3120; // 24 clinics total simulated
|
||||||
|
const totalClinics = tenants.length + 21; // 24 active clinics
|
||||||
|
|
||||||
|
const handleNameChange = (val: string) => {
|
||||||
|
setNewClinicName(val);
|
||||||
|
setNewSlug(
|
||||||
|
val
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProvisionSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsProvisioning(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const created: ClinicTenant = {
|
||||||
|
id: `tenant-${Date.now()}`,
|
||||||
|
name: newClinicName,
|
||||||
|
slug: newSlug,
|
||||||
|
domain: `${newSlug}.mediusa.app`,
|
||||||
|
phone: '(555) 839-2041',
|
||||||
|
email: `contact@${newSlug}.com`,
|
||||||
|
address: '100 Wellness Way, Suite 10',
|
||||||
|
cityStateZip: 'Irvine, CA 92618',
|
||||||
|
npi: newNpi || '1992019481',
|
||||||
|
taxId: '82-9481920',
|
||||||
|
logoText: newClinicName.slice(0, 10).toUpperCase(),
|
||||||
|
brandColor: '#0d9488',
|
||||||
|
accentColor: '#14b8a6',
|
||||||
|
plan: newPlan,
|
||||||
|
mrr: newPlan === 'Starter' ? 99 : newPlan === 'Pro Clinic' ? 149 : 199,
|
||||||
|
stripeConnected: true,
|
||||||
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddTenant(created);
|
||||||
|
setIsProvisioning(false);
|
||||||
|
setProvisionSuccess(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setProvisionSuccess(false);
|
||||||
|
setShowProvisionModal(false);
|
||||||
|
onSwitchTenant(created.id);
|
||||||
|
}, 1200);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Super-Admin Agency Banner */}
|
||||||
|
<div className="bg-gradient-to-r from-slate-900 via-indigo-950/60 to-slate-900 border border-indigo-500/30 rounded-2xl p-6 shadow-2xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-indigo-500/20 text-indigo-400 border border-indigo-500/30">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono font-bold uppercase tracking-widest text-indigo-300">
|
||||||
|
AI PILOTS / MEDIUSA AGENCY ENGINE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-white mt-1">
|
||||||
|
Clinic OS Multi-Tenant Command Center
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-slate-300 mt-1">
|
||||||
|
Autonomous Practice Management SaaS Platform. Onboard 20–100+ chiropractic & wellness clinics with zero developer overhead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowProvisionModal(true)}
|
||||||
|
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 text-xs font-black flex items-center gap-2 shadow-lg shadow-teal-500/20 transition shrink-0"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Provision New Clinic (60s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* High-Level SaaS Metrics */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl shadow-xl">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Current SaaS MRR</span>
|
||||||
|
<DollarSign className="w-4 h-4 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-white mt-2 font-mono">
|
||||||
|
${totalMRR.toLocaleString()}.00
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-emerald-400 mt-1 font-semibold">
|
||||||
|
+$596.00 added this month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl shadow-xl">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Target MRR @ 100 Clinics</span>
|
||||||
|
<TrendingUp className="w-4 h-4 text-teal-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-teal-400 mt-2 font-mono">$17,900.00</div>
|
||||||
|
<p className="text-[11px] text-slate-400 mt-1">Based on $179 avg clinic subscription</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl shadow-xl">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Active Clinic Tenants</span>
|
||||||
|
<Building2 className="w-4 h-4 text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-white mt-2 font-mono">{totalClinics} Clinics</div>
|
||||||
|
<p className="text-[11px] text-indigo-300 mt-1">100% white-label retention</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-5 rounded-2xl shadow-xl">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Active Patients Managed</span>
|
||||||
|
<Users className="w-4 h-4 text-sky-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-sky-400 mt-2 font-mono">3,842</div>
|
||||||
|
<p className="text-[11px] text-slate-400 mt-1">Across California & US clinics</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Clinic Tenant Registry */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between pb-4 border-b border-slate-800 gap-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-white text-base">Client Tenant Registry</h3>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
Each clinic operates on its isolated schema with custom branding and domain routing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-teal-400 font-mono">
|
||||||
|
{tenants.length} Primary Demo Instances Loaded
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-slate-800 mt-3">
|
||||||
|
{tenants.map((t) => (
|
||||||
|
<div
|
||||||
|
key={t.id}
|
||||||
|
className="py-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-slate-800/30 px-3 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="w-3 h-3 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: t.brandColor }}
|
||||||
|
/>
|
||||||
|
<h4 className="font-bold text-white text-sm">{t.name}</h4>
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-800 text-slate-300 font-mono border border-slate-700">
|
||||||
|
{t.plan} (${t.mrr}/mo)
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 font-semibold border border-emerald-500/30">
|
||||||
|
Stripe Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-400 mt-1">
|
||||||
|
<span className="flex items-center gap-1 text-teal-300 font-mono">
|
||||||
|
<Globe className="w-3 h-3 text-teal-400" />
|
||||||
|
https://{t.domain}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>NPI: {t.npi}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>EIN: {t.taxId}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Created: {t.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSwitchTenant(t.id)}
|
||||||
|
className="px-3.5 py-1.5 bg-teal-500 hover:bg-teal-400 text-slate-950 font-bold text-xs rounded-xl shadow transition flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
Launch Clinic OS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 60-Second Instant Clinic Provisioning Modal */}
|
||||||
|
{showProvisionModal && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-lg w-full p-6 shadow-2xl animate-in fade-in zoom-in-95 duration-150 text-slate-100">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-2 rounded-xl bg-teal-500/20 text-teal-400">
|
||||||
|
<Sparkles className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-white text-sm">
|
||||||
|
60-Second Instant Clinic Provisioning
|
||||||
|
</h4>
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
Creates database tenant, seed templates & custom domain
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{provisionSuccess ? (
|
||||||
|
<div className="py-8 text-center text-emerald-300">
|
||||||
|
<CheckCircle2 className="w-12 h-12 text-emerald-400 mx-auto mb-3 animate-bounce" />
|
||||||
|
<h4 className="text-lg font-bold text-white">Clinic Provisioned Successfully!</h4>
|
||||||
|
<p className="text-xs text-slate-300 mt-1">
|
||||||
|
Tenant schema generated. Redirecting to newly created white-labeled instance...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleProvisionSubmit} className="mt-4 space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-medium">Clinic Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="e.g. Pacific Coast Chiropractic & Wellness"
|
||||||
|
value={newClinicName}
|
||||||
|
onChange={(e) => handleNameChange(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-medium">Subdomain Slug</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={newSlug}
|
||||||
|
onChange={(e) => setNewSlug(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 font-mono focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-slate-500 mt-1 block">
|
||||||
|
https://{newSlug || 'slug'}.mediusa.app
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-medium">Lead Doctor Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Dr. Samuel Wright, D.C."
|
||||||
|
value={newDoctorName}
|
||||||
|
onChange={(e) => setNewDoctorName(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-medium">Provider NPI</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="10-digit NPI"
|
||||||
|
value={newNpi}
|
||||||
|
onChange={(e) => setNewNpi(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 font-mono focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1 font-medium">Subscription Plan</label>
|
||||||
|
<select
|
||||||
|
value={newPlan}
|
||||||
|
onChange={(e: any) => setNewPlan(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
>
|
||||||
|
<option value="Starter">Starter Clinic ($99/mo)</option>
|
||||||
|
<option value="Pro Clinic">Pro Clinic ($149/mo)</option>
|
||||||
|
<option value="Multi-Doc Enterprise">Multi-Doc Enterprise ($199/mo)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowProvisionModal(false)}
|
||||||
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold rounded-xl transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isProvisioning}
|
||||||
|
className="px-5 py-2.5 bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 font-black rounded-xl shadow-lg shadow-teal-500/20 transition flex items-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isProvisioning ? (
|
||||||
|
<>
|
||||||
|
<span className="w-3.5 h-3.5 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
|
||||||
|
Generating Tenant Schema...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
Provision Clinic in 60s
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Superbill, ClinicTenant } from '@/types/clinical';
|
||||||
|
import { generateSuperbillPdf } from '@/lib/pdf-generator';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Download,
|
||||||
|
CreditCard,
|
||||||
|
CheckCircle2,
|
||||||
|
ShieldAlert,
|
||||||
|
ArrowLeft,
|
||||||
|
DollarSign,
|
||||||
|
Printer,
|
||||||
|
Sparkles,
|
||||||
|
ExternalLink,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface SuperbillViewProps {
|
||||||
|
superbill: Superbill;
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
onBack: () => void;
|
||||||
|
onMarkPaid: (superbillId: string, method: Superbill['paymentMethod']) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuperbillView: React.FC<SuperbillViewProps> = ({
|
||||||
|
superbill,
|
||||||
|
activeTenant,
|
||||||
|
onBack,
|
||||||
|
onMarkPaid,
|
||||||
|
}) => {
|
||||||
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||||
|
const [isProcessingStripe, setIsProcessingStripe] = useState(false);
|
||||||
|
const [paymentSuccess, setPaymentSuccess] = useState(false);
|
||||||
|
|
||||||
|
const handleDownloadPdf = () => {
|
||||||
|
generateSuperbillPdf(superbill);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStripePay = (method: Superbill['paymentMethod']) => {
|
||||||
|
setIsProcessingStripe(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsProcessingStripe(false);
|
||||||
|
setPaymentSuccess(true);
|
||||||
|
onMarkPaid(superbill.id, method);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowPaymentModal(false);
|
||||||
|
setPaymentSuccess(false);
|
||||||
|
}, 1500);
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Header Bar */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBack}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl transition"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-black text-white">
|
||||||
|
Superbill {superbill.invoiceNumber}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2.5 py-0.5 rounded-full font-bold uppercase ${
|
||||||
|
superbill.balanceDue === 0
|
||||||
|
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
||||||
|
: 'bg-amber-500/20 text-amber-300 border border-amber-500/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{superbill.balanceDue === 0 ? 'Paid in Full' : 'Balance Due'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
Clean Medical Claim Standard (CMS-1500 / 837P Ready) for {superbill.patientName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownloadPdf}
|
||||||
|
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold flex items-center gap-1.5 transition border border-slate-700"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 text-teal-400" />
|
||||||
|
Download PDF Superbill
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{superbill.balanceDue > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPaymentModal(true)}
|
||||||
|
className="px-4 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-slate-950 text-xs font-bold flex items-center gap-1.5 shadow-lg shadow-teal-500/20 transition"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-4 h-4" />
|
||||||
|
Collect with Stripe
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Printed Superbill Card Simulation */}
|
||||||
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-6 sm:p-8 shadow-2xl max-w-4xl mx-auto text-slate-200">
|
||||||
|
{/* Clinic & Statement Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between pb-6 border-b border-slate-800 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-mono font-bold text-teal-400 uppercase tracking-widest">
|
||||||
|
PRACTICE MANAGEMENT SUPERBILL
|
||||||
|
</span>
|
||||||
|
<h2 className="text-xl font-black text-white mt-1">{superbill.clinicName}</h2>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">{superbill.clinicAddress}</p>
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
Tax ID / EIN: <strong className="text-slate-300 font-mono">{superbill.clinicTaxId}</strong> • POS:{' '}
|
||||||
|
<strong className="text-slate-300 font-mono">{superbill.posCode}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:text-right text-xs space-y-1">
|
||||||
|
<div className="text-slate-400 font-medium">Invoice Number</div>
|
||||||
|
<div className="font-mono text-sm font-bold text-white">{superbill.invoiceNumber}</div>
|
||||||
|
<div className="text-slate-400 font-medium mt-2">Date of Service</div>
|
||||||
|
<div className="font-mono text-slate-200">{superbill.dateOfService}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider & Patient 2-Col Info */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 py-6 border-b border-slate-800 text-xs">
|
||||||
|
{/* Provider */}
|
||||||
|
<div className="bg-slate-900/60 p-4 rounded-xl border border-slate-800/80">
|
||||||
|
<h4 className="font-bold text-slate-300 uppercase tracking-wider mb-2">
|
||||||
|
Rendering Provider
|
||||||
|
</h4>
|
||||||
|
<div className="font-semibold text-white">{superbill.providerName}</div>
|
||||||
|
<div className="text-slate-400 mt-1">
|
||||||
|
National Provider Identifier (NPI):{' '}
|
||||||
|
<strong className="text-teal-400 font-mono">{superbill.providerNpi}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-400 mt-1">Specialty: Chiropractic Clinical Biomechanics</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Patient */}
|
||||||
|
<div className="bg-slate-900/60 p-4 rounded-xl border border-slate-800/80">
|
||||||
|
<h4 className="font-bold text-slate-300 uppercase tracking-wider mb-2">
|
||||||
|
Patient Demographics
|
||||||
|
</h4>
|
||||||
|
<div className="font-semibold text-white">{superbill.patientName}</div>
|
||||||
|
<div className="text-slate-400 mt-1">DOB: {superbill.patientDob}</div>
|
||||||
|
<div className="text-slate-400 mt-1">Address: {superbill.patientAddress}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diagnosis ICD-10 Section */}
|
||||||
|
<div className="py-6 border-b border-slate-800">
|
||||||
|
<h4 className="text-xs font-bold text-slate-300 uppercase tracking-wider mb-3">
|
||||||
|
Primary Diagnosis Codes (ICD-10-CM)
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs">
|
||||||
|
{superbill.icd10Codes.map((icd, index) => (
|
||||||
|
<div
|
||||||
|
key={icd.code}
|
||||||
|
className="flex items-center gap-2 bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg"
|
||||||
|
>
|
||||||
|
<span className="w-5 h-5 rounded bg-teal-500/20 text-teal-300 font-bold flex items-center justify-center font-mono text-[11px]">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold font-mono text-teal-300">{icd.code}</span>
|
||||||
|
<span className="text-slate-400 truncate">{icd.description}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Procedures Table */}
|
||||||
|
<div className="py-6 border-b border-slate-800">
|
||||||
|
<h4 className="text-xs font-bold text-slate-300 uppercase tracking-wider mb-3">
|
||||||
|
Rendered Procedures (CPT Codes)
|
||||||
|
</h4>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-900 text-slate-400 border-b border-slate-800">
|
||||||
|
<th className="py-2.5 px-3 font-semibold">CPT Code</th>
|
||||||
|
<th className="py-2.5 px-3 font-semibold">Service Description</th>
|
||||||
|
<th className="py-2.5 px-3 font-semibold text-center">Units</th>
|
||||||
|
<th className="py-2.5 px-3 font-semibold text-right">Fee</th>
|
||||||
|
<th className="py-2.5 px-3 font-semibold text-right">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-800/80">
|
||||||
|
{superbill.items.map((item) => (
|
||||||
|
<tr key={item.cptCode} className="hover:bg-slate-900/40">
|
||||||
|
<td className="py-2.5 px-3 font-mono font-bold text-teal-300">{item.cptCode}</td>
|
||||||
|
<td className="py-2.5 px-3 text-slate-300">{item.description}</td>
|
||||||
|
<td className="py-2.5 px-3 text-center text-slate-400">{item.units}</td>
|
||||||
|
<td className="py-2.5 px-3 text-right font-mono">${item.rate.toFixed(2)}</td>
|
||||||
|
<td className="py-2.5 px-3 text-right font-mono font-bold text-white">
|
||||||
|
${item.total.toFixed(2)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Financial Summary */}
|
||||||
|
<div className="pt-6 flex flex-col sm:flex-row sm:items-end justify-between gap-6">
|
||||||
|
<div className="text-xs text-slate-500 max-w-sm">
|
||||||
|
<p>
|
||||||
|
This Superbill contains all required claim elements (CMS-1500 Boxes 21, 24, 25, 31, 33)
|
||||||
|
for direct patient submission to their private health insurance or FSA/HSA reimbursement plan.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-slate-400">
|
||||||
|
Payment Method: <strong className="text-teal-400">{superbill.paymentMethod}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 p-4 rounded-xl border border-slate-800 w-full sm:w-64 space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between text-slate-400">
|
||||||
|
<span>Total Charges:</span>
|
||||||
|
<span className="font-mono text-white">${superbill.totalAmount.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-slate-400">
|
||||||
|
<span>Patient Paid:</span>
|
||||||
|
<span className="font-mono text-emerald-400">-${superbill.patientPaid.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-slate-800 flex justify-between font-bold text-sm">
|
||||||
|
<span className="text-white">Balance Due:</span>
|
||||||
|
<span className="font-mono text-teal-400">${superbill.balanceDue.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stripe Payment Modal */}
|
||||||
|
{showPaymentModal && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-md w-full p-6 shadow-2xl animate-in fade-in zoom-in-95 duration-150 text-slate-100">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-2 rounded-xl bg-teal-500/20 text-teal-400">
|
||||||
|
<CreditCard className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-white text-sm">Stripe Connect Checkout</h4>
|
||||||
|
<p className="text-[11px] text-slate-400">Direct to {activeTenant.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-3 text-xs">
|
||||||
|
<div className="bg-slate-950 p-3 rounded-xl border border-slate-800 flex justify-between items-center">
|
||||||
|
<span className="text-slate-400">Amount Due:</span>
|
||||||
|
<span className="text-lg font-black text-teal-400 font-mono">
|
||||||
|
${superbill.balanceDue.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{paymentSuccess ? (
|
||||||
|
<div className="p-4 bg-emerald-500/20 border border-emerald-500/40 rounded-xl text-center text-emerald-300">
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-emerald-400 mx-auto mb-2 animate-bounce" />
|
||||||
|
<div className="font-bold text-sm">Payment Processed Successfully!</div>
|
||||||
|
<div className="text-[11px] text-emerald-400/80 mt-1">
|
||||||
|
Stripe Charge ID: ch_3P18f92Ksk921
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
|
Select Payment Method
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isProcessingStripe}
|
||||||
|
onClick={() => handleStripePay('Stripe Card')}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl flex items-center justify-between transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-medium text-slate-200">
|
||||||
|
<CreditCard className="w-4 h-4 text-teal-400" />
|
||||||
|
Credit / Debit Card (Apple Pay)
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-teal-500/20 text-teal-300 px-2 py-0.5 rounded font-mono">
|
||||||
|
Instant
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isProcessingStripe}
|
||||||
|
onClick={() => handleStripePay('HSA/FSA')}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl flex items-center justify-between transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-medium text-slate-200">
|
||||||
|
<ShieldAlert className="w-4 h-4 text-sky-400" />
|
||||||
|
HSA / FSA Health Benefit Card
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-sky-500/20 text-sky-300 px-2 py-0.5 rounded font-mono">
|
||||||
|
Tax-Free
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isProcessingStripe}
|
||||||
|
onClick={() => handleStripePay('Cash')}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl flex items-center justify-between transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-medium text-slate-200">
|
||||||
|
<DollarSign className="w-4 h-4 text-emerald-400" />
|
||||||
|
Cash / In-Person Check
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 px-2 py-0.5 rounded font-mono">
|
||||||
|
No Fee
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!paymentSuccess && (
|
||||||
|
<div className="mt-5 pt-3 border-t border-slate-800 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPaymentModal(false)}
|
||||||
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl transition"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Appointment, ClinicTenant, Provider } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
Plus,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
PlayCircle,
|
||||||
|
XCircle,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Filter,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface CalendarViewProps {
|
||||||
|
appointments: Appointment[];
|
||||||
|
providers: Provider[];
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
onSelectAppointment: (apt: Appointment) => void;
|
||||||
|
onUpdateStatus: (aptId: string, status: Appointment['status']) => void;
|
||||||
|
onNewAppointmentClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CalendarView: React.FC<CalendarViewProps> = ({
|
||||||
|
appointments,
|
||||||
|
providers,
|
||||||
|
activeTenant,
|
||||||
|
onSelectAppointment,
|
||||||
|
onUpdateStatus,
|
||||||
|
onNewAppointmentClick,
|
||||||
|
}) => {
|
||||||
|
const [selectedProviderId, setSelectedProviderId] = useState<string>('all');
|
||||||
|
const [activeDateStr, setActiveDateStr] = useState<string>('2026-09-05');
|
||||||
|
|
||||||
|
const filteredAppointments = appointments.filter(
|
||||||
|
(a) => selectedProviderId === 'all' || a.providerId === selectedProviderId
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStatusBadge = (status: Appointment['status']) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[11px] font-semibold flex items-center gap-1">
|
||||||
|
<CheckCircle className="w-3 h-3" /> Completed
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case 'in_room':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-amber-500/10 text-amber-400 border border-amber-500/20 text-[11px] font-semibold flex items-center gap-1 animate-pulse">
|
||||||
|
<PlayCircle className="w-3 h-3" /> In Operatory
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case 'arrived':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-sky-500/10 text-sky-400 border border-sky-500/20 text-[11px] font-semibold flex items-center gap-1">
|
||||||
|
<User className="w-3 h-3" /> Arrived / Waiting
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case 'confirmed':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-teal-500/10 text-teal-400 border border-teal-500/20 text-[11px] font-semibold flex items-center gap-1">
|
||||||
|
<CalendarIcon className="w-3 h-3" /> Confirmed
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case 'booked':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 border border-slate-700 text-[11px] font-semibold">
|
||||||
|
Booked
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case 'no_show':
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 rounded-md bg-red-500/10 text-red-400 border border-red-500/20 text-[11px] font-semibold flex items-center gap-1">
|
||||||
|
<XCircle className="w-3 h-3" /> No Show
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Action Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900 border border-slate-800 p-4 rounded-2xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center bg-slate-800/80 rounded-xl p-1 border border-slate-700/60">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-1.5 hover:bg-slate-700 rounded-lg text-slate-400 hover:text-white transition"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<span className="px-3 text-xs font-bold text-white tracking-wide">
|
||||||
|
Saturday, September 5, 2026
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-1.5 hover:bg-slate-700 rounded-lg text-slate-400 hover:text-white transition"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Filter */}
|
||||||
|
<div className="flex items-center gap-1 bg-slate-800/50 p-1 rounded-xl border border-slate-700/60 text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedProviderId('all')}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium transition ${
|
||||||
|
selectedProviderId === 'all'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Providers
|
||||||
|
</button>
|
||||||
|
{providers.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedProviderId(p.id)}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium transition ${
|
||||||
|
selectedProviderId === p.id
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onNewAppointmentClick}
|
||||||
|
className="px-4 py-2 rounded-xl bg-teal-500 hover:bg-teal-400 text-slate-950 text-xs font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 transition shrink-0"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Book New Patient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Appointments */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{filteredAppointments.map((apt) => (
|
||||||
|
<div
|
||||||
|
key={apt.id}
|
||||||
|
className="bg-slate-900 border border-slate-800 hover:border-teal-500/40 rounded-2xl p-4 shadow-xl transition-all duration-200 group flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* Top Row: Time & Status */}
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-teal-500/10 text-teal-400 font-mono text-xs font-bold">
|
||||||
|
<Clock className="w-3.5 h-3.5 inline mr-1" />
|
||||||
|
{apt.time}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-slate-400 font-medium">
|
||||||
|
{apt.durationMinutes} min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(apt.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Patient & Service Details */}
|
||||||
|
<div className="mt-3">
|
||||||
|
<h4 className="text-sm font-bold text-white group-hover:text-teal-300 transition">
|
||||||
|
{apt.patientName}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-teal-400/90 font-medium mt-0.5">
|
||||||
|
{apt.serviceType}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 text-[11px] text-slate-400 mt-2">
|
||||||
|
<span>Provider: {apt.providerName}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{apt.room}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apt.notes && (
|
||||||
|
<div className="mt-2.5 p-2 bg-slate-950/60 rounded-lg border border-slate-800/80 text-[11px] text-slate-300 italic">
|
||||||
|
"{apt.notes}"
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Bar */}
|
||||||
|
<div className="mt-4 pt-3 border-t border-slate-800 flex items-center justify-between gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectAppointment(apt)}
|
||||||
|
className="px-3 py-1.5 rounded-lg bg-teal-500/10 hover:bg-teal-500/20 text-teal-300 text-xs font-semibold border border-teal-500/30 transition flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<PlayCircle className="w-3.5 h-3.5" />
|
||||||
|
Open SOAP Chart
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{apt.status === 'booked' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdateStatus(apt.id, 'arrived')}
|
||||||
|
className="px-2.5 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-[11px] font-medium"
|
||||||
|
>
|
||||||
|
Check In
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{apt.status === 'arrived' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdateStatus(apt.id, 'in_room')}
|
||||||
|
className="px-2.5 py-1 rounded-lg bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-[11px] font-medium border border-amber-500/30"
|
||||||
|
>
|
||||||
|
Move to Table
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{apt.status === 'in_room' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdateStatus(apt.id, 'completed')}
|
||||||
|
className="px-2.5 py-1 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 text-[11px] font-medium border border-emerald-500/30"
|
||||||
|
>
|
||||||
|
Mark Done
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { SoapNote, Patient, Provider, SpinalAdjustmentEntry, CptCodeItem, Icd10CodeItem } from '@/types/clinical';
|
||||||
|
import { SpineVisualizer } from '@/components/ui/SpineVisualizer';
|
||||||
|
import { AmbientAudioRecorder } from '@/components/ui/AmbientAudioRecorder';
|
||||||
|
import { STANDARD_CPT_CODES, STANDARD_ICD10_CODES } from '@/lib/mock-data';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Sparkles,
|
||||||
|
CheckCircle,
|
||||||
|
Copy,
|
||||||
|
PenTool,
|
||||||
|
Save,
|
||||||
|
DollarSign,
|
||||||
|
ChevronLeft,
|
||||||
|
Tag,
|
||||||
|
ShieldCheck,
|
||||||
|
AlertCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface SoapChartEditorProps {
|
||||||
|
patient: Patient;
|
||||||
|
provider: Provider;
|
||||||
|
initialSoapNote?: SoapNote;
|
||||||
|
onSaveSoapNote: (note: SoapNote) => void;
|
||||||
|
onGenerateSuperbill: (note: SoapNote) => void;
|
||||||
|
onBackToCalendar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
||||||
|
patient,
|
||||||
|
provider,
|
||||||
|
initialSoapNote,
|
||||||
|
onSaveSoapNote,
|
||||||
|
onGenerateSuperbill,
|
||||||
|
onBackToCalendar,
|
||||||
|
}) => {
|
||||||
|
const [subjective, setSubjective] = useState(
|
||||||
|
initialSoapNote?.subjective ||
|
||||||
|
`Patient presents for scheduled visit ${patient.carePlan.completedVisits + 1} of ${
|
||||||
|
patient.carePlan.totalVisits
|
||||||
|
}. Chief complaint: ${patient.chiefComplaint}. VAS pain reported at 4/10.`
|
||||||
|
);
|
||||||
|
const [objective, setObjective] = useState(
|
||||||
|
initialSoapNote?.objective ||
|
||||||
|
'Postural examination reveals mild anterior head carriage and unleveling of right iliac crest. Motion palpation identifies segmental hypomobility with paraspinal guarding.'
|
||||||
|
);
|
||||||
|
const [assessment, setAssessment] = useState(
|
||||||
|
initialSoapNote?.assessment ||
|
||||||
|
'Segmental and somatic dysfunction of lumbar and cervical spine. Patient progressing steadily toward functional goals outlined in care plan.'
|
||||||
|
);
|
||||||
|
const [plan, setPlan] = useState(
|
||||||
|
initialSoapNote?.plan ||
|
||||||
|
'1. High velocity low amplitude (HVLA) manipulative therapy delivered to listed subluxations.\n2. Prescribed postural home stabilization exercises.\n3. Return in 3 days for scheduled care plan visit.'
|
||||||
|
);
|
||||||
|
const [adjustments, setAdjustments] = useState<SpinalAdjustmentEntry[]>(
|
||||||
|
initialSoapNote?.spinalAdjustments || [
|
||||||
|
{ vertebra: 'L4', region: 'Lumbar', listing: 'Right Posterior (RP)', technique: 'Diversified' },
|
||||||
|
{ vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Sacral Base Anterior', technique: 'Thompson Drop' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
const [selectedIcd10, setSelectedIcd10] = useState<Icd10CodeItem[]>(
|
||||||
|
initialSoapNote?.icd10Codes || [STANDARD_ICD10_CODES[2], STANDARD_ICD10_CODES[5]] // M99.03 & M54.50
|
||||||
|
);
|
||||||
|
const [selectedCpt, setSelectedCpt] = useState<CptCodeItem[]>(
|
||||||
|
initialSoapNote?.cptCodes || [STANDARD_CPT_CODES[0], STANDARD_CPT_CODES[3]] // 98940 & 97140
|
||||||
|
);
|
||||||
|
const [vasScore, setVasScore] = useState<number>(initialSoapNote?.vasScore || 3);
|
||||||
|
const [isSigned, setIsSigned] = useState<boolean>(initialSoapNote?.status === 'signed');
|
||||||
|
const [signedTimestamp, setSignedTimestamp] = useState<string | undefined>(initialSoapNote?.signedAt);
|
||||||
|
|
||||||
|
// Ambient AI Scribe callback
|
||||||
|
const handleApplyExtractedSoap = (data: {
|
||||||
|
vasScore: number;
|
||||||
|
subjective: string;
|
||||||
|
objective: string;
|
||||||
|
assessment: string;
|
||||||
|
plan: string;
|
||||||
|
spinalAdjustments: SpinalAdjustmentEntry[];
|
||||||
|
icd10: string[];
|
||||||
|
cpt: string[];
|
||||||
|
}) => {
|
||||||
|
setVasScore(data.vasScore);
|
||||||
|
setSubjective(data.subjective);
|
||||||
|
setObjective(data.objective);
|
||||||
|
setAssessment(data.assessment);
|
||||||
|
setPlan(data.plan);
|
||||||
|
|
||||||
|
if (data.spinalAdjustments && data.spinalAdjustments.length > 0) {
|
||||||
|
setAdjustments(data.spinalAdjustments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto map ICD10
|
||||||
|
const matchedIcds = STANDARD_ICD10_CODES.filter((icd) => data.icd10.includes(icd.code));
|
||||||
|
if (matchedIcds.length > 0) setSelectedIcd10(matchedIcds);
|
||||||
|
|
||||||
|
// Auto map CPT
|
||||||
|
const matchedCpts = STANDARD_CPT_CODES.filter((cpt) => data.cpt.includes(cpt.code));
|
||||||
|
if (matchedCpts.length > 0) setSelectedCpt(matchedCpts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAdjustment = (entry: SpinalAdjustmentEntry) => {
|
||||||
|
setAdjustments((prev) => {
|
||||||
|
const exists = prev.some((a) => a.vertebra === entry.vertebra);
|
||||||
|
if (exists) {
|
||||||
|
return prev.filter((a) => a.vertebra !== entry.vertebra);
|
||||||
|
} else {
|
||||||
|
return [...prev, entry];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloneLastNote = () => {
|
||||||
|
setSubjective('Patient reports continued symptom improvement following last spinal adjustment. Morning stiffness resolved within 10 minutes. Current pain rated 3/10.');
|
||||||
|
setObjective('Palpation reveals decreased tone in lumbar paraspinals. Persistent fixation noted at L4-L5 with right sacral torsion.');
|
||||||
|
setAssessment('Care plan compliance high. Significant restoration of active range of motion noted.');
|
||||||
|
setPlan('1. Diversified adjustment delivered to indicated segments.\n2. Manual myofascial release 10 minutes.\n3. Continue active home rehabilitation.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignChart = () => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
setIsSigned(true);
|
||||||
|
setSignedTimestamp(now);
|
||||||
|
|
||||||
|
const savedNote: SoapNote = {
|
||||||
|
id: initialSoapNote?.id || `soap-${Date.now()}`,
|
||||||
|
tenantId: patient.tenantId,
|
||||||
|
patientId: patient.id,
|
||||||
|
patientName: `${patient.firstName} ${patient.lastName}`,
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
date: '2026-09-05',
|
||||||
|
status: 'signed',
|
||||||
|
vasScore,
|
||||||
|
subjective,
|
||||||
|
objective,
|
||||||
|
assessment,
|
||||||
|
plan,
|
||||||
|
spinalAdjustments: adjustments,
|
||||||
|
icd10Codes: selectedIcd10,
|
||||||
|
cptCodes: selectedCpt,
|
||||||
|
signedAt: now,
|
||||||
|
signedBy: `${provider.name}, ${provider.credentials} (NPI ${provider.npi})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSaveSoapNote(savedNote);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSuperbillClick = () => {
|
||||||
|
const savedNote: SoapNote = {
|
||||||
|
id: initialSoapNote?.id || `soap-${Date.now()}`,
|
||||||
|
tenantId: patient.tenantId,
|
||||||
|
patientId: patient.id,
|
||||||
|
patientName: `${patient.firstName} ${patient.lastName}`,
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
date: '2026-09-05',
|
||||||
|
status: isSigned ? 'signed' : 'draft',
|
||||||
|
vasScore,
|
||||||
|
subjective,
|
||||||
|
objective,
|
||||||
|
assessment,
|
||||||
|
plan,
|
||||||
|
spinalAdjustments: adjustments,
|
||||||
|
icd10Codes: selectedIcd10,
|
||||||
|
cptCodes: selectedCpt,
|
||||||
|
signedAt: signedTimestamp,
|
||||||
|
signedBy: `${provider.name}, ${provider.credentials}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
onGenerateSuperbill(savedNote);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Breadcrumb & Patient Header */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBackToCalendar}
|
||||||
|
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl transition"
|
||||||
|
title="Back to Calendar"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-black text-white">
|
||||||
|
{patient.firstName} {patient.lastName}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-full bg-teal-500/20 text-teal-300 border border-teal-500/30 font-semibold">
|
||||||
|
Care Plan: Visit {patient.carePlan.completedVisits + 1} of {patient.carePlan.totalVisits}
|
||||||
|
</span>
|
||||||
|
{isSigned ? (
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold flex items-center gap-1">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5" /> Signed & Locked
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/30 font-semibold">
|
||||||
|
In Progress (Draft)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-400 mt-1">
|
||||||
|
<span>DOB: {patient.dob}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Insurance: {patient.insuranceName}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="text-teal-400 font-medium">Attending: {provider.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCloneLastNote}
|
||||||
|
className="px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold flex items-center gap-1.5 transition"
|
||||||
|
>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
Clone Last Note
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSignChart}
|
||||||
|
disabled={isSigned}
|
||||||
|
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-1.5 shadow-lg transition ${
|
||||||
|
isSigned
|
||||||
|
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 cursor-default'
|
||||||
|
: 'bg-teal-500 hover:bg-teal-400 text-slate-950 shadow-teal-500/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
{isSigned ? 'Signed by Doctor' : 'Sign & Complete Note'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateSuperbillClick}
|
||||||
|
className="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold flex items-center gap-1.5 shadow-lg shadow-indigo-600/20 transition"
|
||||||
|
>
|
||||||
|
<DollarSign className="w-4 h-4" />
|
||||||
|
Generate Superbill
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ambient AI Clinical Scribe Section */}
|
||||||
|
<AmbientAudioRecorder onApplyExtractedSoap={handleApplyExtractedSoap} />
|
||||||
|
|
||||||
|
{/* 2D Spine Visualizer Subluxation Clicker */}
|
||||||
|
<SpineVisualizer
|
||||||
|
adjustments={adjustments}
|
||||||
|
onToggleAdjustment={handleToggleAdjustment}
|
||||||
|
readOnly={isSigned}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Structured SOAP Fields Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{/* Subjective */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
|
||||||
|
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
|
||||||
|
S
|
||||||
|
</span>
|
||||||
|
Subjective Complaint & History
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-slate-400">VAS: {vasScore}/10</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={subjective}
|
||||||
|
disabled={isSigned}
|
||||||
|
onChange={(e) => setSubjective(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
|
||||||
|
placeholder="Patient reports symptoms, VAS rating, changes since last visit..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-slate-500 mt-2">
|
||||||
|
Auto-populated from Intake and Ambient Scribe.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Objective */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
|
||||||
|
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
|
||||||
|
O
|
||||||
|
</span>
|
||||||
|
Objective Findings & Palpation
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-slate-400">
|
||||||
|
{adjustments.length} Segment(s) Listed
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={objective}
|
||||||
|
disabled={isSigned}
|
||||||
|
onChange={(e) => setObjective(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
|
||||||
|
placeholder="Physical findings, spinal fixations, muscle spasms, range of motion..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-slate-500 mt-2">
|
||||||
|
Vertebrae selected in 2D Spine Map sync automatically.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assessment */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
|
||||||
|
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
|
||||||
|
A
|
||||||
|
</span>
|
||||||
|
Clinical Assessment & Progress
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={assessment}
|
||||||
|
disabled={isSigned}
|
||||||
|
onChange={(e) => setAssessment(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
|
||||||
|
placeholder="Clinical impression, diagnosis evaluation, treatment response..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-slate-500 mt-2">
|
||||||
|
Maps to ICD-10 diagnostic codes below.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plan */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
|
||||||
|
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
|
||||||
|
P
|
||||||
|
</span>
|
||||||
|
Treatment Plan & Home Care
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={plan}
|
||||||
|
disabled={isSigned}
|
||||||
|
onChange={(e) => setPlan(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
|
||||||
|
placeholder="Techniques delivered, rehabilitation modalities, next visit schedule..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-slate-500 mt-2">
|
||||||
|
Determines CPT procedure line items on Superbill.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ICD-10 & CPT Billing Code Selectors */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{/* ICD-10 Codes */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-3">
|
||||||
|
<span className="text-xs font-bold text-slate-200 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Tag className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
ICD-10 Diagnostic Codes
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-teal-400 font-mono">{selectedIcd10.length} Selected</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto pr-1">
|
||||||
|
{STANDARD_ICD10_CODES.map((item) => {
|
||||||
|
const active = selectedIcd10.some((c) => c.code === item.code);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.code}
|
||||||
|
type="button"
|
||||||
|
disabled={isSigned}
|
||||||
|
onClick={() => {
|
||||||
|
if (active) {
|
||||||
|
setSelectedIcd10((prev) => prev.filter((c) => c.code !== item.code));
|
||||||
|
} else {
|
||||||
|
setSelectedIcd10((prev) => [...prev, item]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`px-2.5 py-1.5 rounded-lg text-xs font-medium text-left transition ${
|
||||||
|
active
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow-sm'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-mono font-bold mr-1">{item.code}</span>
|
||||||
|
<span className="text-[11px] opacity-90 truncate">{item.description}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CPT Codes */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl">
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-3">
|
||||||
|
<span className="text-xs font-bold text-slate-200 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<DollarSign className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
CPT Procedure Codes & Fee Schedule
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-teal-400 font-mono">
|
||||||
|
Total: ${selectedCpt.reduce((sum, c) => sum + c.fee, 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto pr-1">
|
||||||
|
{STANDARD_CPT_CODES.map((item) => {
|
||||||
|
const active = selectedCpt.some((c) => c.code === item.code);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.code}
|
||||||
|
type="button"
|
||||||
|
disabled={isSigned}
|
||||||
|
onClick={() => {
|
||||||
|
if (active) {
|
||||||
|
setSelectedCpt((prev) => prev.filter((c) => c.code !== item.code));
|
||||||
|
} else {
|
||||||
|
setSelectedCpt((prev) => [...prev, item]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`px-2.5 py-1.5 rounded-lg text-xs font-medium text-left transition flex items-center justify-between gap-2 ${
|
||||||
|
active
|
||||||
|
? 'bg-indigo-600 text-white font-bold shadow-sm'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-mono font-bold mr-1">{item.code}</span>
|
||||||
|
<span className="text-[11px] opacity-90 truncate">{item.description}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-teal-300 shrink-0 font-bold">${item.fee}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signature Attestation Footer */}
|
||||||
|
{isSigned && (
|
||||||
|
<div className="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-2xl flex items-center justify-between text-xs text-emerald-300">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-emerald-400" />
|
||||||
|
<div>
|
||||||
|
<strong>Electronically Signed by:</strong> {provider.name}, {provider.credentials} (NPI {provider.npi})
|
||||||
|
<div className="text-[11px] text-emerald-400/80 mt-0.5">
|
||||||
|
Timestamp: {signedTimestamp || '2026-09-05T09:25:00-07:00'} • Immutable Legal Record
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateSuperbillClick}
|
||||||
|
className="px-3 py-1.5 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold rounded-lg transition"
|
||||||
|
>
|
||||||
|
Open Superbill
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ClinicTenant, Provider } from '@/types/clinical';
|
||||||
|
import { BodyPainMap } from '@/components/ui/BodyPainMap';
|
||||||
|
import { DigitalSignaturePad } from '@/components/ui/DigitalSignaturePad';
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
CheckCircle2,
|
||||||
|
Sparkles,
|
||||||
|
MapPin,
|
||||||
|
Phone,
|
||||||
|
ArrowRight,
|
||||||
|
ShieldCheck,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface PatientPortalViewProps {
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
providers: Provider[];
|
||||||
|
onBookingComplete: (newAppointmentData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PatientPortalView: React.FC<PatientPortalViewProps> = ({
|
||||||
|
activeTenant,
|
||||||
|
providers,
|
||||||
|
onBookingComplete,
|
||||||
|
}) => {
|
||||||
|
const [step, setStep] = useState<'service' | 'intake' | 'confirmed'>('service');
|
||||||
|
|
||||||
|
// Booking selections
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0]);
|
||||||
|
const [selectedService, setSelectedService] = useState('Initial Chiropractic Exam & Adjustment');
|
||||||
|
const [selectedDate, setSelectedDate] = useState('2026-09-08');
|
||||||
|
const [selectedTime, setSelectedTime] = useState('10:30 AM');
|
||||||
|
|
||||||
|
// Intake form state
|
||||||
|
const [fullName, setFullName] = useState('Jessica Morales');
|
||||||
|
const [dob, setDob] = useState('1991-08-14');
|
||||||
|
const [phone, setPhone] = useState('(555) 302-9912');
|
||||||
|
const [email, setEmail] = useState('jess.morales@example.com');
|
||||||
|
const [selectedPainAreas, setSelectedPainAreas] = useState<string[]>([
|
||||||
|
'Cervical Spine (Neck)',
|
||||||
|
'Left Trapezius / Shoulder',
|
||||||
|
]);
|
||||||
|
const [vasScore, setVasScore] = useState<number>(6);
|
||||||
|
const [painNatures, setPainNatures] = useState<string[]>([
|
||||||
|
'Sharp / Stabbing',
|
||||||
|
'Tingling / Pins & Needles',
|
||||||
|
]);
|
||||||
|
const [signature, setSignature] = useState<string>('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleToggleArea = (area: string) => {
|
||||||
|
setSelectedPainAreas((prev) =>
|
||||||
|
prev.includes(area) ? prev.filter((a) => a !== area) : [...prev, area]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleNature = (nature: string) => {
|
||||||
|
setPainNatures((prev) =>
|
||||||
|
prev.includes(nature) ? prev.filter((n) => n !== nature) : [...prev, nature]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitIntake = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setStep('confirmed');
|
||||||
|
onBookingComplete({
|
||||||
|
patientName: fullName,
|
||||||
|
patientPhone: phone,
|
||||||
|
providerId: selectedProvider.id,
|
||||||
|
providerName: selectedProvider.name,
|
||||||
|
date: selectedDate,
|
||||||
|
time: selectedTime,
|
||||||
|
serviceType: selectedService,
|
||||||
|
status: 'booked',
|
||||||
|
room: 'Table 1',
|
||||||
|
fee: 145,
|
||||||
|
notes: `Intake complete. Chief complaint: Neck/Shoulder pain (VAS ${vasScore}/10).`,
|
||||||
|
});
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
{/* Branded Clinic Header (100% White-Label) */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-2xl flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="w-3.5 h-3.5 rounded-full"
|
||||||
|
style={{ backgroundColor: activeTenant.brandColor }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs font-mono font-bold tracking-widest uppercase text-slate-400">
|
||||||
|
{activeTenant.logoText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-black text-white mt-1">{activeTenant.name}</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-400 mt-2">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
{activeTenant.address}, {activeTenant.cityStateZip}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Phone className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
{activeTenant.phone}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 bg-slate-800/60 px-3 py-1.5 rounded-xl border border-slate-700/60 text-xs">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-teal-400" />
|
||||||
|
<span className="text-slate-300">White-Label Patient Portal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Progress Steps */}
|
||||||
|
<div className="flex items-center justify-center gap-3 text-xs">
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-full font-semibold transition ${
|
||||||
|
step === 'service'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold'
|
||||||
|
: 'bg-slate-800 text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>1. Select Service & Time</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-600">→</span>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-full font-semibold transition ${
|
||||||
|
step === 'intake'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold'
|
||||||
|
: 'bg-slate-800 text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>2. Paperless Intake & Pain Map</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-600">→</span>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-full font-semibold transition ${
|
||||||
|
step === 'confirmed'
|
||||||
|
? 'bg-emerald-500 text-slate-950 font-bold'
|
||||||
|
: 'bg-slate-800 text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>3. Confirmed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 1: Select Service & Doctor */}
|
||||||
|
{step === 'service' && (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white">Choose Your Chiropractic Provider</h3>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
Select a doctor licensed in state-of-the-art spinal biomechanics and gentle adjustment techniques.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||||
|
{providers.map((p) => {
|
||||||
|
const isChosen = selectedProvider.id === p.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedProvider(p)}
|
||||||
|
className={`flex items-center gap-3 p-4 rounded-xl border text-left transition ${
|
||||||
|
isChosen
|
||||||
|
? 'bg-slate-800 border-teal-500/80 text-white shadow-lg'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 text-slate-300 hover:bg-slate-800/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-11 h-11 rounded-full flex items-center justify-center font-bold text-sm text-white shrink-0"
|
||||||
|
style={{ backgroundColor: p.color }}
|
||||||
|
>
|
||||||
|
{p.name
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-sm text-white">{p.name}</div>
|
||||||
|
<div className="text-xs text-teal-400 font-medium">{p.title}</div>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5">{p.specialty}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white">Select Appointment Type</h3>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-3 text-xs">
|
||||||
|
{[
|
||||||
|
{ name: 'Initial Chiropractic Exam & Adjustment', dur: '45 mins', fee: '$145' },
|
||||||
|
{ name: 'Follow-Up Spinal CMT & Decompression', dur: '20 mins', fee: '$75' },
|
||||||
|
{ name: 'Triton Computerized Disc Decompression', dur: '30 mins', fee: '$95' },
|
||||||
|
].map((svc) => (
|
||||||
|
<button
|
||||||
|
key={svc.name}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedService(svc.name)}
|
||||||
|
className={`p-4 rounded-xl border text-left transition flex flex-col justify-between ${
|
||||||
|
selectedService === svc.name
|
||||||
|
? 'bg-teal-500/10 border-teal-500/80 text-white shadow'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 text-slate-400 hover:bg-slate-800/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-sm text-slate-200">{svc.name}</div>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-1">{svc.dur}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-bold text-teal-400 font-mono mt-3">{svc.fee}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-white">Pick Date & Available Slot</h3>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mt-3 text-xs">
|
||||||
|
{['09:00 AM', '10:30 AM', '01:15 PM', '03:45 PM'].map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTime(t)}
|
||||||
|
className={`p-3 rounded-xl border text-center font-bold transition font-mono ${
|
||||||
|
selectedTime === t
|
||||||
|
? 'bg-teal-500 text-slate-950 shadow-md shadow-teal-500/20'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 text-slate-300 hover:bg-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Clock className="w-3.5 h-3.5 inline mr-1" />
|
||||||
|
{t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-slate-800 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStep('intake')}
|
||||||
|
className="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-slate-950 text-xs font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 transition"
|
||||||
|
>
|
||||||
|
Continue to Digital Intake
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Paperless Intake & Body Pain Map */}
|
||||||
|
{step === 'intake' && (
|
||||||
|
<form onSubmit={handleSubmitIntake} className="space-y-6">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-white">Patient Demographics</h3>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1">Full Legal Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1">Date of Birth</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={dob}
|
||||||
|
onChange={(e) => setDob(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1">Mobile Phone (for SMS Reminders)</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-400 mb-1">Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Pain Map */}
|
||||||
|
<BodyPainMap
|
||||||
|
selectedAreas={selectedPainAreas}
|
||||||
|
onToggleArea={handleToggleArea}
|
||||||
|
vasScore={vasScore}
|
||||||
|
onChangeVasScore={setVasScore}
|
||||||
|
painNatures={painNatures}
|
||||||
|
onTogglePainNature={handleToggleNature}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Signature & Consent */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-white">HIPAA & Chiropractic Informed Consent</h3>
|
||||||
|
<p className="text-xs text-slate-400 leading-relaxed">
|
||||||
|
I hereby authorize {activeTenant.name} and attending doctors to examine and administer
|
||||||
|
chiropractic adjustments and physical modalities as deemed necessary. I understand that,
|
||||||
|
as with any healthcare procedure, chiropractic care carries potential benefits and risks.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<DigitalSignaturePad onSaveSignature={setSignature} signerName={fullName} />
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStep('service')}
|
||||||
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl transition"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-6 py-3 bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 text-xs font-bold rounded-xl shadow-lg shadow-teal-500/20 transition flex items-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<span className="w-4 h-4 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
|
||||||
|
Submitting to Doctor's EHR...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
Confirm Appointment & Submit Intake
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Confirmed Screen */}
|
||||||
|
{step === 'confirmed' && (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-8 shadow-2xl text-center space-y-4 max-w-lg mx-auto">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto border border-emerald-500/40">
|
||||||
|
<CheckCircle2 className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-xl font-black text-white">You're All Set, {fullName}!</h3>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">
|
||||||
|
Your appointment has been confirmed with <strong>{selectedProvider.name}</strong> at{' '}
|
||||||
|
<strong>{activeTenant.name}</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-slate-950 p-4 rounded-xl border border-slate-800 text-xs space-y-2 text-left">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">Date & Time:</span>
|
||||||
|
<span className="font-bold text-white font-mono">
|
||||||
|
{selectedDate} at {selectedTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">Service:</span>
|
||||||
|
<span className="font-bold text-teal-400">{selectedService}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-400">Paperless Intake:</span>
|
||||||
|
<span className="font-bold text-emerald-400">Completed & Signed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-teal-500/10 border border-teal-500/20 rounded-xl text-xs text-teal-300">
|
||||||
|
A confirmation SMS with calendar link and operatory directions has been sent to{' '}
|
||||||
|
<strong>{phone}</strong>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStep('service')}
|
||||||
|
className="mt-4 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl transition"
|
||||||
|
>
|
||||||
|
Book Another Patient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Patient, ClinicTenant } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Users,
|
||||||
|
AlertTriangle,
|
||||||
|
MessageSquare,
|
||||||
|
TrendingUp,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
Sparkles,
|
||||||
|
PhoneCall,
|
||||||
|
Calendar,
|
||||||
|
Send,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface RetentionViewProps {
|
||||||
|
patients: Patient[];
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
onSelectPatientChart: (patient: Patient) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RetentionView: React.FC<RetentionViewProps> = ({
|
||||||
|
patients,
|
||||||
|
activeTenant,
|
||||||
|
onSelectPatientChart,
|
||||||
|
}) => {
|
||||||
|
const [selectedPatientForSms, setSelectedPatientForSms] = useState<Patient | null>(null);
|
||||||
|
const [customSmsMessage, setCustomSmsMessage] = useState('');
|
||||||
|
const [isSendingSms, setIsSendingSms] = useState(false);
|
||||||
|
const [sentToast, setSentToast] = useState(false);
|
||||||
|
const [filterType, setFilterType] = useState<'all' | 'dropout_risk' | 'active'>('dropout_risk');
|
||||||
|
|
||||||
|
const dropoutPatients = patients.filter((p) => p.status === 'dropout_risk');
|
||||||
|
const activePatients = patients.filter((p) => p.status === 'active');
|
||||||
|
|
||||||
|
const displayedPatients =
|
||||||
|
filterType === 'dropout_risk'
|
||||||
|
? dropoutPatients
|
||||||
|
: filterType === 'active'
|
||||||
|
? activePatients
|
||||||
|
: patients;
|
||||||
|
|
||||||
|
const handleOpenSmsModal = (patient: Patient) => {
|
||||||
|
setSelectedPatientForSms(patient);
|
||||||
|
setCustomSmsMessage(
|
||||||
|
`Hi ${patient.firstName}! Dr. Vance noticed you're due for visit ${
|
||||||
|
patient.carePlan.completedVisits + 1
|
||||||
|
} of your ${patient.carePlan.title}. We want to make sure your progress stays on track! Click here to pick a quick 15-min adjustment slot this week: https://${
|
||||||
|
activeTenant.domain
|
||||||
|
}/book?ref=careplan`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendSms = () => {
|
||||||
|
setIsSendingSms(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsSendingSms(false);
|
||||||
|
setSentToast(true);
|
||||||
|
if (selectedPatientForSms) {
|
||||||
|
selectedPatientForSms.status = 'reactivated';
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setSentToast(false);
|
||||||
|
setSelectedPatientForSms(null);
|
||||||
|
}, 1500);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Value Metric Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl shadow-lg">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Care Plan Retention</span>
|
||||||
|
<TrendingUp className="w-4 h-4 text-teal-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-white mt-2">84.2%</div>
|
||||||
|
<p className="text-[11px] text-teal-400 mt-1 flex items-center gap-1 font-medium">
|
||||||
|
<span>+18% higher than Jane App</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl shadow-lg">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Patients at Dropout Risk</span>
|
||||||
|
<AlertTriangle className="w-4 h-4 text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-amber-400 mt-2">
|
||||||
|
{dropoutPatients.length} Patients
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-400 mt-1">Missed expected cadence > 10 days</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl shadow-lg">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Revenue at Risk</span>
|
||||||
|
<span className="text-xs font-mono text-red-400 font-bold">$</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-white mt-2">$2,140.00</div>
|
||||||
|
<p className="text-[11px] text-red-400 mt-1">Unbilled care plan visits</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl shadow-lg">
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Recovered This Month</span>
|
||||||
|
<Sparkles className="w-4 h-4 text-teal-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-emerald-400 mt-2">+$3,850.00</div>
|
||||||
|
<p className="text-[11px] text-emerald-400/80 mt-1">Via AI Autonomous SMS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Table / Patient List Header */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-xl">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-800">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-white text-base flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4 text-teal-400" />
|
||||||
|
Care Plan Adherence & Reactivation Radar
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
Identifies patients who have dropped off their prescribed spinal treatment plans before completing their clinical goals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 bg-slate-800 p-1 rounded-xl border border-slate-700 text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFilterType('dropout_risk')}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium transition ${
|
||||||
|
filterType === 'dropout_risk'
|
||||||
|
? 'bg-amber-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Dropout Risk ({dropoutPatients.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFilterType('active')}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium transition ${
|
||||||
|
filterType === 'active'
|
||||||
|
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
On Track ({activePatients.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFilterType('all')}
|
||||||
|
className={`px-3 py-1 rounded-lg font-medium transition ${
|
||||||
|
filterType === 'all'
|
||||||
|
? 'bg-indigo-600 text-white font-bold shadow'
|
||||||
|
: 'text-slate-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Patients ({patients.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Patient Rows */}
|
||||||
|
<div className="divide-y divide-slate-800 mt-2">
|
||||||
|
{displayedPatients.map((p) => {
|
||||||
|
const isAtRisk = p.status === 'dropout_risk';
|
||||||
|
const progressPct = Math.round(
|
||||||
|
(p.carePlan.completedVisits / p.carePlan.totalVisits) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="py-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-slate-800/30 px-2 rounded-xl transition"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-bold text-white text-sm">
|
||||||
|
{p.firstName} {p.lastName}
|
||||||
|
</h4>
|
||||||
|
{isAtRisk ? (
|
||||||
|
<span className="px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/30 text-[10px] font-bold uppercase flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3 h-3" /> {p.daysSinceLastVisit} Days Since Visit
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-[10px] font-bold uppercase flex items-center gap-1">
|
||||||
|
<CheckCircle className="w-3 h-3" /> On Track
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-300 mt-1">
|
||||||
|
Care Plan: <strong>{p.carePlan.title}</strong> ({p.carePlan.frequency})
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mt-2 text-xs text-slate-400">
|
||||||
|
<div className="flex items-center gap-2 w-48">
|
||||||
|
<div className="w-full bg-slate-800 rounded-full h-1.5 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full ${
|
||||||
|
isAtRisk ? 'bg-amber-400' : 'bg-teal-400'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${progressPct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-[11px] shrink-0">
|
||||||
|
{p.carePlan.completedVisits}/{p.carePlan.totalVisits}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>Phone: {p.phone}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Last Visit: {p.lastVisitDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectPatientChart(p)}
|
||||||
|
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold rounded-lg transition"
|
||||||
|
>
|
||||||
|
View Chart
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOpenSmsModal(p)}
|
||||||
|
className={`px-3 py-1.5 text-xs font-bold rounded-lg flex items-center gap-1.5 transition ${
|
||||||
|
isAtRisk
|
||||||
|
? 'bg-amber-500 hover:bg-amber-400 text-slate-950 shadow-lg shadow-amber-500/20'
|
||||||
|
: 'bg-teal-500/20 hover:bg-teal-500/30 text-teal-300 border border-teal-500/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
|
{isAtRisk ? 'Send AI Reactivation SMS' : 'Message Patient'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SMS Reactivation Modal */}
|
||||||
|
{selectedPatientForSms && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-lg w-full p-6 shadow-2xl animate-in fade-in zoom-in-95 duration-150 text-slate-100">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-2 rounded-xl bg-amber-500/20 text-amber-300">
|
||||||
|
<MessageSquare className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-white text-sm">
|
||||||
|
Autonomous SMS Reactivation Dispatch
|
||||||
|
</h4>
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
To: {selectedPatientForSms.firstName} {selectedPatientForSms.lastName} ({selectedPatientForSms.phone})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPatientForSms(null)}
|
||||||
|
className="text-slate-400 hover:text-white p-1"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-3 text-xs">
|
||||||
|
<div className="bg-slate-950/80 p-3 rounded-xl border border-slate-800 text-slate-300 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400">Sending Clinic Line:</span>{' '}
|
||||||
|
<strong className="text-white font-mono">{activeTenant.phone}</strong>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-teal-500/20 text-teal-300 px-2 py-0.5 rounded font-bold">
|
||||||
|
AI Pilots Gateway
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">
|
||||||
|
Personalized 2-Way Message (Editable)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={5}
|
||||||
|
value={customSmsMessage}
|
||||||
|
onChange={(e) => setCustomSmsMessage(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-amber-500 resize-none font-sans"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sentToast && (
|
||||||
|
<div className="p-3 bg-emerald-500/20 border border-emerald-500/40 rounded-xl text-xs text-emerald-300 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>SMS Dispatched! Patient status updated to Reactivated.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-3 border-t border-slate-800 flex items-center justify-between">
|
||||||
|
<span className="text-[11px] text-slate-500">
|
||||||
|
Auto-fills calendar slots when patient clicks.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPatientForSms(null)}
|
||||||
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSendingSms}
|
||||||
|
onClick={handleSendSms}
|
||||||
|
className="px-4 py-2 bg-amber-500 hover:bg-amber-400 text-slate-950 text-xs font-bold rounded-xl shadow-lg shadow-amber-500/20 transition flex items-center gap-1.5 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSendingSms ? (
|
||||||
|
<>
|
||||||
|
<span className="w-3.5 h-3.5 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
|
||||||
|
Sending SMS...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
Send Reactivation Text
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Mic, MicOff, Sparkles, Volume2, Play, Pause, RotateCcw, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||||
|
import { SAMPLE_AUDIO_PRESETS } from '@/lib/mock-data';
|
||||||
|
|
||||||
|
interface AmbientAudioRecorderProps {
|
||||||
|
onApplyExtractedSoap: (soapData: {
|
||||||
|
vasScore: number;
|
||||||
|
subjective: string;
|
||||||
|
objective: string;
|
||||||
|
assessment: string;
|
||||||
|
plan: string;
|
||||||
|
spinalAdjustments: Array<{ vertebra: string; region: any; listing: string; technique: any; notes?: string }>;
|
||||||
|
icd10: string[];
|
||||||
|
cpt: string[];
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
|
||||||
|
onApplyExtractedSoap,
|
||||||
|
}) => {
|
||||||
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
const [recordingSeconds, setRecordingSeconds] = useState(0);
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState(SAMPLE_AUDIO_PRESETS[0]);
|
||||||
|
const [isProcessingAI, setIsProcessingAI] = useState(false);
|
||||||
|
const [liveTranscript, setLiveTranscript] = useState('');
|
||||||
|
const [showAppliedToast, setShowAppliedToast] = useState(false);
|
||||||
|
|
||||||
|
// Timer simulation
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: any = null;
|
||||||
|
if (isRecording) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setRecordingSeconds((prev) => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isRecording]);
|
||||||
|
|
||||||
|
const toggleRecording = () => {
|
||||||
|
if (!isRecording) {
|
||||||
|
setIsRecording(true);
|
||||||
|
setRecordingSeconds(0);
|
||||||
|
setLiveTranscript('Listening to ambient doctor-patient dialogue in operatory...');
|
||||||
|
// Simulate live ambient audio transcription
|
||||||
|
setTimeout(() => {
|
||||||
|
setLiveTranscript(selectedPreset.audioTranscript);
|
||||||
|
}, 2500);
|
||||||
|
} else {
|
||||||
|
setIsRecording(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectPreset = (preset: typeof SAMPLE_AUDIO_PRESETS[0]) => {
|
||||||
|
setSelectedPreset(preset);
|
||||||
|
if (!isRecording) {
|
||||||
|
setLiveTranscript(preset.audioTranscript);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSynthesizeSoap = () => {
|
||||||
|
setIsProcessingAI(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsProcessingAI(false);
|
||||||
|
onApplyExtractedSoap({
|
||||||
|
vasScore: selectedPreset.extractedSOAP.vasScore,
|
||||||
|
subjective: selectedPreset.extractedSOAP.subjective,
|
||||||
|
objective: selectedPreset.extractedSOAP.objective,
|
||||||
|
assessment: selectedPreset.extractedSOAP.assessment,
|
||||||
|
plan: selectedPreset.extractedSOAP.plan,
|
||||||
|
spinalAdjustments: selectedPreset.extractedSOAP.spinalAdjustments as any,
|
||||||
|
icd10: selectedPreset.extractedSOAP.icd10,
|
||||||
|
cpt: selectedPreset.extractedSOAP.cpt,
|
||||||
|
});
|
||||||
|
setShowAppliedToast(true);
|
||||||
|
setTimeout(() => setShowAppliedToast(false), 4000);
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (secs: number) => {
|
||||||
|
const mins = Math.floor(secs / 60);
|
||||||
|
const remaining = secs % 60;
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${remaining.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-slate-900 via-slate-900 to-teal-950/40 border border-teal-500/30 rounded-2xl p-5 shadow-2xl text-slate-100">
|
||||||
|
{/* Top Banner */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-800">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-teal-500/20 text-teal-400 border border-teal-500/30">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<h3 className="font-semibold text-white text-base tracking-tight">
|
||||||
|
Mediusa Ambient Clinical AI Scribe
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] font-semibold px-2 py-0.5 rounded-full bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
|
||||||
|
Powered by gpt-5.4-mini Clinical Engine
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
Zero typing required. Scribe listens to ambient doctor-patient encounter and converts conversational dialogue into compliant SOAP + CPT/ICD-10 codes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recording Status / Mic Button */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isRecording && (
|
||||||
|
<div className="flex items-center gap-2 text-xs font-mono font-semibold text-red-400 animate-pulse">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-red-500" />
|
||||||
|
LIVE {formatTime(recordingSeconds)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleRecording}
|
||||||
|
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 transition-all shadow-lg ${
|
||||||
|
isRecording
|
||||||
|
? 'bg-red-500/20 text-red-400 border border-red-500/50 hover:bg-red-500/30'
|
||||||
|
: 'bg-teal-500 hover:bg-teal-400 text-slate-950 shadow-teal-500/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isRecording ? (
|
||||||
|
<>
|
||||||
|
<MicOff className="w-4 h-4" />
|
||||||
|
Stop Listening
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Mic className="w-4 h-4" />
|
||||||
|
Start Ambient Scribe
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Waveform Graphic Animation */}
|
||||||
|
<div className="mt-4 p-4 bg-slate-950/60 rounded-xl border border-slate-800/80">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Volume2 className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
Operatory Ambient Stream
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-500 font-mono">
|
||||||
|
44.1kHz • 24-bit • HIPAA Speech Encryption
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic Animated Bars */}
|
||||||
|
<div className="h-10 flex items-center justify-center gap-1">
|
||||||
|
{[
|
||||||
|
12, 28, 45, 18, 62, 85, 40, 92, 70, 30, 80, 65, 45, 90, 35, 75, 55, 20, 68, 88,
|
||||||
|
42, 78, 52, 22, 60, 95, 38, 72, 58, 25, 48, 82, 32, 64, 44, 15,
|
||||||
|
].map((height, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`w-1 rounded-full transition-all duration-150 ${
|
||||||
|
isRecording
|
||||||
|
? 'bg-gradient-to-t from-teal-500 to-emerald-300 animate-pulse'
|
||||||
|
: 'bg-slate-800'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
height: isRecording ? `${Math.max(10, (height * (1 + (i % 3) * 0.2)) % 40)}px` : '6px',
|
||||||
|
animationDelay: `${(i * 40) % 600}ms`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preset Encounter Selector & Live Transcript View */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 mt-4">
|
||||||
|
{/* Left 4 Cols: Presets */}
|
||||||
|
<div className="md:col-span-4 flex flex-col gap-2">
|
||||||
|
<label className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
|
Clinical Encounter Presets (1-Click Demo)
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{SAMPLE_AUDIO_PRESETS.map((preset) => (
|
||||||
|
<button
|
||||||
|
key={preset.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectPreset(preset)}
|
||||||
|
className={`w-full text-left p-2.5 rounded-xl border transition text-xs ${
|
||||||
|
selectedPreset.id === preset.id
|
||||||
|
? 'bg-slate-800 border-teal-500/60 text-white shadow-sm'
|
||||||
|
: 'bg-slate-900/60 border-slate-800 text-slate-400 hover:bg-slate-800/40 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="font-semibold text-slate-200 truncate">{preset.title}</div>
|
||||||
|
<div className="text-[10px] text-teal-400 mt-0.5">Encounter Duration: {preset.duration}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSynthesizeSoap}
|
||||||
|
disabled={isProcessingAI}
|
||||||
|
className="mt-3 w-full py-2.5 px-4 rounded-xl text-xs font-bold bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 shadow-lg shadow-teal-500/20 transition flex items-center justify-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isProcessingAI ? (
|
||||||
|
<>
|
||||||
|
<span className="w-3.5 h-3.5 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
|
||||||
|
AI Extracting SOAP...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="w-4 h-4 text-slate-950" />
|
||||||
|
Extract SOAP & Auto-Codes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right 8 Cols: Dialogue Transcript */}
|
||||||
|
<div className="md:col-span-8 bg-slate-950/70 border border-slate-800 rounded-xl p-3.5 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-2 border-b border-slate-800 text-xs">
|
||||||
|
<span className="font-semibold text-slate-300">Encounter Dialogue Feed</span>
|
||||||
|
<span className="text-[10px] text-teal-400/90 font-mono">Live Whisper Stream</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 whitespace-pre-line mt-2 font-mono leading-relaxed max-h-[160px] overflow-y-auto pr-1">
|
||||||
|
{liveTranscript || selectedPreset.audioTranscript}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-2 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
|
||||||
|
<span className="flex items-center gap-1 text-teal-400">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
Auto-detects Subluxations, VAS score, CPT 98940, ICD-10 M99.0x
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-500">HIPAA BAA Compliant</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAppliedToast && (
|
||||||
|
<div className="mt-3 p-3 bg-emerald-500/20 border border-emerald-500/40 rounded-xl text-xs text-emerald-300 flex items-center justify-between animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||||
|
<span>
|
||||||
|
<strong>Success!</strong> Encounter conversation extracted into Subjective, Objective, Assessment, Plan, Spine segments, and Billing codes.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Activity, AlertCircle, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface BodyPainMapProps {
|
||||||
|
selectedAreas: string[];
|
||||||
|
onToggleArea: (area: string) => void;
|
||||||
|
vasScore: number;
|
||||||
|
onChangeVasScore: (score: number) => void;
|
||||||
|
painNatures: string[];
|
||||||
|
onTogglePainNature: (nature: string) => void;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BODY_ZONES = [
|
||||||
|
{ id: 'Cervical Spine (Neck)', label: 'Neck / Cervical Spine', side: 'Posterior' },
|
||||||
|
{ id: 'Left Trapezius / Shoulder', label: 'Left Shoulder / Trap', side: 'Posterior' },
|
||||||
|
{ id: 'Right Trapezius / Shoulder', label: 'Right Shoulder / Trap', side: 'Posterior' },
|
||||||
|
{ id: 'Thoracic (Mid-Back)', label: 'Mid-Back / Ribs', side: 'Posterior' },
|
||||||
|
{ id: 'Lumbar Spine (Low Back)', label: 'Lower Back / Lumbar', side: 'Posterior' },
|
||||||
|
{ id: 'Sacroiliac (SI Joint) / Pelvis', label: 'SI Joints / Pelvis', side: 'Posterior' },
|
||||||
|
{ id: 'Right Glute / Sciatic Nerve', label: 'Right Glute & Sciatica', side: 'Posterior' },
|
||||||
|
{ id: 'Left Glute / Sciatic Nerve', label: 'Left Glute & Sciatica', side: 'Posterior' },
|
||||||
|
{ id: 'Head / Occipital', label: 'Head / Base of Skull (Headaches)', side: 'Posterior' },
|
||||||
|
{ id: 'Bilateral Knees', label: 'Knees / Gait Imbalance', side: 'Anterior' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PAIN_NATURE_OPTIONS = [
|
||||||
|
'Sharp / Stabbing',
|
||||||
|
'Dull Chronic Ache',
|
||||||
|
'Burning Heat',
|
||||||
|
'Shooting / Radiating',
|
||||||
|
'Tingling / Pins & Needles',
|
||||||
|
'Numbness / Loss of Sensation',
|
||||||
|
'Throbbing Tension',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BodyPainMap: React.FC<BodyPainMapProps> = ({
|
||||||
|
selectedAreas,
|
||||||
|
onToggleArea,
|
||||||
|
vasScore,
|
||||||
|
onChangeVasScore,
|
||||||
|
painNatures,
|
||||||
|
onTogglePainNature,
|
||||||
|
readOnly = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 text-slate-100 shadow-xl">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-4 border-b border-slate-800">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-white text-sm flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4 text-teal-400" />
|
||||||
|
Interactive Pain & Symptom Locator
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
Tap the areas on your body where you experience pain, tension, or nerve radiation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-slate-400 font-medium">Areas Selected:</span>
|
||||||
|
<span className="px-2.5 py-0.5 rounded-full bg-teal-500/20 text-teal-300 font-bold text-xs border border-teal-500/30">
|
||||||
|
{selectedAreas.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 mt-4">
|
||||||
|
{/* Pain Hotspots Grid (7 cols) */}
|
||||||
|
<div className="md:col-span-7 space-y-2">
|
||||||
|
<label className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider block mb-1">
|
||||||
|
Select Pain Regions
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{BODY_ZONES.map((zone) => {
|
||||||
|
const isSelected = selectedAreas.includes(zone.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={zone.id}
|
||||||
|
type="button"
|
||||||
|
disabled={readOnly}
|
||||||
|
onClick={() => onToggleArea(zone.id)}
|
||||||
|
className={`flex items-center justify-between p-3 rounded-xl border text-left text-xs transition ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-teal-500/20 border-teal-500/60 text-teal-200 shadow-sm'
|
||||||
|
: 'bg-slate-800/60 border-slate-700/60 text-slate-300 hover:bg-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{zone.label}</div>
|
||||||
|
<div className="text-[10px] text-slate-400">{zone.side} Chain</div>
|
||||||
|
</div>
|
||||||
|
{isSelected ? (
|
||||||
|
<span className="w-5 h-5 rounded-full bg-teal-500 text-slate-950 flex items-center justify-center font-bold">
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-slate-700" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VAS Pain Scale & Pain Nature (5 cols) */}
|
||||||
|
<div className="md:col-span-5 bg-slate-800/40 border border-slate-700/60 rounded-xl p-4 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
{/* Visual Analog Scale (VAS) Slider */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-[11px] font-semibold text-slate-300 uppercase tracking-wider">
|
||||||
|
Visual Analog Scale (VAS)
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-black px-2 py-0.5 rounded-lg ${
|
||||||
|
vasScore >= 7
|
||||||
|
? 'bg-red-500/20 text-red-400 border border-red-500/30'
|
||||||
|
: vasScore >= 4
|
||||||
|
? 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
|
||||||
|
: 'bg-teal-500/20 text-teal-400 border border-teal-500/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{vasScore} / 10
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
step="1"
|
||||||
|
value={vasScore}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={(e) => onChangeVasScore(parseInt(e.target.value))}
|
||||||
|
className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-teal-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between text-[10px] text-slate-400 mt-1 font-mono">
|
||||||
|
<span>0 (No Pain)</span>
|
||||||
|
<span>5 (Moderate)</span>
|
||||||
|
<span>10 (Severe)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pain Nature Checkboxes */}
|
||||||
|
<div>
|
||||||
|
<label className="text-[11px] font-semibold text-slate-300 uppercase tracking-wider block mb-2">
|
||||||
|
Nature of Pain Sensation
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{PAIN_NATURE_OPTIONS.map((nature) => {
|
||||||
|
const isChecked = painNatures.includes(nature);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={nature}
|
||||||
|
type="button"
|
||||||
|
disabled={readOnly}
|
||||||
|
onClick={() => onTogglePainNature(nature)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition ${
|
||||||
|
isChecked
|
||||||
|
? 'bg-indigo-600 text-white font-semibold shadow-sm'
|
||||||
|
: 'bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{nature}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-3 border-t border-slate-700/60 text-[11px] text-slate-400 flex items-center gap-1.5">
|
||||||
|
<AlertCircle className="w-3.5 h-3.5 text-teal-400 shrink-0" />
|
||||||
|
<span>Maps directly into Provider SOAP & ICD-10 diagnostic codes.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { PenTool, RotateCcw, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DigitalSignaturePadProps {
|
||||||
|
onSaveSignature: (signatureData: string) => void;
|
||||||
|
signerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DigitalSignaturePad: React.FC<DigitalSignaturePadProps> = ({
|
||||||
|
onSaveSignature,
|
||||||
|
signerName,
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [hasDrawn, setHasDrawn] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.strokeStyle = '#2dd4bf'; // Teal 400
|
||||||
|
ctx.lineWidth = 2.5;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
setIsDrawing(true);
|
||||||
|
setHasDrawn(true);
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = 'touches' in e ? e.touches[0].clientX - rect.left : e.clientX - rect.left;
|
||||||
|
const y = 'touches' in e ? e.touches[0].clientY - rect.top : e.clientY - rect.top;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = 'touches' in e ? e.touches[0].clientX - rect.left : e.clientX - rect.left;
|
||||||
|
const y = 'touches' in e ? e.touches[0].clientY - rect.top : e.clientY - rect.top;
|
||||||
|
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
setIsDrawing(false);
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (canvas && hasDrawn) {
|
||||||
|
onSaveSignature(canvas.toDataURL('image/png'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCanvas = () => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
setHasDrawn(false);
|
||||||
|
onSaveSignature('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-slate-300 font-semibold">
|
||||||
|
<PenTool className="w-3.5 h-3.5 text-teal-400" />
|
||||||
|
<span>Digital Signature Pad</span>
|
||||||
|
{signerName && <span className="text-slate-400 font-normal">({signerName})</span>}
|
||||||
|
</div>
|
||||||
|
{hasDrawn && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearCanvas}
|
||||||
|
className="text-[11px] text-slate-400 hover:text-slate-200 flex items-center gap-1 transition"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative border-2 border-dashed border-slate-700 rounded-lg bg-slate-950 overflow-hidden">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={500}
|
||||||
|
height={120}
|
||||||
|
className="w-full h-[120px] touch-none cursor-crosshair"
|
||||||
|
onMouseDown={startDrawing}
|
||||||
|
onMouseMove={draw}
|
||||||
|
onMouseUp={stopDrawing}
|
||||||
|
onMouseLeave={stopDrawing}
|
||||||
|
onTouchStart={startDrawing}
|
||||||
|
onTouchMove={draw}
|
||||||
|
onTouchEnd={stopDrawing}
|
||||||
|
/>
|
||||||
|
{!hasDrawn && (
|
||||||
|
<div className="absolute inset-0 pointer-events-none flex items-center justify-center text-xs text-slate-500">
|
||||||
|
Sign with finger or mouse here (HIPAA Certified)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { SpinalAdjustmentEntry } from '@/types/clinical';
|
||||||
|
import { Check, X, ShieldAlert, Zap, Sparkles, Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
interface SpineVisualizerProps {
|
||||||
|
adjustments: SpinalAdjustmentEntry[];
|
||||||
|
onToggleAdjustment: (entry: SpinalAdjustmentEntry) => void;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VertebraDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
shortName: string;
|
||||||
|
region: 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum';
|
||||||
|
commonSymptoms: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VERTEBRAE_LIST: VertebraDef[] = [
|
||||||
|
// Cervical
|
||||||
|
{ id: 'C1', name: 'Atlas (C1)', shortName: 'C1', region: 'Cervical', commonSymptoms: 'Headaches, migraines, dizziness, high BP' },
|
||||||
|
{ id: 'C2', name: 'Axis (C2)', shortName: 'C2', region: 'Cervical', commonSymptoms: 'Sinus issues, ear pain, eye fatigue' },
|
||||||
|
{ id: 'C3', name: 'C3 Vertebra', shortName: 'C3', region: 'Cervical', commonSymptoms: 'Facial neuralgia, tinnitus, neck stiffness' },
|
||||||
|
{ id: 'C4', name: 'C4 Vertebra', shortName: 'C4', region: 'Cervical', commonSymptoms: 'Hay fever, hearing loss, diaphragm' },
|
||||||
|
{ id: 'C5', name: 'C5 Vertebra', shortName: 'C5', region: 'Cervical', commonSymptoms: 'Vocal cords, hoarseness, shoulder pain' },
|
||||||
|
{ id: 'C6', name: 'C6 Vertebra', shortName: 'C6', region: 'Cervical', commonSymptoms: 'Neck pain, upper arm pain, tonsillitis' },
|
||||||
|
{ id: 'C7', name: 'C7 Vertebra', shortName: 'C7', region: 'Cervical', commonSymptoms: 'Bursitis, colds, thyroid conditions' },
|
||||||
|
|
||||||
|
// Thoracic
|
||||||
|
{ id: 'T1', name: 'T1 Vertebra', shortName: 'T1', region: 'Thoracic', commonSymptoms: 'Arms, wrists & hands, asthma' },
|
||||||
|
{ id: 'T2', name: 'T2 Vertebra', shortName: 'T2', region: 'Thoracic', commonSymptoms: 'Heart conditions, chest pain' },
|
||||||
|
{ id: 'T3', name: 'T3 Vertebra', shortName: 'T3', region: 'Thoracic', commonSymptoms: 'Lungs, bronchial tubes, pleurisy' },
|
||||||
|
{ id: 'T4', name: 'T4 Vertebra', shortName: 'T4', region: 'Thoracic', commonSymptoms: 'Gallbladder, jaundice, shingles' },
|
||||||
|
{ id: 'T5', name: 'T5 Vertebra', shortName: 'T5', region: 'Thoracic', commonSymptoms: 'Liver, low BP, poor circulation' },
|
||||||
|
{ id: 'T6', name: 'T6 Vertebra', shortName: 'T6', region: 'Thoracic', commonSymptoms: 'Stomach, heartburn, indigestion' },
|
||||||
|
{ id: 'T7', name: 'T7 Vertebra', shortName: 'T7', region: 'Thoracic', commonSymptoms: 'Pancreas, ulcers, gastritis' },
|
||||||
|
{ id: 'T8', name: 'T8 Vertebra', shortName: 'T8', region: 'Thoracic', commonSymptoms: 'Spleen, lowered immune resistance' },
|
||||||
|
{ id: 'T9', name: 'T9 Vertebra', shortName: 'T9', region: 'Thoracic', commonSymptoms: 'Adrenal glands, hives, allergies' },
|
||||||
|
{ id: 'T10', name: 'T10 Vertebra', shortName: 'T10', region: 'Thoracic', commonSymptoms: 'Kidneys, nephritis, chronic fatigue' },
|
||||||
|
{ id: 'T11', name: 'T11 Vertebra', shortName: 'T11', region: 'Thoracic', commonSymptoms: 'Skin disorders, acne, eczema' },
|
||||||
|
{ id: 'T12', name: 'T12 Vertebra', shortName: 'T12', region: 'Thoracic', commonSymptoms: 'Small intestines, rheumatisms, gas' },
|
||||||
|
|
||||||
|
// Lumbar
|
||||||
|
{ id: 'L1', name: 'L1 Vertebra', shortName: 'L1', region: 'Lumbar', commonSymptoms: 'Large intestines, constipation, colitis' },
|
||||||
|
{ id: 'L2', name: 'L2 Vertebra', shortName: 'L2', region: 'Lumbar', commonSymptoms: 'Appendix, cramps, varicose veins' },
|
||||||
|
{ id: 'L3', name: 'L3 Vertebra', shortName: 'L3', region: 'Lumbar', commonSymptoms: 'Bladder, knee pain, menstrual cramps' },
|
||||||
|
{ id: 'L4', name: 'L4 Vertebra', shortName: 'L4', region: 'Lumbar', commonSymptoms: 'Prostate, sciatica, lumbago, difficult urination' },
|
||||||
|
{ id: 'L5', name: 'L5 Vertebra', shortName: 'L5', region: 'Lumbar', commonSymptoms: 'Poor circulation in legs, cold feet, weak ankles' },
|
||||||
|
|
||||||
|
// Pelvis / Sacrum
|
||||||
|
{ id: 'Sacrum', name: 'Sacrum (S1-S5)', shortName: 'Sacrum', region: 'Pelvis/Sacrum', commonSymptoms: 'Sacroiliac pain, spinal curvatures, pelvic lean' },
|
||||||
|
{ id: 'Left Ilium', name: 'Left Ilium (SI Joint)', shortName: 'L-SI', region: 'Pelvis/Sacrum', commonSymptoms: 'Left SI joint fixation, antalgic posture' },
|
||||||
|
{ id: 'Right Ilium', name: 'Right Ilium (SI Joint)', shortName: 'R-SI', region: 'Pelvis/Sacrum', commonSymptoms: 'Right SI joint fixation, leg length disparity' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TECHNIQUES = [
|
||||||
|
'Diversified',
|
||||||
|
'Thompson Drop',
|
||||||
|
'Activator',
|
||||||
|
'Gonstead',
|
||||||
|
'Webster',
|
||||||
|
'Flexion-Distraction',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const COMMON_LISTINGS = [
|
||||||
|
'Posterior (P)',
|
||||||
|
'Right Posterior (RP)',
|
||||||
|
'Left Posterior (LP)',
|
||||||
|
'Right Lateral (R)',
|
||||||
|
'Left Lateral (L)',
|
||||||
|
'Anterior (A)',
|
||||||
|
'Rotation / Fixation',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
|
||||||
|
adjustments,
|
||||||
|
onToggleAdjustment,
|
||||||
|
readOnly = false,
|
||||||
|
}) => {
|
||||||
|
const [selectedVertebra, setSelectedVertebra] = useState<VertebraDef | null>(null);
|
||||||
|
const [activeListing, setActiveListing] = useState<string>('Right Posterior (RP)');
|
||||||
|
const [activeTechnique, setActiveTechnique] = useState<typeof TECHNIQUES[number]>('Diversified');
|
||||||
|
const [activeFilter, setActiveFilter] = useState<'All' | 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum'>('All');
|
||||||
|
|
||||||
|
const filteredVertebrae = VERTEBRAE_LIST.filter(
|
||||||
|
(v) => activeFilter === 'All' || v.region === activeFilter
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAdjusted = (id: string) => adjustments.some((a) => a.vertebra === id);
|
||||||
|
const getAdjustment = (id: string) => adjustments.find((a) => a.vertebra === id);
|
||||||
|
|
||||||
|
const handleOpenModal = (v: VertebraDef) => {
|
||||||
|
if (readOnly) return;
|
||||||
|
const existing = getAdjustment(v.id);
|
||||||
|
if (existing) {
|
||||||
|
setActiveListing(existing.listing);
|
||||||
|
setActiveTechnique(existing.technique);
|
||||||
|
} else {
|
||||||
|
setActiveListing('Right Posterior (RP)');
|
||||||
|
setActiveTechnique(v.region === 'Cervical' ? 'Diversified' : v.region === 'Lumbar' ? 'Thompson Drop' : 'Diversified');
|
||||||
|
}
|
||||||
|
setSelectedVertebra(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveListing = () => {
|
||||||
|
if (!selectedVertebra) return;
|
||||||
|
onToggleAdjustment({
|
||||||
|
vertebra: selectedVertebra.id,
|
||||||
|
region: selectedVertebra.region,
|
||||||
|
listing: activeListing,
|
||||||
|
technique: activeTechnique,
|
||||||
|
notes: `${activeTechnique} adjustment delivered to ${selectedVertebra.id}`,
|
||||||
|
});
|
||||||
|
setSelectedVertebra(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveListing = (vertebraId: string) => {
|
||||||
|
const existing = getAdjustment(vertebraId);
|
||||||
|
if (existing) {
|
||||||
|
onToggleAdjustment(existing);
|
||||||
|
}
|
||||||
|
setSelectedVertebra(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-2xl text-slate-100">
|
||||||
|
{/* Header bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-800">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-teal-500/10 text-teal-400 border border-teal-500/20">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<h3 className="font-semibold text-white text-base tracking-tight">
|
||||||
|
Interactive 2D Spine Subluxation Visualizer
|
||||||
|
</h3>
|
||||||
|
<span className="ml-2 text-xs font-medium px-2 py-0.5 rounded-full bg-teal-500/20 text-teal-300 border border-teal-500/30">
|
||||||
|
{adjustments.length} Listings Adjusted
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
Rapid click-to-chart anatomical interface. Tap any spinal segment to assign listing & technique in < 5 seconds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Region Filter Buttons */}
|
||||||
|
<div className="flex items-center gap-1 bg-slate-800/80 p-1 rounded-xl border border-slate-700/60 overflow-x-auto text-xs">
|
||||||
|
{(['All', 'Cervical', 'Thoracic', 'Lumbar', 'Pelvis/Sacrum'] as const).map((region) => (
|
||||||
|
<button
|
||||||
|
key={region}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveFilter(region)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg font-medium transition-all whitespace-nowrap ${
|
||||||
|
activeFilter === region
|
||||||
|
? 'bg-teal-500 text-white shadow-sm'
|
||||||
|
: 'text-slate-400 hover:text-white hover:bg-slate-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{region}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Spine Grid Layout */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 mt-5">
|
||||||
|
{/* Visual Column / Spine Segment Buttons (8 cols) */}
|
||||||
|
<div className="md:col-span-8 flex flex-col gap-2 max-h-[460px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-700">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
{filteredVertebrae.map((v) => {
|
||||||
|
const active = isAdjusted(v.id);
|
||||||
|
const adj = getAdjustment(v.id);
|
||||||
|
|
||||||
|
const regionColor =
|
||||||
|
v.region === 'Cervical'
|
||||||
|
? 'border-indigo-500/40 hover:border-indigo-400'
|
||||||
|
: v.region === 'Thoracic'
|
||||||
|
? 'border-blue-500/40 hover:border-blue-400'
|
||||||
|
: v.region === 'Lumbar'
|
||||||
|
? 'border-teal-500/40 hover:border-teal-400'
|
||||||
|
: 'border-purple-500/40 hover:border-purple-400';
|
||||||
|
|
||||||
|
const activeBg =
|
||||||
|
v.region === 'Cervical'
|
||||||
|
? 'bg-gradient-to-r from-indigo-950 to-slate-900 border-indigo-400 text-indigo-200'
|
||||||
|
: v.region === 'Thoracic'
|
||||||
|
? 'bg-gradient-to-r from-blue-950 to-slate-900 border-blue-400 text-blue-200'
|
||||||
|
: v.region === 'Lumbar'
|
||||||
|
? 'bg-gradient-to-r from-teal-950 to-slate-900 border-teal-400 text-teal-200'
|
||||||
|
: 'bg-gradient-to-r from-purple-950 to-slate-900 border-purple-400 text-purple-200';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={v.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOpenModal(v)}
|
||||||
|
className={`relative flex items-center justify-between p-2.5 rounded-xl border text-left transition-all duration-150 group ${
|
||||||
|
active ? `${activeBg} shadow-lg shadow-teal-950/20` : `bg-slate-800/40 ${regionColor} text-slate-300 hover:bg-slate-800/80`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span
|
||||||
|
className={`w-7 h-7 flex items-center justify-center rounded-lg font-bold text-xs shrink-0 ${
|
||||||
|
active
|
||||||
|
? 'bg-teal-500 text-slate-950 font-black'
|
||||||
|
: 'bg-slate-800 text-slate-400 group-hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{v.shortName}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-xs font-semibold truncate leading-tight">{v.name}</div>
|
||||||
|
<div className="text-[10px] text-slate-400 truncate mt-0.5">
|
||||||
|
{active && adj ? `${adj.listing} • ${adj.technique}` : v.region}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{active ? (
|
||||||
|
<span className="w-5 h-5 rounded-full bg-teal-500/20 text-teal-400 flex items-center justify-center shrink-0 border border-teal-500/40">
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="w-2 h-2 rounded-full bg-slate-700 group-hover:bg-slate-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Details Panel: Active Encounters & Clinical Notes (4 cols) */}
|
||||||
|
<div className="md:col-span-4 bg-slate-800/50 border border-slate-700/60 rounded-xl p-4 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-700">
|
||||||
|
<span className="text-xs font-semibold text-slate-300 uppercase tracking-wider">
|
||||||
|
Visit Adjustment Summary
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-teal-400 font-mono">
|
||||||
|
{adjustments.length} Segment(s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{adjustments.length === 0 ? (
|
||||||
|
<div className="py-10 text-center text-slate-500">
|
||||||
|
<Activity className="w-8 h-8 mx-auto mb-2 opacity-40 text-slate-400" />
|
||||||
|
<p className="text-xs">No vertebrae selected.</p>
|
||||||
|
<p className="text-[11px] text-slate-600 mt-1">
|
||||||
|
Click any vertebra on the left to add adjustment listing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 mt-3 max-h-[300px] overflow-y-auto pr-1">
|
||||||
|
{adjustments.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.vertebra}
|
||||||
|
className="flex items-center justify-between bg-slate-900/80 border border-slate-700/80 p-2.5 rounded-lg text-xs"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="font-bold text-teal-300">{item.vertebra}</span>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono">({item.region})</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-300 mt-0.5 font-medium">
|
||||||
|
{item.listing}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-teal-400/90 font-sans">
|
||||||
|
Technique: {item.technique}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveListing(item.vertebra)}
|
||||||
|
className="text-slate-500 hover:text-red-400 p-1 transition-colors"
|
||||||
|
title="Remove adjustment"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Subluxation Preset Macros */}
|
||||||
|
{!readOnly && (
|
||||||
|
<div className="mt-4 pt-3 border-t border-slate-700">
|
||||||
|
<div className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-2">
|
||||||
|
Chiropractic Quick Macros
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onToggleAdjustment({
|
||||||
|
vertebra: 'C1',
|
||||||
|
region: 'Cervical',
|
||||||
|
listing: 'Right Lateral / Posterior',
|
||||||
|
technique: 'Diversified',
|
||||||
|
});
|
||||||
|
onToggleAdjustment({
|
||||||
|
vertebra: 'C5',
|
||||||
|
region: 'Cervical',
|
||||||
|
listing: 'Bilateral Facet Fixation',
|
||||||
|
technique: 'Diversified',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="px-2 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-[11px] font-medium transition text-left truncate"
|
||||||
|
>
|
||||||
|
+ Cervical Protocol
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onToggleAdjustment({
|
||||||
|
vertebra: 'L4',
|
||||||
|
region: 'Lumbar',
|
||||||
|
listing: 'Right Posterior (RP)',
|
||||||
|
technique: 'Diversified',
|
||||||
|
});
|
||||||
|
onToggleAdjustment({
|
||||||
|
vertebra: 'Sacrum',
|
||||||
|
region: 'Pelvis/Sacrum',
|
||||||
|
listing: 'Right Sacral Base Anterior',
|
||||||
|
technique: 'Thompson Drop',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="px-2 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-[11px] font-medium transition text-left truncate"
|
||||||
|
>
|
||||||
|
+ L4/SI Protocol
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Adjustment Modal / Drawer */}
|
||||||
|
{selectedVertebra && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-md w-full p-6 shadow-2xl animate-in fade-in zoom-in-95 duration-150 text-slate-100">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-8 h-8 rounded-lg bg-teal-500/20 text-teal-400 font-bold flex items-center justify-center text-sm border border-teal-500/30">
|
||||||
|
{selectedVertebra.shortName}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-white text-sm">{selectedVertebra.name}</h4>
|
||||||
|
<p className="text-xs text-slate-400">{selectedVertebra.region} Spine</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedVertebra(null)}
|
||||||
|
className="text-slate-400 hover:text-white p-1"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
{/* Common Clinical Symptoms Reference */}
|
||||||
|
<div className="bg-slate-800/60 rounded-xl p-2.5 border border-slate-700/60 text-xs">
|
||||||
|
<span className="text-slate-400 font-semibold">Anatomical Correlate:</span>{' '}
|
||||||
|
<span className="text-slate-300">{selectedVertebra.commonSymptoms}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subluxation Listing Selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||||
|
Subluxation Listing / Motion Restriction
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{COMMON_LISTINGS.map((listing) => (
|
||||||
|
<button
|
||||||
|
key={listing}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveListing(listing)}
|
||||||
|
className={`px-3 py-2 rounded-lg text-xs font-medium text-left transition ${
|
||||||
|
activeListing === listing
|
||||||
|
? 'bg-teal-500 text-white font-semibold shadow-sm'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{listing}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technique Selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||||
|
Chiropractic Technique
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{TECHNIQUES.map((tech) => (
|
||||||
|
<button
|
||||||
|
key={tech}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTechnique(tech)}
|
||||||
|
className={`px-2 py-1.5 rounded-lg text-xs font-medium transition text-center ${
|
||||||
|
activeTechnique === tech
|
||||||
|
? 'bg-indigo-600 text-white font-semibold shadow-sm'
|
||||||
|
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Actions */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-slate-800 flex items-center justify-between gap-3">
|
||||||
|
{isAdjusted(selectedVertebra.id) ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveListing(selectedVertebra.id)}
|
||||||
|
className="px-3 py-2 rounded-xl text-xs font-semibold text-red-400 hover:bg-red-500/10 transition"
|
||||||
|
>
|
||||||
|
Clear Segment
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedVertebra(null)}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-slate-300 transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveListing}
|
||||||
|
className="px-4 py-2 rounded-xl text-xs font-semibold bg-teal-500 hover:bg-teal-400 text-slate-950 font-bold shadow-lg shadow-teal-500/20 transition flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
Apply Listing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
import { ClinicTenant, Provider, Patient, Appointment, SoapNote, Superbill, IntakeSubmission, CptCodeItem, Icd10CodeItem } from '@/types/clinical';
|
||||||
|
|
||||||
|
export const STANDARD_CPT_CODES: CptCodeItem[] = [
|
||||||
|
{ code: '98940', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 1-2 Regions', fee: 55 },
|
||||||
|
{ code: '98941', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 3-4 Regions', fee: 75 },
|
||||||
|
{ code: '98942', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 5 Regions', fee: 95 },
|
||||||
|
{ code: '97140', description: 'Manual Therapy Techniques (Mobilization, Myofascial Release, 15 min)', fee: 45 },
|
||||||
|
{ code: '97110', description: 'Therapeutic Exercises to Develop Strength/Endurance (15 min)', fee: 50 },
|
||||||
|
{ code: '97014', description: 'Electrical Stimulation (Unattended)', fee: 25 },
|
||||||
|
{ code: '99203', description: 'Office Visit; New Patient, Detailed Exam & Medical Decision', fee: 145 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STANDARD_ICD10_CODES: Icd10CodeItem[] = [
|
||||||
|
{ code: 'M99.01', description: 'Segmental and somatic dysfunction of cervical region' },
|
||||||
|
{ code: 'M99.02', description: 'Segmental and somatic dysfunction of thoracic region' },
|
||||||
|
{ code: 'M99.03', description: 'Segmental and somatic dysfunction of lumbar region' },
|
||||||
|
{ code: 'M99.04', description: 'Segmental and somatic dysfunction of sacral region' },
|
||||||
|
{ code: 'M99.05', description: 'Segmental and somatic dysfunction of pelvic region' },
|
||||||
|
{ code: 'M54.50', description: 'Low back pain, unspecified' },
|
||||||
|
{ code: 'M54.2', description: 'Cervicalgia (neck pain)' },
|
||||||
|
{ code: 'M54.16', description: 'Radiculopathy, lumbar region (Sciatica)' },
|
||||||
|
{ code: 'G44.209', description: 'Tension-type headache, unspecified' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_TENANTS: ClinicTenant[] = [
|
||||||
|
{
|
||||||
|
id: 'tenant-1',
|
||||||
|
name: 'Apex Spine & Wellness Clinic',
|
||||||
|
slug: 'apex-spine',
|
||||||
|
domain: 'apex.mediusa.app',
|
||||||
|
phone: '(555) 234-8901',
|
||||||
|
email: 'hello@apexspine.clinic',
|
||||||
|
address: '1420 Meridian Ave, Suite 300',
|
||||||
|
cityStateZip: 'San Jose, CA 95125',
|
||||||
|
npi: '1942857391',
|
||||||
|
taxId: '84-2918471',
|
||||||
|
logoText: 'APEX SPINE',
|
||||||
|
brandColor: '#0d9488', // Teal 600
|
||||||
|
accentColor: '#14b8a6',
|
||||||
|
plan: 'Pro Clinic',
|
||||||
|
mrr: 149,
|
||||||
|
stripeConnected: true,
|
||||||
|
createdAt: '2026-01-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenant-2',
|
||||||
|
name: 'Origins of Wellness Chiropractic',
|
||||||
|
slug: 'origins-wellness',
|
||||||
|
domain: 'origins.mediusa.app',
|
||||||
|
phone: '(555) 789-1234',
|
||||||
|
email: 'care@originsofwellness.com',
|
||||||
|
address: '880 Palmdale Blvd, Suite B',
|
||||||
|
cityStateZip: 'Palmdale, CA 93551',
|
||||||
|
npi: '1839201948',
|
||||||
|
taxId: '95-3029182',
|
||||||
|
logoText: 'ORIGINS',
|
||||||
|
brandColor: '#0284c7', // Sky 600
|
||||||
|
accentColor: '#38bdf8',
|
||||||
|
plan: 'Pro Clinic',
|
||||||
|
mrr: 149,
|
||||||
|
stripeConnected: true,
|
||||||
|
createdAt: '2026-02-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenant-3',
|
||||||
|
name: 'Elevate Sports & Spine Rehab',
|
||||||
|
slug: 'elevate-rehab',
|
||||||
|
domain: 'elevate.mediusa.app',
|
||||||
|
phone: '(555) 441-9022',
|
||||||
|
email: 'team@elevatespine.com',
|
||||||
|
address: '420 Olympic Blvd',
|
||||||
|
cityStateZip: 'Los Angeles, CA 90015',
|
||||||
|
npi: '1720394812',
|
||||||
|
taxId: '93-4819203',
|
||||||
|
logoText: 'ELEVATE',
|
||||||
|
brandColor: '#6366f1', // Indigo 500
|
||||||
|
accentColor: '#818cf8',
|
||||||
|
plan: 'Multi-Doc Enterprise',
|
||||||
|
mrr: 199,
|
||||||
|
stripeConnected: true,
|
||||||
|
createdAt: '2026-02-18',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_PROVIDERS: Provider[] = [
|
||||||
|
{
|
||||||
|
id: 'prov-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Dr. Marcus Vance',
|
||||||
|
title: 'Lead Chiropractor & Clinic Director',
|
||||||
|
credentials: 'D.C., CCSP, CSCS',
|
||||||
|
specialty: 'Sports Chiropractic & Spinal Biomechanics',
|
||||||
|
npi: '1942857391',
|
||||||
|
email: 'dr.vance@apexspine.clinic',
|
||||||
|
phone: '(555) 234-8902',
|
||||||
|
avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=150&auto=format&fit=crop&q=80',
|
||||||
|
color: '#0d9488',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prov-2',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Dr. Elena Rostova',
|
||||||
|
title: 'Associate Chiropractor',
|
||||||
|
credentials: 'D.C., Webster Certified',
|
||||||
|
specialty: 'Prenatal, Pediatric & Family Wellness',
|
||||||
|
npi: '1628491029',
|
||||||
|
email: 'dr.rostova@apexspine.clinic',
|
||||||
|
phone: '(555) 234-8903',
|
||||||
|
avatarUrl: 'https://images.unsplash.com/photo-1594824813571-638f026385a8?w=150&auto=format&fit=crop&q=80',
|
||||||
|
color: '#0284c7',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_PATIENTS: Patient[] = [
|
||||||
|
{
|
||||||
|
id: 'pat-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
firstName: 'Marcus',
|
||||||
|
lastName: 'Chen',
|
||||||
|
email: 'marcus.chen@example.com',
|
||||||
|
phone: '(555) 392-8172',
|
||||||
|
dob: '1988-04-14',
|
||||||
|
gender: 'Male',
|
||||||
|
address: '381 Willow Street, San Jose, CA',
|
||||||
|
insuranceName: 'Anthem Blue Cross PPO',
|
||||||
|
insuranceId: 'XED9381029',
|
||||||
|
chiefComplaint: 'Sharp lower back pain with right glute radiation after deadlifts',
|
||||||
|
lastVisitDate: '2026-09-02',
|
||||||
|
nextAppointmentDate: '2026-09-06',
|
||||||
|
daysSinceLastVisit: 3,
|
||||||
|
status: 'active',
|
||||||
|
carePlan: {
|
||||||
|
title: 'Lumbar Disc Decompression & Stabilization Protocol',
|
||||||
|
totalVisits: 12,
|
||||||
|
completedVisits: 6,
|
||||||
|
frequency: '2x / week for 6 weeks',
|
||||||
|
targetCondition: 'L4-L5 Disc Bulge with Sciatica',
|
||||||
|
startDate: '2026-08-10',
|
||||||
|
status: 'on_track',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pat-2',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
firstName: 'Sarah',
|
||||||
|
lastName: 'Holloway',
|
||||||
|
email: 's.holloway@example.com',
|
||||||
|
phone: '(555) 819-3011',
|
||||||
|
dob: '1993-11-20',
|
||||||
|
gender: 'Female',
|
||||||
|
address: '924 Lincoln Ave, San Jose, CA',
|
||||||
|
insuranceName: 'Aetna Open Choice',
|
||||||
|
insuranceId: 'W192840192',
|
||||||
|
chiefComplaint: 'Cervicogenic headaches and upper trap burning from desk posture',
|
||||||
|
lastVisitDate: '2026-08-18',
|
||||||
|
nextAppointmentDate: undefined,
|
||||||
|
daysSinceLastVisit: 18,
|
||||||
|
status: 'dropout_risk',
|
||||||
|
carePlan: {
|
||||||
|
title: 'Cervical Postural Realignment & Headache Relief',
|
||||||
|
totalVisits: 8,
|
||||||
|
completedVisits: 3,
|
||||||
|
frequency: '1x / week for 8 weeks',
|
||||||
|
targetCondition: 'C1-C2 Subluxation & Upper Cross Syndrome',
|
||||||
|
startDate: '2026-07-28',
|
||||||
|
status: 'at_risk',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pat-3',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
firstName: 'David',
|
||||||
|
lastName: 'Miller',
|
||||||
|
email: 'dmiller.tech@example.com',
|
||||||
|
phone: '(555) 442-9901',
|
||||||
|
dob: '1979-06-03',
|
||||||
|
gender: 'Male',
|
||||||
|
address: '110 Hamilton Ct, Campbell, CA',
|
||||||
|
insuranceName: 'UnitedHealthcare Choice Plus',
|
||||||
|
insuranceId: 'UH9920194',
|
||||||
|
chiefComplaint: 'Mid-back thoracic stiffness and restricted rotation on golf swing',
|
||||||
|
lastVisitDate: '2026-09-04',
|
||||||
|
nextAppointmentDate: '2026-09-11',
|
||||||
|
daysSinceLastVisit: 1,
|
||||||
|
status: 'active',
|
||||||
|
carePlan: {
|
||||||
|
title: 'Thoracic Mobility & Biomechanical Alignment',
|
||||||
|
totalVisits: 6,
|
||||||
|
completedVisits: 2,
|
||||||
|
frequency: '1x / week for 6 weeks',
|
||||||
|
targetCondition: 'T4-T8 Hypomobility',
|
||||||
|
startDate: '2026-08-25',
|
||||||
|
status: 'on_track',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pat-4',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
firstName: 'Amanda',
|
||||||
|
lastName: 'Torres',
|
||||||
|
email: 'amanda.t@example.com',
|
||||||
|
phone: '(555) 902-1456',
|
||||||
|
dob: '1995-02-18',
|
||||||
|
gender: 'Female',
|
||||||
|
address: '542 Race St, San Jose, CA',
|
||||||
|
insuranceName: 'Cigna Open Access',
|
||||||
|
insuranceId: 'CG882910',
|
||||||
|
chiefComplaint: 'Pelvic asymmetry and SI joint catching during third trimester',
|
||||||
|
lastVisitDate: '2026-09-01',
|
||||||
|
nextAppointmentDate: '2026-09-08',
|
||||||
|
daysSinceLastVisit: 4,
|
||||||
|
status: 'active',
|
||||||
|
carePlan: {
|
||||||
|
title: 'Webster Technique Pelvic Balance & Comfort',
|
||||||
|
totalVisits: 10,
|
||||||
|
completedVisits: 5,
|
||||||
|
frequency: '1x / week until delivery',
|
||||||
|
targetCondition: 'Sacroiliac Dysfunction & Round Ligament Tension',
|
||||||
|
startDate: '2026-08-01',
|
||||||
|
status: 'on_track',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pat-5',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
firstName: 'James',
|
||||||
|
lastName: 'Kowalski',
|
||||||
|
email: 'j.kowalski@example.com',
|
||||||
|
phone: '(555) 601-3329',
|
||||||
|
dob: '1965-09-12',
|
||||||
|
gender: 'Male',
|
||||||
|
address: '782 Minnesota Ave, San Jose, CA',
|
||||||
|
insuranceName: 'Medicare Part B',
|
||||||
|
insuranceId: '1EG4-TE8-MK99',
|
||||||
|
chiefComplaint: 'Chronic lumbar facet syndrome and bilateral morning stiffness',
|
||||||
|
lastVisitDate: '2026-08-12',
|
||||||
|
nextAppointmentDate: undefined,
|
||||||
|
daysSinceLastVisit: 24,
|
||||||
|
status: 'dropout_risk',
|
||||||
|
carePlan: {
|
||||||
|
title: 'Spinal Decompression & Maintenance Care',
|
||||||
|
totalVisits: 12,
|
||||||
|
completedVisits: 7,
|
||||||
|
frequency: 'Every 2 weeks',
|
||||||
|
targetCondition: 'Degenerative Disc Disease L3-S1',
|
||||||
|
startDate: '2026-06-15',
|
||||||
|
status: 'at_risk',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_APPOINTMENTS: Appointment[] = [
|
||||||
|
{
|
||||||
|
id: 'apt-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-1',
|
||||||
|
patientName: 'Marcus Chen',
|
||||||
|
patientPhone: '(555) 392-8172',
|
||||||
|
providerId: 'prov-1',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
date: '2026-09-05',
|
||||||
|
time: '09:00 AM',
|
||||||
|
durationMinutes: 20,
|
||||||
|
serviceType: 'Spinal Adjustment & Decompression',
|
||||||
|
status: 'completed',
|
||||||
|
room: 'Table 1 (Zenith Hi-Lo)',
|
||||||
|
fee: 75,
|
||||||
|
notes: 'Patient reports VAS down to 3/10. Focus on L4-L5 flexion distraction.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apt-2',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-3',
|
||||||
|
patientName: 'David Miller',
|
||||||
|
patientPhone: '(555) 442-9901',
|
||||||
|
providerId: 'prov-1',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
date: '2026-09-05',
|
||||||
|
time: '10:00 AM',
|
||||||
|
durationMinutes: 15,
|
||||||
|
serviceType: 'Subluxation Check & CMT',
|
||||||
|
status: 'in_room',
|
||||||
|
room: 'Table 2 (Drop Table)',
|
||||||
|
fee: 55,
|
||||||
|
notes: 'Check T6 anteriority and bilateral rib cage excursion.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apt-3',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-4',
|
||||||
|
patientName: 'Amanda Torres',
|
||||||
|
patientPhone: '(555) 902-1456',
|
||||||
|
providerId: 'prov-2',
|
||||||
|
providerName: 'Dr. Elena Rostova',
|
||||||
|
date: '2026-09-05',
|
||||||
|
time: '11:15 AM',
|
||||||
|
durationMinutes: 30,
|
||||||
|
serviceType: 'Webster Technique & Sacral Adjustment',
|
||||||
|
status: 'arrived',
|
||||||
|
room: 'Suite 2 (Pregnancy Cushion)',
|
||||||
|
fee: 85,
|
||||||
|
notes: 'Check right sacral rotation and round ligament tone.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apt-4',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-2',
|
||||||
|
patientName: 'Sarah Holloway',
|
||||||
|
patientPhone: '(555) 819-3011',
|
||||||
|
providerId: 'prov-1',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
date: '2026-09-05',
|
||||||
|
time: '02:00 PM',
|
||||||
|
durationMinutes: 20,
|
||||||
|
serviceType: 'Cervical Spinal Alignment',
|
||||||
|
status: 'confirmed',
|
||||||
|
room: 'Table 1',
|
||||||
|
fee: 75,
|
||||||
|
notes: 'Recheck C2 lateral slip and suboccipital tension.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apt-5',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-5',
|
||||||
|
patientName: 'James Kowalski',
|
||||||
|
patientPhone: '(555) 601-3329',
|
||||||
|
providerId: 'prov-1',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
date: '2026-09-05',
|
||||||
|
time: '03:30 PM',
|
||||||
|
durationMinutes: 30,
|
||||||
|
serviceType: 'Triton Lumbar Decompression Therapy',
|
||||||
|
status: 'booked',
|
||||||
|
room: 'Decompression Suite',
|
||||||
|
fee: 95,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_SOAP_NOTES: SoapNote[] = [
|
||||||
|
{
|
||||||
|
id: 'soap-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientId: 'pat-1',
|
||||||
|
patientName: 'Marcus Chen',
|
||||||
|
providerId: 'prov-1',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
appointmentId: 'apt-1',
|
||||||
|
date: '2026-09-05',
|
||||||
|
status: 'signed',
|
||||||
|
vasScore: 3,
|
||||||
|
subjective: 'Patient reports noticeable reduction in morning stiffness. Sciatic sensation into right calf has resolved; occasional dull ache localized to right L5 facet upon prolonged sitting (> 45 min). Rated 3/10 today vs 7/10 at intake.',
|
||||||
|
objective: 'Inspection reveals reduced antalgic lean. Active Lumbar ROM: Flexion 75 deg (mild tightness), Extension 18 deg. Palpation demonstrates segmental fixation at L4-L5 and right sacral base anteriority. Paraspinal hypertonicity reduced in right quadratus lumborum.',
|
||||||
|
assessment: 'Patient showing favorable progression in care plan visit 6 of 12. Segmental somatic dysfunction of lumbar spine (M99.03) and pelvic region (M99.05) stabilizing under manual decompression.',
|
||||||
|
plan: '1. Diversified CMT delivered to L4 (Right Mamillary) and Right Sacral Base.\n2. Flexion-Distraction therapy L4-L5 at 12 lbs traction for 8 minutes.\n3. Prescribed Bird-Dog and McGill Big 3 core stabilization home exercises.\n4. Follow-up adjustment scheduled in 3 days.',
|
||||||
|
spinalAdjustments: [
|
||||||
|
{ vertebra: 'L4', region: 'Lumbar', listing: 'Right Mamillary Posterior (RP)', technique: 'Diversified', notes: 'Audible cavitation, immediate release' },
|
||||||
|
{ vertebra: 'L5', region: 'Lumbar', listing: 'L5 Flexion Restricted', technique: 'Flexion-Distraction', notes: '8 minutes gentle rhythmic traction' },
|
||||||
|
{ vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Sacral Base Anterior', technique: 'Thompson Drop', notes: 'Double drop piece protocol' },
|
||||||
|
],
|
||||||
|
icd10Codes: [
|
||||||
|
{ code: 'M99.03', description: 'Segmental dysfunction of lumbar region' },
|
||||||
|
{ code: 'M99.05', description: 'Segmental dysfunction of pelvic region' },
|
||||||
|
{ code: 'M54.50', description: 'Low back pain, unspecified' },
|
||||||
|
],
|
||||||
|
cptCodes: [
|
||||||
|
{ code: '98940', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 1-2 Regions', fee: 55 },
|
||||||
|
{ code: '97140', description: 'Manual Therapy Techniques (15 min)', fee: 45 },
|
||||||
|
],
|
||||||
|
signedAt: '2026-09-05T09:25:00-07:00',
|
||||||
|
signedBy: 'Dr. Marcus Vance, D.C. (NPI 1942857391)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_SUPERBILLS: Superbill[] = [
|
||||||
|
{
|
||||||
|
id: 'sb-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
invoiceNumber: 'SB-2026-0901',
|
||||||
|
patientId: 'pat-1',
|
||||||
|
patientName: 'Marcus Chen',
|
||||||
|
patientDob: '1988-04-14',
|
||||||
|
patientAddress: '381 Willow Street, San Jose, CA 95125',
|
||||||
|
providerName: 'Dr. Marcus Vance, D.C.',
|
||||||
|
providerNpi: '1942857391',
|
||||||
|
clinicName: 'Apex Spine & Wellness Clinic',
|
||||||
|
clinicAddress: '1420 Meridian Ave, Suite 300, San Jose, CA 95125',
|
||||||
|
clinicTaxId: '84-2918471',
|
||||||
|
dateOfService: '2026-09-05',
|
||||||
|
posCode: '11 (Office)',
|
||||||
|
icd10Codes: [
|
||||||
|
{ code: 'M99.03', description: 'Segmental somatic dysfunction of lumbar region' },
|
||||||
|
{ code: 'M99.05', description: 'Segmental somatic dysfunction of pelvic region' },
|
||||||
|
{ code: 'M54.50', description: 'Low back pain, unspecified' },
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
{ cptCode: '98940', description: 'CMT Spinal, 1-2 Regions (Lumbar/Sacral)', units: 1, rate: 55, total: 55 },
|
||||||
|
{ cptCode: '97140', description: 'Manual Therapy Techniques / Myofascial Release', units: 1, rate: 45, total: 45 },
|
||||||
|
],
|
||||||
|
totalAmount: 100,
|
||||||
|
patientPaid: 100,
|
||||||
|
balanceDue: 0,
|
||||||
|
paymentMethod: 'Stripe Card',
|
||||||
|
generatedAt: '2026-09-05T09:30:00-07:00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_INTAKE_SUBMISSIONS: IntakeSubmission[] = [
|
||||||
|
{
|
||||||
|
id: 'intake-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
patientName: 'Jessica Morales',
|
||||||
|
dob: '1991-08-14',
|
||||||
|
phone: '(555) 302-9912',
|
||||||
|
email: 'jess.morales@example.com',
|
||||||
|
chiefComplaints: ['Neck Pain', 'Shoulder Blade Tightness', 'Numbness in Left Fingers'],
|
||||||
|
painAreas: ['Cervical Spine', 'Left Trapezius', 'Left Medial Scapula'],
|
||||||
|
vasScore: 6,
|
||||||
|
painNature: ['Shooting Pain', 'Dull Ache', 'Tingling / Pins & Needles'],
|
||||||
|
symptomsOnset: '3 weeks ago following a motor vehicle rear-end collision',
|
||||||
|
medications: 'Ibuprofen 600mg PRN',
|
||||||
|
priorChiropractic: true,
|
||||||
|
signatureName: 'Jessica Morales',
|
||||||
|
submittedAt: '2026-09-05T11:45:00-07:00',
|
||||||
|
status: 'pending_review',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SAMPLE_AUDIO_PRESETS = [
|
||||||
|
{
|
||||||
|
id: 'acute-lumbar',
|
||||||
|
title: 'Encounter: Acute L4-L5 Disc & Sciatica Follow-Up',
|
||||||
|
duration: '1m 15s',
|
||||||
|
audioTranscript: `Dr. Vance: "Good morning Marcus. How did the lower back hold up since Wednesday's adjustment?"
|
||||||
|
Marcus: "Hey Doc, morning stiffness is way down. The numbness down my right hamstring is pretty much gone. Still getting an ache right over that lower belt line if I sit for more than 40 minutes at work, maybe a 3 out of 10."
|
||||||
|
Dr. Vance: "Great, let's take a look. Face down on the hi-lo table please. Palpating L4 and L5... definitely still feeling that right-sided fixation at L4 mamillary, and your right sacral base is shifted anterior about a quarter inch. Left SI is moving freely now. I'm going to set the drop piece for the sacrum... deep breath in, let it all the way out... [Drop release]. Perfect. Now on your left side facing me for L4 Diversified... nice clear cavitation there. We will run 8 minutes on the flexion distraction table to keep that disc hydrated. Keep up your bird-dogs at home, and let's see you Tuesday to keep your 12-visit stabilization on track."`,
|
||||||
|
extractedSOAP: {
|
||||||
|
vasScore: 3,
|
||||||
|
subjective: "Patient reports morning stiffness is significantly reduced. Radiation/numbness into right hamstring has resolved. Mild localized dull ache persists across the lower lumbar belt line after sitting > 40 minutes. VAS rated 3/10 (improved from initial 7/10).",
|
||||||
|
objective: "Physical exam reveals improved lumbar postural alignment. Motion palpation demonstrates persistent right-sided fixation at L4 mamillary process and right anterior sacral base subluxation. Left sacroiliac joint displays restored articulation.",
|
||||||
|
assessment: "Subacute L4-L5 disc injury and segmental dysfunction lumbar region (M99.03) with secondary pelvic dysfunction (M99.05). Demonstrating excellent compliance and functional milestone recovery on care plan visit 6/12.",
|
||||||
|
plan: "1. High-velocity low-amplitude (HVLA) Diversified adjustment to L4 in side posture.\n2. Thompson drop table adjustment to right sacral base.\n3. Flexion-distraction axial traction L4-L5 for 8 minutes.\n4. Continue home McGill Big 3 stabilization. Follow up in 3 days for next care plan visit.",
|
||||||
|
spinalAdjustments: [
|
||||||
|
{ vertebra: 'L4', region: 'Lumbar', listing: 'RP (Right Posterior)', technique: 'Diversified', notes: 'Clear cavitation, restored rotation' },
|
||||||
|
{ vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Base Anterior', technique: 'Thompson Drop', notes: 'Single drop release' },
|
||||||
|
],
|
||||||
|
icd10: ['M99.03', 'M99.05', 'M54.50'],
|
||||||
|
cpt: ['98940', '97140'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cervical-headache',
|
||||||
|
title: 'Encounter: Cervicogenic Headache & Atlas Subluxation',
|
||||||
|
duration: '0m 52s',
|
||||||
|
audioTranscript: `Dr. Elena: "Hi Sarah, how has your neck been feeling with the new ergonomic monitor setup?"
|
||||||
|
Sarah: "Hi Dr. Elena! The sharp migraines are down, but I still get that heavy throbbing behind my right eye and burning at the base of my skull by 3 PM every day."
|
||||||
|
Dr. Elena: "Let's check cervical motion. Rotation to the right is restricted at 55 degrees with suboccipital spasm. Motion palpation shows C1 Atlas right lateral and posterior listing, and C5-C6 bilateral facet restriction. Let's do gentle Atlas specific diversified adjustment on the right... gentle breath out... [click]. Excellent. Re-checking range of motion: right rotation cleared to 80 degrees smoothly. I'm going to apply 5 minutes of suboccipital release. Come back in 5 days."`,
|
||||||
|
extractedSOAP: {
|
||||||
|
vasScore: 4,
|
||||||
|
subjective: "Patient reports resolution of severe acute migraine episodes. Throbbing occipital pain radiating to right retro-orbital area persists toward late afternoon with desk work. Rated 4/10.",
|
||||||
|
objective: "Active cervical rotation to the right restricted to 55 degrees (normal 80) with palpable hypertonicity in right rectus capitis and obliquus capitis. Palpation reveals C1 Atlas Right Lateral/Posterior listing and C5-C6 facet imbrication.",
|
||||||
|
assessment: "Cervicogenic tension headache (G44.209) secondary to C1-C2 segmental subluxation (M99.01) and postural cervicalgia (M54.2).",
|
||||||
|
plan: "1. Specific diversified Atlas (C1) contact right lateral mass adjustment.\n2. C5-C6 bilateral cervical seated toggle recoil.\n3. Manual myofascial release to suboccipital triangle (5 mins).\n4. Follow-up appointment scheduled in 5 days.",
|
||||||
|
spinalAdjustments: [
|
||||||
|
{ vertebra: 'C1', region: 'Cervical', listing: 'Atlas Right Lateral Posterior (ASR)', technique: 'Diversified', notes: 'Immediate cervical rotation clearance' },
|
||||||
|
{ vertebra: 'C5', region: 'Cervical', listing: 'Bilateral Facet Fixation', technique: 'Activator', notes: 'Pressure 3 setting' },
|
||||||
|
],
|
||||||
|
icd10: ['M99.01', 'M54.2', 'G44.209'],
|
||||||
|
cpt: ['98940', '97140'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import { Superbill } from '@/types/clinical';
|
||||||
|
|
||||||
|
export function generateSuperbillPdf(superbill: Superbill) {
|
||||||
|
const doc = new jsPDF({
|
||||||
|
orientation: 'portrait',
|
||||||
|
unit: 'pt',
|
||||||
|
format: 'letter',
|
||||||
|
});
|
||||||
|
|
||||||
|
const margin = 40;
|
||||||
|
let y = margin;
|
||||||
|
|
||||||
|
// Header Banner
|
||||||
|
doc.setFillColor(15, 23, 42); // Slate 900
|
||||||
|
doc.rect(0, 0, doc.internal.pageSize.getWidth(), 80, 'F');
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setTextColor(255, 255, 255);
|
||||||
|
doc.setFontSize(20);
|
||||||
|
doc.text('MEDICAL SUPERBILL & STATEMENT', margin, 42);
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setTextColor(148, 163, 184); // Slate 400
|
||||||
|
doc.text('Comprehensive Out-of-Network Insurance Reimbursement Form', margin, 60);
|
||||||
|
|
||||||
|
doc.setTextColor(255, 255, 255);
|
||||||
|
doc.text(`Invoice #: ${superbill.invoiceNumber}`, doc.internal.pageSize.getWidth() - margin - 150, 42);
|
||||||
|
doc.text(`Date of Service: ${superbill.dateOfService}`, doc.internal.pageSize.getWidth() - margin - 150, 60);
|
||||||
|
|
||||||
|
y = 110;
|
||||||
|
|
||||||
|
// Two columns: Clinic (Billing Provider) & Patient Details
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setTextColor(15, 23, 42);
|
||||||
|
doc.text('BILLING PROVIDER & CLINIC', margin, y);
|
||||||
|
doc.text('PATIENT INFORMATION', 320, y);
|
||||||
|
|
||||||
|
doc.setDrawColor(226, 232, 240);
|
||||||
|
doc.line(margin, y + 6, 280, y + 6);
|
||||||
|
doc.line(320, y + 6, doc.internal.pageSize.getWidth() - margin, y + 6);
|
||||||
|
|
||||||
|
y += 22;
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(51, 65, 85);
|
||||||
|
|
||||||
|
// Clinic Info
|
||||||
|
doc.text(superbill.clinicName, margin, y);
|
||||||
|
doc.text(superbill.clinicAddress, margin, y + 14);
|
||||||
|
doc.text(`Billing Provider: ${superbill.providerName}`, margin, y + 28);
|
||||||
|
doc.text(`Provider NPI: ${superbill.providerNpi} | Tax ID: ${superbill.clinicTaxId}`, margin, y + 42);
|
||||||
|
doc.text(`Place of Service (POS): ${superbill.posCode}`, margin, y + 56);
|
||||||
|
|
||||||
|
// Patient Info
|
||||||
|
doc.text(`Name: ${superbill.patientName}`, 320, y);
|
||||||
|
doc.text(`DOB: ${superbill.patientDob}`, 320, y + 14);
|
||||||
|
doc.text(`Address: ${superbill.patientAddress}`, 320, y + 28);
|
||||||
|
doc.text(`Payment Status: ${superbill.paymentMethod} (PAID IN FULL)`, 320, y + 42);
|
||||||
|
|
||||||
|
y += 85;
|
||||||
|
|
||||||
|
// Diagnosis (ICD-10) Section
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setTextColor(15, 23, 42);
|
||||||
|
doc.text('DIAGNOSIS CODES (ICD-10-CM)', margin, y);
|
||||||
|
doc.line(margin, y + 6, doc.internal.pageSize.getWidth() - margin, y + 6);
|
||||||
|
|
||||||
|
y += 20;
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(51, 65, 85);
|
||||||
|
|
||||||
|
superbill.icd10Codes.forEach((icd, idx) => {
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text(`(${idx + 1}) ${icd.code}`, margin, y);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(` - ${icd.description}`, margin + 65, y);
|
||||||
|
y += 15;
|
||||||
|
});
|
||||||
|
|
||||||
|
y += 15;
|
||||||
|
|
||||||
|
// Procedure Line Items Table
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setTextColor(15, 23, 42);
|
||||||
|
doc.text('PROCEDURES & SERVICES (CPT CODES)', margin, y);
|
||||||
|
doc.line(margin, y + 6, doc.internal.pageSize.getWidth() - margin, y + 6);
|
||||||
|
|
||||||
|
y += 18;
|
||||||
|
|
||||||
|
// Table Header
|
||||||
|
doc.setFillColor(241, 245, 249);
|
||||||
|
doc.rect(margin, y, doc.internal.pageSize.getWidth() - (margin * 2), 22, 'F');
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(71, 85, 105);
|
||||||
|
doc.text('CPT CODE', margin + 8, y + 15);
|
||||||
|
doc.text('DESCRIPTION', margin + 80, y + 15);
|
||||||
|
doc.text('UNITS', 400, y + 15);
|
||||||
|
doc.text('RATE', 450, y + 15);
|
||||||
|
doc.text('TOTAL', 505, y + 15);
|
||||||
|
|
||||||
|
y += 24;
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(30, 41, 59);
|
||||||
|
|
||||||
|
superbill.items.forEach((item) => {
|
||||||
|
doc.text(item.cptCode, margin + 8, y + 14);
|
||||||
|
doc.text(item.description, margin + 80, y + 14);
|
||||||
|
doc.text(String(item.units), 405, y + 14);
|
||||||
|
doc.text(`$${item.rate.toFixed(2)}`, 450, y + 14);
|
||||||
|
doc.text(`$${item.total.toFixed(2)}`, 505, y + 14);
|
||||||
|
|
||||||
|
doc.setDrawColor(241, 245, 249);
|
||||||
|
doc.line(margin, y + 20, doc.internal.pageSize.getWidth() - margin, y + 20);
|
||||||
|
y += 22;
|
||||||
|
});
|
||||||
|
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
// Summary Totals Box
|
||||||
|
const summaryX = 350;
|
||||||
|
doc.setFillColor(248, 250, 252);
|
||||||
|
doc.rect(summaryX, y, doc.internal.pageSize.getWidth() - margin - summaryX, 70, 'F');
|
||||||
|
doc.rect(summaryX, y, doc.internal.pageSize.getWidth() - margin - summaryX, 70, 'S');
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('Total Charges:', summaryX + 15, y + 20);
|
||||||
|
doc.text(`$${superbill.totalAmount.toFixed(2)}`, 505, y + 20);
|
||||||
|
|
||||||
|
doc.text('Patient Amount Paid:', summaryX + 15, y + 38);
|
||||||
|
doc.text(`$${superbill.patientPaid.toFixed(2)}`, 505, y + 38);
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Balance Due:', summaryX + 15, y + 56);
|
||||||
|
doc.text(`$${superbill.balanceDue.toFixed(2)}`, 505, y + 56);
|
||||||
|
|
||||||
|
y += 95;
|
||||||
|
|
||||||
|
// Practitioner Attestation & Signature
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(15, 23, 42);
|
||||||
|
doc.text('PRACTITIONER ATTESTATION & SIGNATURE', margin, y);
|
||||||
|
doc.line(margin, y + 4, doc.internal.pageSize.getWidth() - margin, y + 4);
|
||||||
|
|
||||||
|
y += 16;
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setTextColor(100, 116, 139);
|
||||||
|
doc.text(
|
||||||
|
'I certify that the services listed above were medically necessary and personally rendered by me or under my direct supervision on the date indicated.',
|
||||||
|
margin,
|
||||||
|
y
|
||||||
|
);
|
||||||
|
|
||||||
|
y += 25;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setTextColor(15, 23, 42);
|
||||||
|
doc.text(`Electronically Signed by: ${superbill.providerName}`, margin, y);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(`Timestamp: ${superbill.generatedAt}`, margin, y + 12);
|
||||||
|
doc.text(`Verification ID: MED-${superbill.id.toUpperCase()}`, margin, y + 24);
|
||||||
|
|
||||||
|
// Footer Branding
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setTextColor(148, 163, 184);
|
||||||
|
doc.text('Generated via Mediusa Clinic OS • Clean Claim Direct Superbill Standard', margin, 760);
|
||||||
|
|
||||||
|
doc.save(`Superbill_${superbill.invoiceNumber}_${superbill.patientName.replace(/\s+/g, '_')}.pdf`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
export interface ClinicTenant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
domain: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
address: string;
|
||||||
|
cityStateZip: string;
|
||||||
|
npi: string;
|
||||||
|
taxId: string;
|
||||||
|
logoText: string;
|
||||||
|
brandColor: string;
|
||||||
|
accentColor: string;
|
||||||
|
plan: 'Starter' | 'Pro Clinic' | 'Multi-Doc Enterprise';
|
||||||
|
mrr: number;
|
||||||
|
stripeConnected: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Provider {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
credentials: string; // e.g., "D.C., CCSP"
|
||||||
|
specialty: string;
|
||||||
|
npi: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarePlan {
|
||||||
|
title: string;
|
||||||
|
totalVisits: number;
|
||||||
|
completedVisits: number;
|
||||||
|
frequency: string; // e.g. "2x / week for 6 weeks"
|
||||||
|
targetCondition: string;
|
||||||
|
startDate: string;
|
||||||
|
status: 'on_track' | 'lagging' | 'at_risk' | 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Patient {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
dob: string;
|
||||||
|
gender: string;
|
||||||
|
address: string;
|
||||||
|
insuranceName: string;
|
||||||
|
insuranceId: string;
|
||||||
|
carePlan: CarePlan;
|
||||||
|
lastVisitDate: string;
|
||||||
|
nextAppointmentDate?: string;
|
||||||
|
status: 'active' | 'dropout_risk' | 'reactivated' | 'discharged';
|
||||||
|
chiefComplaint: string;
|
||||||
|
daysSinceLastVisit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';
|
||||||
|
|
||||||
|
export interface Appointment {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
patientId: string;
|
||||||
|
patientName: string;
|
||||||
|
patientPhone: string;
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
time: string; // HH:MM AM/PM
|
||||||
|
durationMinutes: number;
|
||||||
|
serviceType: string;
|
||||||
|
status: AppointmentStatus;
|
||||||
|
room: string;
|
||||||
|
notes?: string;
|
||||||
|
fee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpinalAdjustmentEntry {
|
||||||
|
vertebra: string; // e.g. "C2", "L5"
|
||||||
|
region: 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum';
|
||||||
|
listing: string; // e.g., "Right Lateral / Posterior"
|
||||||
|
technique: 'Diversified' | 'Thompson Drop' | 'Activator' | 'Gonstead' | 'Webster' | 'Flexion-Distraction';
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CptCodeItem {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
fee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Icd10CodeItem {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoapNote {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
patientId: string;
|
||||||
|
patientName: string;
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
appointmentId?: string;
|
||||||
|
date: string;
|
||||||
|
status: 'draft' | 'signed';
|
||||||
|
subjective: string;
|
||||||
|
objective: string;
|
||||||
|
assessment: string;
|
||||||
|
plan: string;
|
||||||
|
spinalAdjustments: SpinalAdjustmentEntry[];
|
||||||
|
icd10Codes: Icd10CodeItem[];
|
||||||
|
cptCodes: CptCodeItem[];
|
||||||
|
vasScore: number; // 0-10
|
||||||
|
signedAt?: string;
|
||||||
|
signedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuperbillLineItem {
|
||||||
|
cptCode: string;
|
||||||
|
description: string;
|
||||||
|
units: number;
|
||||||
|
rate: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Superbill {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
patientId: string;
|
||||||
|
patientName: string;
|
||||||
|
patientDob: string;
|
||||||
|
patientAddress: string;
|
||||||
|
providerName: string;
|
||||||
|
providerNpi: string;
|
||||||
|
clinicName: string;
|
||||||
|
clinicAddress: string;
|
||||||
|
clinicTaxId: string;
|
||||||
|
dateOfService: string;
|
||||||
|
posCode: string; // Place of Service "11 - Office"
|
||||||
|
icd10Codes: Icd10CodeItem[];
|
||||||
|
items: SuperbillLineItem[];
|
||||||
|
totalAmount: number;
|
||||||
|
patientPaid: number;
|
||||||
|
balanceDue: number;
|
||||||
|
paymentMethod: 'Stripe Card' | 'HSA/FSA' | 'Cash' | 'Unpaid';
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IntakeSubmission {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
patientName: string;
|
||||||
|
dob: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
chiefComplaints: string[];
|
||||||
|
painAreas: string[];
|
||||||
|
vasScore: number;
|
||||||
|
painNature: string[]; // ['Sharp', 'Dull Ache', 'Shooting']
|
||||||
|
symptomsOnset: string;
|
||||||
|
medications: string;
|
||||||
|
priorChiropractic: boolean;
|
||||||
|
signatureName: string;
|
||||||
|
submittedAt: string;
|
||||||
|
status: 'pending_review' | 'imported_to_chart';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user