From 4e4e46cf0088406874851ed8b097b0e530676c0c Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:06:05 +0200 Subject: [PATCH 1/4] chore: add a formatter, matching the style this repo already writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit singleQuote=false was chosen by counting this repo's own imports, not by fleet decree. The fleet is genuinely split and the two repos that already had a .prettierrc disagreed with each other, so there was no standard to restore. Quote style does not cross repo boundaries; having a gate does. Markdown is ignored for now — prettier rewraps prose, which would bury the real diff. Co-Authored-By: Claude Opus 5 --- .prettierignore | 21 +++++++++++++++++++++ .prettierrc | 9 +++++++++ package.json | 5 ++++- pnpm-lock.yaml | 25 +++++++++++-------------- 4 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1ed5ea5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# Build output and vendored trees — formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a2f11f0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/package.json b/package.json index e37e052..9b7b826 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "db:push": "dotenv -e .env.local -- drizzle-kit push", "db:studio": "dotenv -e .env.local -- drizzle-kit studio", "test": "vitest run", - "verify": "pnpm run lint && pnpm run typecheck && pnpm run test" + "verify": "npm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@auth/drizzle-adapter": "^1.11.3", @@ -45,6 +47,7 @@ "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.3.3", + "prettier": "3.9.6", "tailwindcss": "^4", "typescript": "^6", "vitest": "^4.1.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d3939a..413ba86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: eslint-config-next: specifier: 16.3.3 version: 16.3.3(@typescript-eslint/parser@8.68.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + prettier: + specifier: 3.9.6 + version: 3.9.6 tailwindcss: specifier: ^4 version: 4.3.3 @@ -3652,6 +3655,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -3943,11 +3951,6 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - terser@5.50.0: - resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} - engines: {node: '>=10'} - hasBin: true - terser@5.51.2: resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} engines: {node: '>=10'} @@ -7196,7 +7199,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.50.0 + terser: 5.51.2 webpack: 5.108.4(esbuild@0.27.7) optionalDependencies: esbuild: 0.27.7 @@ -7449,6 +7452,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.9.6: {} + progress@2.0.3: {} prop-types@15.8.1: @@ -7848,20 +7853,12 @@ snapshots: tapable@2.3.3: {} - terser@5.50.0: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.18.0 - commander: 2.20.3 - source-map-support: 0.5.21 - terser@5.51.2: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 - optional: true threadkit@0.1.1: {} From 2294ac57371ee75c597505bf4a3a4f5c7e1834ef Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:07:52 +0200 Subject: [PATCH 2/4] style: format with prettier (304 files) Mechanical. No behaviour change. This SHA is listed in .git-blame-ignore-revs so `git blame` skips it. Co-Authored-By: Claude Opus 5 --- .github/workflows/auto-merge.yml | 6 +- app/(admin)/admin.module.css | 83 +++- app/(admin)/admin/bookings/page.tsx | 70 ++-- app/(admin)/admin/documents/page.tsx | 47 +-- .../admin/messages/[threadId]/page.tsx | 33 +- app/(admin)/admin/messages/page.tsx | 8 +- app/(admin)/admin/page.tsx | 12 +- app/(admin)/admin/patients/[id]/page.tsx | 55 ++- app/(admin)/admin/patients/page.tsx | 82 ++-- app/(admin)/admin/profile/page.tsx | 3 +- app/(admin)/admin/reports/page.tsx | 122 ++++-- app/(admin)/layout.tsx | 9 +- app/(portal)/assessment/page.tsx | 2 +- .../assessments/assessments.module.css | 16 +- app/(portal)/assessments/page.tsx | 29 +- app/(portal)/bookings/BookingHistoryList.tsx | 24 +- app/(portal)/bookings/ClinicianPicker.tsx | 19 +- app/(portal)/bookings/RequestBookingForm.tsx | 47 ++- app/(portal)/bookings/bookings.module.css | 18 +- app/(portal)/bookings/page.tsx | 75 ++-- app/(portal)/care-team/CareTeamPanel.tsx | 34 +- app/(portal)/care-team/[clinicianId]/page.tsx | 9 +- app/(portal)/care-team/care-team.module.css | 36 +- app/(portal)/care-team/page.tsx | 6 +- app/(portal)/checkin/CheckinForm.tsx | 67 +++- app/(portal)/checkin/checkin.module.css | 5 +- app/(portal)/dashboard/AiInsightCard.tsx | 3 +- app/(portal)/dashboard/AssessmentSection.tsx | 12 +- app/(portal)/dashboard/CheckinCard.tsx | 4 +- app/(portal)/dashboard/CheckinMiniTrend.tsx | 7 +- app/(portal)/dashboard/GoalsCard.tsx | 19 +- app/(portal)/dashboard/NextStepCard.tsx | 5 +- .../dashboard/PendingAssessmentSaver.tsx | 2 +- .../dashboard/ProfileCompletenessBar.tsx | 12 +- app/(portal)/dashboard/dashboard.module.css | 28 +- app/(portal)/dashboard/page.tsx | 35 +- app/(portal)/documents/DocumentUploadForm.tsx | 20 +- app/(portal)/documents/page.tsx | 16 +- app/(portal)/goals/page.tsx | 48 ++- app/(portal)/labs/page.tsx | 10 +- app/(portal)/layout.tsx | 62 +-- app/(portal)/messages/[threadId]/page.tsx | 51 ++- app/(portal)/messages/page.tsx | 93 +++-- app/(portal)/portal.module.css | 361 ++++++++++++----- .../profile/ClinicalContextFields.tsx | 65 ++- .../profile/EmailPreferencesFields.tsx | 8 +- app/(portal)/profile/LifestyleFields.tsx | 19 +- app/(portal)/profile/PasswordForm.tsx | 8 +- app/(portal)/profile/PersonalFields.tsx | 86 +++- app/(portal)/profile/PrivacyCard.tsx | 8 +- app/(portal)/profile/ProfileForm.tsx | 82 ++-- app/(portal)/profile/page.tsx | 10 +- app/(portal)/profile/profile-types.ts | 2 +- app/(portal)/profile/profile.module.css | 12 +- app/(portal)/regulation/page.tsx | 20 +- app/[locale]/(auth)/auth.module.css | 16 +- app/[locale]/(auth)/forgot-password/page.tsx | 16 +- app/[locale]/(auth)/login/page.tsx | 8 +- app/[locale]/(auth)/register/page.tsx | 12 +- app/[locale]/(auth)/reset-password/page.tsx | 12 +- app/[locale]/blog/blog.module.css | 4 +- app/[locale]/layout.tsx | 4 +- app/api/account/export/route.ts | 9 +- app/api/account/route.test.ts | 26 +- app/api/account/route.ts | 18 +- app/api/admin/clinicians/[id]/route.ts | 2 +- app/api/admin/clinicians/route.ts | 8 +- app/api/admin/goals/[goalId]/route.test.ts | 17 +- app/api/admin/goals/[goalId]/route.ts | 14 +- app/api/admin/patients/[id]/ai-brief/route.ts | 9 +- .../patients/[id]/ai-differential/route.ts | 4 +- .../admin/patients/[id]/goals/route.test.ts | 17 +- app/api/admin/patients/[id]/goals/route.ts | 5 +- .../admin/patients/[id]/notes/route.test.ts | 22 +- app/api/admin/patients/[id]/notes/route.ts | 17 +- .../admin/patients/[id]/profile/route.test.ts | 6 +- app/api/admin/patients/[id]/profile/route.ts | 14 +- .../patients/[id]/programme/route.test.ts | 47 ++- .../admin/patients/[id]/programme/route.ts | 5 +- app/api/admin/patients/[id]/route.test.ts | 15 +- app/api/admin/patients/[id]/route.ts | 14 +- app/api/admin/patients/route.test.ts | 12 +- app/api/ai/insight/route.ts | 9 +- .../assessment-leads/convert/route.test.ts | 16 +- app/api/assessment-leads/convert/route.ts | 4 +- app/api/assessment-leads/route.test.ts | 12 +- app/api/assessment/route.test.ts | 57 ++- app/api/assessment/route.ts | 22 +- app/api/auth/change-password/route.test.ts | 31 +- app/api/auth/change-password/route.ts | 18 +- app/api/auth/forgot-password/route.test.ts | 17 +- app/api/auth/forgot-password/route.ts | 12 +- app/api/auth/reset-password/route.test.ts | 31 +- app/api/auth/reset-password/route.ts | 25 +- app/api/bookings/[id]/route.test.ts | 33 +- app/api/bookings/[id]/route.ts | 52 ++- app/api/bookings/route.test.ts | 101 +++-- app/api/bookings/route.ts | 372 +++++++++-------- app/api/calendar/[id]/[token]/route.ts | 3 +- app/api/calendars/route.ts | 25 +- app/api/care-team/route.test.ts | 30 +- app/api/care-team/route.ts | 5 +- app/api/checkin/route.test.ts | 132 ++++--- app/api/checkin/route.ts | 40 +- app/api/clinician/accepting-patients/route.ts | 2 +- app/api/clinician/profile/route.ts | 2 +- app/api/cron/checkin-dip-alert/route.test.ts | 16 +- app/api/cron/checkin-reminder/route.test.ts | 4 +- app/api/cron/emails/route.test.ts | 4 +- app/api/cron/signals/route.test.ts | 8 +- app/api/cron/weekly-digest/route.test.ts | 8 +- app/api/documents/[id]/file/route.test.ts | 10 +- app/api/documents/[id]/file/route.ts | 10 +- app/api/documents/route.test.ts | 46 ++- app/api/documents/route.ts | 17 +- app/api/documents/upload/route.test.ts | 45 ++- app/api/documents/upload/route.ts | 31 +- app/api/goals/route.test.ts | 13 +- app/api/health/route.ts | 2 +- app/api/measurements/[id]/route.ts | 7 +- app/api/measurements/route.ts | 10 +- .../messages/[threadId]/assistant/route.ts | 7 +- app/api/messages/[threadId]/route.test.ts | 2 +- app/api/messages/[threadId]/route.ts | 26 +- app/api/messages/route.test.ts | 6 +- app/api/messages/route.ts | 10 +- app/api/profile/route.test.ts | 76 ++-- app/api/profile/route.ts | 17 +- app/api/route-auth-invariant.test.ts | 4 +- app/api/user-isolation.test.ts | 16 +- app/fonts.test.ts | 2 +- app/globals.css | 124 +++--- app/layout.tsx | 12 +- app/opengraph-image.tsx | 2 +- app/robots.ts | 6 +- app/shared.module.css | 41 +- app/sitemap.ts | 4 +- components/AskAssistant.tsx | 3 +- components/Assessment/ResultsScreen.tsx | 22 +- components/Assessment/index.tsx | 49 +-- components/LanguageSwitcher.module.css | 8 +- components/LanguageSwitcher.tsx | 16 +- components/LoadingState.module.css | 7 +- components/RegulationNotice.tsx | 14 +- components/admin/AcceptingPatientsToggle.tsx | 4 +- components/admin/AdminBookingForm.tsx | 38 +- components/admin/AdminNav.tsx | 183 ++++++--- components/admin/AdminProfileEditForm.tsx | 202 ++++++++-- components/admin/AiBriefCard.tsx | 19 +- components/admin/CalendarSubscribeCard.tsx | 17 +- components/admin/CalendarSubscriptions.tsx | 18 +- components/admin/CareTeamCard.tsx | 6 +- components/admin/ClinicianManager.tsx | 17 +- components/admin/ClinicianSettingsForm.tsx | 137 +++++-- components/admin/DocumentAddForm.tsx | 24 +- components/admin/InlineMessageCompose.tsx | 5 +- components/admin/PatientAssessmentCard.tsx | 106 ++--- components/admin/PatientBookingsCard.tsx | 11 +- components/admin/PatientGoalsCard.tsx | 145 +++++-- components/admin/PatientMessagesCard.tsx | 32 +- components/admin/PatientProfileCard.tsx | 12 +- components/admin/ProgrammeAssignmentForm.tsx | 44 ++- components/clinical/BookingActions.tsx | 19 +- components/clinical/MeasurementsPanel.tsx | 22 +- components/portal/AssessmentTrendChart.tsx | 75 ++-- components/portal/CheckinTrendChart.tsx | 90 ++--- components/portal/NavBreadcrumb.tsx | 4 +- components/portal/PortalNav.tsx | 229 ++++++++--- components/portal/UserDropdown.module.css | 4 +- components/portal/UserDropdown.tsx | 18 +- components/sections/Addiction.tsx | 13 +- components/sections/Cta.tsx | 13 +- components/sections/Footer.module.css | 4 +- components/sections/Footer.tsx | 5 +- components/sections/Hero.module.css | 10 +- components/sections/ImpactStats.tsx | 4 +- components/sections/Nav.module.css | 14 +- components/sections/Nav.tsx | 109 ++++- components/sections/NavAuthActions.tsx | 6 +- components/sections/Pillars.tsx | 9 +- components/sections/Programs.tsx | 5 +- .../sections/PsychedelicReadiness.module.css | 6 +- components/sections/PsychedelicReadiness.tsx | 20 +- components/sections/SylClock.tsx | 14 +- components/shared/NotificationBell.module.css | 4 +- components/shared/NotificationBell.tsx | 21 +- drizzle/meta/0000_snapshot.json | 226 +++-------- drizzle/meta/0001_snapshot.json | 226 +++-------- drizzle/meta/0002_snapshot.json | 255 +++--------- drizzle/meta/0003_snapshot.json | 263 +++--------- drizzle/meta/0004_snapshot.json | 263 +++--------- drizzle/meta/0005_snapshot.json | 263 +++--------- drizzle/meta/0006_snapshot.json | 263 +++--------- drizzle/meta/0007_snapshot.json | 285 +++---------- drizzle/meta/0008_snapshot.json | 293 ++++---------- drizzle/meta/0009_snapshot.json | 317 ++++----------- drizzle/meta/0010_snapshot.json | 331 ++++------------ drizzle/meta/0011_snapshot.json | 331 ++++------------ drizzle/meta/0012_snapshot.json | 331 ++++------------ drizzle/meta/0013_snapshot.json | 365 ++++------------- drizzle/meta/0014_snapshot.json | 365 ++++------------- drizzle/meta/0015_snapshot.json | 373 ++++-------------- drizzle/meta/_journal.json | 2 +- lib/assessment/data.test.ts | 14 +- lib/assessment/data.ts | 4 +- lib/auth/edge.ts | 3 +- lib/auth/guards.ts | 6 +- lib/auth/index.ts | 19 +- lib/config/booking-status.ts | 16 +- lib/config/email-sequences.ts | 18 +- lib/config/marketing-nav.ts | 10 +- lib/config/measurements.test.ts | 18 +- lib/config/measurements.ts | 48 ++- lib/config/portal.ts | 64 ++- lib/config/programmes.ts | 14 +- lib/config/public-url.test.ts | 11 +- lib/config/regulation.ts | 3 +- lib/config/routes.ts | 125 +++--- lib/config/scheduling.ts | 20 +- lib/config/team.ts | 2 +- lib/css-order.test.ts | 4 +- lib/db/relations.test.ts | 8 +- lib/db/schema-discipline.test.ts | 8 +- lib/db/schema-usable.ts | 6 +- lib/db/schema.ts | 71 ++-- lib/db/tables.test.ts | 6 +- lib/design-discipline.test.ts | 4 +- lib/domain/ai-brief.ts | 8 +- lib/domain/assessment.test.ts | 8 +- lib/domain/assessment.ts | 7 +- lib/domain/auth.test.ts | 26 +- lib/domain/auth.ts | 19 +- lib/domain/bookings.ts | 6 +- lib/domain/calendar-fetch.test.ts | 4 +- lib/domain/calendar-fetch.ts | 28 +- lib/domain/calendar-sync.ts | 44 ++- lib/domain/calendar-token.ts | 5 +- lib/domain/cancellation.ts | 17 +- lib/domain/care-team.ts | 6 +- lib/domain/checkin.test.ts | 15 +- lib/domain/checkin.ts | 20 +- lib/domain/clinical-inbox-data.ts | 8 +- lib/domain/clinical-inbox.test.ts | 82 +++- lib/domain/clinician-discipline.test.ts | 14 +- lib/domain/clinician-profile.test.ts | 4 +- lib/domain/clinician-profile.ts | 37 +- lib/domain/clinicians.test.ts | 24 +- lib/domain/clinicians.ts | 2 +- lib/domain/email-queue.ts | 6 +- lib/domain/goals.test.ts | 8 +- lib/domain/goals.ts | 4 +- lib/domain/ics-parse.test.ts | 44 +-- lib/domain/ics-parse.ts | 10 +- lib/domain/ics.ts | 14 +- lib/domain/measurements.ts | 4 +- lib/domain/message-notifications.ts | 13 +- lib/domain/messages-view.ts | 20 +- lib/domain/messages.test.ts | 16 +- lib/domain/messages.ts | 24 +- lib/domain/next-step.test.ts | 14 +- lib/domain/next-step.ts | 9 +- lib/domain/notifications.ts | 6 +- lib/domain/patient-access.ts | 2 +- lib/domain/patients.ts | 2 +- lib/domain/profile-discipline.test.ts | 7 +- lib/domain/profile.test.ts | 10 +- lib/domain/profile.ts | 29 +- lib/domain/programmes.test.ts | 5 +- lib/domain/scheduling-data.ts | 4 +- lib/domain/scheduling.test.ts | 42 +- lib/domain/scheduling.ts | 6 +- lib/domain/signals.test.ts | 20 +- lib/domain/signals.ts | 36 +- lib/domain/thread-assistant.test.ts | 21 +- lib/domain/thread-assistant.ts | 2 +- lib/email/index.test.ts | 6 +- lib/email/index.ts | 7 +- lib/email/templates.test.ts | 163 ++++++-- lib/email/templates.ts | 166 +++++--- lib/i18n/navigation.ts | 3 +- lib/storage-discipline.test.ts | 4 +- lib/storage.ts | 7 +- lib/utils/api-response.ts | 2 +- lib/utils/format.ts | 9 +- lib/utils/post-response.ts | 5 +- lib/utils/storage.test.ts | 26 +- lib/utils/storage.ts | 14 +- lib/utils/timezone.ts | 5 +- lib/workflows/cron-checkin-dip-alert.test.ts | 29 +- lib/workflows/cron-checkin-dip-alert.ts | 23 +- lib/workflows/cron-checkin-reminder.test.ts | 6 +- lib/workflows/cron-checkin-reminder.ts | 15 +- lib/workflows/cron-emails.test.ts | 10 +- lib/workflows/cron-emails.ts | 25 +- lib/workflows/cron-orphaned-files.test.ts | 4 +- lib/workflows/cron-signals.test.ts | 121 ++++-- lib/workflows/cron-signals.ts | 78 +++- lib/workflows/cron-weekly-digest.test.ts | 30 +- lib/workflows/cron-weekly-digest.ts | 23 +- messages/de.json | 51 +-- messages/en.json | 56 +-- messages/fr.json | 42 +- pnpm-workspace.yaml | 8 +- proxy.ts | 29 +- 304 files changed, 6314 insertions(+), 6541 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 29dcf88..f814320 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -19,10 +19,10 @@ name: Auto-merge on: workflow_run: - workflows: ['CI'] + workflows: ["CI"] types: [completed] schedule: - - cron: '*/10 * * * *' + - cron: "*/10 * * * *" workflow_dispatch: {} # Declared on the CALLER as well as inside the reusable workflow: a called @@ -46,4 +46,4 @@ jobs: # SPACE-separated: the sweep word-splits this. A comma would become one # bogus token, every dispatch would fail, and the only symptom would be # that nothing deploys — while the sweep still reported success. - rearm_workflows: 'ci.yml deploy.yml' + rearm_workflows: "ci.yml deploy.yml" diff --git a/app/(admin)/admin.module.css b/app/(admin)/admin.module.css index de35a01..65b860c 100644 --- a/app/(admin)/admin.module.css +++ b/app/(admin)/admin.module.css @@ -67,7 +67,9 @@ color: var(--white-60); text-decoration: none; font-size: 0.82rem; - transition: background var(--transition-fast), color var(--transition-fast); + transition: + background var(--transition-fast), + color var(--transition-fast); display: flex; align-items: center; gap: 0.65rem; @@ -142,7 +144,6 @@ background: var(--warn); } - .mainWrap { composes: mainWrap from "../shared.module.css"; } @@ -163,7 +164,6 @@ composes: main from "../shared.module.css"; } - /* Shared admin UI — geometry from app/shared.module.css */ .pageTitle { composes: pageTitle from "../shared.module.css"; @@ -181,9 +181,15 @@ composes: cardMb from "../shared.module.css"; } -.checkboxRow { composes: checkboxRow from "../shared.module.css"; } -.checkboxInput { composes: checkboxInput from "../shared.module.css"; } -.checkboxHint { composes: checkboxHint from "../shared.module.css"; } +.checkboxRow { + composes: checkboxRow from "../shared.module.css"; +} +.checkboxInput { + composes: checkboxInput from "../shared.module.css"; +} +.checkboxHint { + composes: checkboxHint from "../shared.module.css"; +} .table { width: 100%; @@ -310,12 +316,31 @@ margin-top: 0.2rem; } -.phasePill[data-phase="intake"] { color: var(--muted); background: var(--muted-10); } -.phasePill[data-phase="assessment"] { color: var(--teal); background: var(--teal-10); } -.phasePill[data-phase="planning"] { color: var(--warn); background: var(--warn-10); } -.phasePill[data-phase="active"] { color: var(--teal); background: var(--teal-15); font-weight: 600; } -.phasePill[data-phase="review"] { color: var(--warn); background: var(--warn-10); } -.phasePill[data-phase="completed"] { color: var(--muted); background: var(--muted-8); } +.phasePill[data-phase="intake"] { + color: var(--muted); + background: var(--muted-10); +} +.phasePill[data-phase="assessment"] { + color: var(--teal); + background: var(--teal-10); +} +.phasePill[data-phase="planning"] { + color: var(--warn); + background: var(--warn-10); +} +.phasePill[data-phase="active"] { + color: var(--teal); + background: var(--teal-15); + font-weight: 600; +} +.phasePill[data-phase="review"] { + color: var(--warn); + background: var(--warn-10); +} +.phasePill[data-phase="completed"] { + color: var(--muted); + background: var(--muted-8); +} /* ─── Sparkline ──────────────────────────────────────────────────────────────── */ @@ -333,9 +358,15 @@ flex-shrink: 0; } -.sparkDot[data-level="low"] { background: var(--danger); } -.sparkDot[data-level="mid"] { background: var(--warn); } -.sparkDot[data-level="high"] { background: var(--teal); } +.sparkDot[data-level="low"] { + background: var(--danger); +} +.sparkDot[data-level="mid"] { + background: var(--warn); +} +.sparkDot[data-level="high"] { + background: var(--teal); +} /* ─── Card label (used in section headers within detail cards) ─────────────── */ @@ -903,7 +934,7 @@ } .stretchedLink::after { - content: ''; + content: ""; position: absolute; inset: 0; z-index: 0; @@ -1010,7 +1041,10 @@ cursor: pointer; text-transform: capitalize; letter-spacing: 0.04em; - transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); + transition: + background var(--transition-fast), + color var(--transition-fast), + border-color var(--transition-fast); } .filterTab:hover, @@ -1903,9 +1937,18 @@ /* Tone is the whole design of this screen: a section that shouts must be one the reader can trust, so only two of the five are allowed to. */ -.inboxTitleUrgent { composes: inboxTitle; color: var(--danger); } -.inboxTitleAttention { composes: inboxTitle; color: var(--warn); } -.inboxTitleRoutine { composes: inboxTitle; color: var(--muted); } +.inboxTitleUrgent { + composes: inboxTitle; + color: var(--danger); +} +.inboxTitleAttention { + composes: inboxTitle; + color: var(--warn); +} +.inboxTitleRoutine { + composes: inboxTitle; + color: var(--muted); +} .inboxCount { font-size: 0.7rem; diff --git a/app/(admin)/admin/bookings/page.tsx b/app/(admin)/admin/bookings/page.tsx index ec3d5f4..17941fe 100644 --- a/app/(admin)/admin/bookings/page.tsx +++ b/app/(admin)/admin/bookings/page.tsx @@ -4,16 +4,24 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import styles from "../../admin.module.css"; import { - BOOKING_STATUS, BOOKING_STATUS_CONFIG, BOOKING_STATUS_VALUES, - BOOKING_TYPE_CONFIG, MACHINE_TYPE_CONFIG, - type BookingRowWithUser, type BookingStatus, + BOOKING_STATUS, + BOOKING_STATUS_CONFIG, + BOOKING_STATUS_VALUES, + BOOKING_TYPE_CONFIG, + MACHINE_TYPE_CONFIG, + type BookingRowWithUser, + type BookingStatus, } from "@/lib/config/booking-status"; import { BookingActions } from "@/components/clinical/BookingActions"; -import { formatDateShort, formatDateNumeric, formatSlotDay, formatSlotTime } from "@/lib/utils/format"; +import { + formatDateShort, + formatDateNumeric, + formatSlotDay, + formatSlotTime, +} from "@/lib/utils/format"; import { ADMIN_ROUTES } from "@/lib/config/routes"; import { LoadingState } from "@/components/LoadingState"; - // "upcoming" answers the clinician's daily question — who am I seeing next? — // and is the default: slot bookings are born confirmed, so the old "pending" // default showed an empty list on a normal day. @@ -33,7 +41,10 @@ export default function AdminBookingsPage() { const load = useCallback(async () => { try { const res = await fetch("/api/bookings"); - if (!res.ok) { setLoadError(true); return; } + if (!res.ok) { + setLoadError(true); + return; + } const data = await res.json(); setBookings(data.data ?? []); } catch { @@ -43,7 +54,9 @@ export default function AdminBookingsPage() { } }, []); - useEffect(() => { load(); }, [load]); + useEffect(() => { + load(); + }, [load]); // Clears the admin nav's "new bookings" badge — fire-and-forget, the same // "I looked at this page" signal the patient portal's goals page sends. @@ -72,7 +85,12 @@ export default function AdminBookingsPage() { const now = Date.now(); const upcoming = bookings - .filter((b) => b.scheduledAt && new Date(b.scheduledAt).getTime() >= now && ACTIVE_STATUSES.includes(b.status)) + .filter( + (b) => + b.scheduledAt && + new Date(b.scheduledAt).getTime() >= now && + ACTIVE_STATUSES.includes(b.status), + ) .sort((a, b) => new Date(a.scheduledAt!).getTime() - new Date(b.scheduledAt!).getTime()); const filtered = filter === "upcoming" @@ -100,7 +118,9 @@ export default function AdminBookingsPage() { went unused: nobody had connected a real calendar. */}

