feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')

This commit is contained in:
2026-09-05 14:12:19 -07:00
parent 441e5eb57d
commit deb5197e53
17 changed files with 4675 additions and 71 deletions
+249
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>
); );
} }
+359
View File
@@ -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 20100+ chiropractic &amp; 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 &amp; 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 &amp; 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>
);
};
+346
View File
@@ -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>
);
};
+237
View File
@@ -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">
&quot;{apt.notes}&quot;
</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>
);
};
+478
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>
);
};
+410
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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&apos;s EHR...
</>
) : (
<>
<CheckCircle2 className="w-4 h-4" />
Confirm Appointment &amp; 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&apos;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 &amp; 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 &amp; 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>
);
};
+346
View File
@@ -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 &gt; 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 &amp; 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>
);
};
+256
View File
@@ -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 &amp; 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>
);
};
+182
View File
@@ -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 &amp; 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 &amp; ICD-10 diagnostic codes.</span>
</div>
</div>
</div>
</div>
);
};
+122
View File
@@ -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>
);
};
+457
View File
@@ -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 &lt; 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>
);
};
+471
View File
@@ -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'],
},
},
];
+178
View File
@@ -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`);
}
+175
View File
@@ -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';
}