Working hours, intake and calendar sync now live on{" "} - My Profile → + + My Profile → +

@@ -111,7 +131,8 @@ export default function AdminBookingsPage() { onClick={() => setFilter(f)} className={`${styles.filterTab}${filter === f ? ` ${styles.filterTabActive}` : ""}`} > - {f}{f === BOOKING_STATUS.pending && pendingCount > 0 ? ` (${pendingCount})` : ""} + {f} + {f === BOOKING_STATUS.pending && pendingCount > 0 ? ` (${pendingCount})` : ""} ))}
@@ -155,22 +176,29 @@ export default function AdminBookingsPage() { {b.user.name ?? No name}
{b.user.email}
- + View patient →
{BOOKING_TYPE_CONFIG[b.bookingType]?.label ?? b.bookingType}
{b.machineType && ( -
{MACHINE_TYPE_CONFIG[b.machineType]?.label}
+
+ {MACHINE_TYPE_CONFIG[b.machineType]?.label} +
)} - {b.scheduledAt - ? `${formatSlotDay(b.scheduledAt)}, ${formatSlotTime(b.scheduledAt)}` - : b.preferredDate - ? formatDateShort(b.preferredDate) - : } + {b.scheduledAt ? ( + `${formatSlotDay(b.scheduledAt)}, ${formatSlotTime(b.scheduledAt)}` + ) : b.preferredDate ? ( + formatDateShort(b.preferredDate) + ) : ( + + )} {b.notes ? ( @@ -179,13 +207,9 @@ export default function AdminBookingsPage() { )} - - {formatDateNumeric(b.createdAt)} - + {formatDateNumeric(b.createdAt)} - - {s.label} - + {s.label} {/* A late cancellation is clinical information, not just accounting — a pattern of them is worth a conversation. */} {b.lateCancellation && ( diff --git a/app/(admin)/admin/documents/page.tsx b/app/(admin)/admin/documents/page.tsx index 0d5bc28..42d14e2 100644 --- a/app/(admin)/admin/documents/page.tsx +++ b/app/(admin)/admin/documents/page.tsx @@ -97,25 +97,21 @@ export default function AdminDocumentsPage() {

Patient documents and lab results

- - {success && ( -
Document added successfully.
- )} + {success &&
Document added successfully.
} {showForm && (

New document

- +
- + {error &&

{error}

}
- -
@@ -201,7 +193,10 @@ export default function AdminDocumentsPage() { {documents.map((doc) => ( - +
{doc.user.name ?? No name}
@@ -209,12 +204,8 @@ export default function AdminDocumentsPage() { {doc.title} - - {doc.mimeType ?? "—"} - - - {formatDateShort(doc.createdAt)} - + {doc.mimeType ?? "—"} + {formatDateShort(doc.createdAt)} { try { const res = await fetch(`/api/messages/${threadId}`); - if (!res.ok) { setLoadError(true); return; } + if (!res.ok) { + setLoadError(true); + return; + } const data = await res.json(); setThread(data.data); } catch { @@ -32,7 +35,9 @@ export default function AdminThreadPage() { } }, [threadId]); - useEffect(() => { load(); }, [load]); + useEffect(() => { + load(); + }, [load]); // Poll for new messages while the tab is focused — matches portal thread behaviour useEffect(() => { @@ -42,7 +47,9 @@ export default function AdminThreadPage() { return () => clearInterval(interval); }, [load]); - useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [thread?.messages.length]); + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [thread?.messages.length]); async function handleSend(e: React.FormEvent) { e.preventDefault(); @@ -55,7 +62,10 @@ export default function AdminThreadPage() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }), }); - if (!res.ok) { setSendError("Failed to send. Please try again."); return; } + if (!res.ok) { + setSendError("Failed to send. Please try again."); + return; + } setBody(""); load(); } catch { @@ -65,7 +75,8 @@ export default function AdminThreadPage() { } } - if (loadError) return
Failed to load thread. Please refresh the page.
; + if (loadError) + return
Failed to load thread. Please refresh the page.
; if (!thread) return ; return ( @@ -80,7 +91,10 @@ export default function AdminThreadPage() { Patient: {thread.patient.name ?? thread.patient.email} {thread.patient.name && ` · ${thread.patient.email}`} {" · "} - + View profile → {thread.clinician?.name && ` · → ${thread.clinician.name}`} @@ -124,7 +138,12 @@ export default function AdminThreadPage() { onChange={(e) => setBody(e.target.value)} maxLength={MESSAGE_BODY_MAX_LENGTH} placeholder="Reply to patient…" - onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(e); } }} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(e); + } + }} />
@@ -84,15 +96,11 @@ export function BookingHistoryList({ {machineLabel ? `${typeLabel} — ${machineLabel}` : typeLabel} {b.clinician?.name ? ` with ${b.clinician.name}` : ""}

- {b.scheduledAt && ( -

{CLINIC_LOCATION}

- )} + {b.scheduledAt &&

{CLINIC_LOCATION}

} {b.notes &&

{b.notes}

}
- - {s.label} - + {s.label} {/* Was gated to `pending`, which is a status no booked appointment ever has — every patient who picked a time was stuck with it. The control now decides for itself, diff --git a/app/(portal)/bookings/ClinicianPicker.tsx b/app/(portal)/bookings/ClinicianPicker.tsx index b801078..c1bf7f3 100644 --- a/app/(portal)/bookings/ClinicianPicker.tsx +++ b/app/(portal)/bookings/ClinicianPicker.tsx @@ -21,13 +21,24 @@ type Props = { }; /** Step 1 of the booking picker: "who you'd like to see". */ -export function ClinicianPicker({ clinicians, clinicianId, careTeam, selfId, nextFreeDay, onSelect }: Props) { +export function ClinicianPicker({ + clinicians, + clinicianId, + careTeam, + selfId, + nextFreeDay, + onSelect, +}: Props) { if (clinicians.length === 0) return null; return (

1 · Who you'd like to see

-
+
{clinicians.map((c) => { const active = c.id === clinicianId; const mine = careTeam.includes(c.id); @@ -56,7 +67,9 @@ export function ClinicianPicker({ clinicians, clinicianId, careTeam, selfId, nex {c.name ?? "Clinician"} {mine && Your clinician} {closedToMe && ( - Not accepting new patients + + Not accepting new patients + )} diff --git a/app/(portal)/bookings/RequestBookingForm.tsx b/app/(portal)/bookings/RequestBookingForm.tsx index 593305f..837bf4a 100644 --- a/app/(portal)/bookings/RequestBookingForm.tsx +++ b/app/(portal)/bookings/RequestBookingForm.tsx @@ -32,12 +32,18 @@ type Props = { * confirmed slot. */ export function RequestBookingForm({ - bookingType, onBookingTypeChange, - machineType, onMachineTypeChange, - preferredDate, onPreferredDateChange, - notes, onNotesChange, - submitting, submitError, - onSubmit, onCancel, + bookingType, + onBookingTypeChange, + machineType, + onMachineTypeChange, + preferredDate, + onPreferredDateChange, + notes, + onNotesChange, + submitting, + submitError, + onSubmit, + onCancel, }: Props) { return (
@@ -55,7 +61,10 @@ export function RequestBookingForm({ key={t} type="button" className={`${bookingStyles.typeBtn}${bookingType === t ? ` ${bookingStyles.typeBtnActive}` : ""}`} - onClick={() => { onBookingTypeChange(t); onMachineTypeChange(""); }} + onClick={() => { + onBookingTypeChange(t); + onMachineTypeChange(""); + }} > {BOOKING_TYPE_CONFIG[t].label} @@ -65,7 +74,9 @@ export function RequestBookingForm({ {bookingType === "machine" && (
- +
)}
- - onPreferredDateChange(e.target.value)} /> + + onPreferredDateChange(e.target.value)} + />