From 2b1298eeb87262256608c40c5ae91eb2fd5f83f6 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 17 Aug 2026 23:12:25 +0700 Subject: [PATCH 1/3] build: replace TypeScript toolchain with strict checkJs JS setup - one strict checkJs jsconfig.json replaces tsconfig.json (keeps noUncheckedIndexedAccess); typecheck script now tsc -p jsconfig.json - next.config.ts -> next.config.mjs; vitest/playwright configs -> .js - db scripts run under plain node as .mjs; tsx devDep removed - generated Supabase types move to types/supabase-bsk.d.ts and db:gen-types redirects there - eslint drops eslint-config-next/typescript, adds eslint-plugin-jsdoc; factory-boundary no-restricted-imports globs now target .js --- README.md | 6 +- .../admin/doctors/{actions.ts => actions.js} | 0 ...dd-doctor-form.tsx => add-doctor-form.jsx} | 0 .../admin/doctors/{page.tsx => page.jsx} | 0 .../admin/invite/{actions.ts => actions.js} | 0 ...ite-user-form.tsx => invite-user-form.jsx} | 0 .../(app)/admin/invite/{page.tsx => page.jsx} | 0 .../(app)/admin/{layout.tsx => layout.jsx} | 0 .../[id]/edit/{page.tsx => page.jsx} | 0 .../medicines/{actions.ts => actions.js} | 0 .../{medicine-form.tsx => medicine-form.jsx} | 0 .../medicines/new/{page.tsx => page.jsx} | 0 .../admin/medicines/{page.tsx => page.jsx} | 0 .../admin/reports/{page.tsx => page.jsx} | 0 .../admin/services/{actions.ts => actions.js} | 0 ...-service-form.tsx => add-service-form.jsx} | 0 .../admin/services/{page.tsx => page.jsx} | 0 .../admin/settings/{actions.ts => actions.js} | 0 ...ings-form.tsx => clinic-settings-form.jsx} | 0 .../admin/settings/{page.tsx => page.jsx} | 0 .../admin/staff/{actions.ts => actions.js} | 0 .../(app)/admin/staff/{page.tsx => page.jsx} | 0 .../[id]/edit/{page.tsx => page.jsx} | 0 .../templates/{actions.ts => actions.js} | 0 .../templates/new/{page.tsx => page.jsx} | 0 .../admin/templates/{page.tsx => page.jsx} | 0 .../{template-form.tsx => template-form.jsx} | 0 .../[id]/imaging/{actions.ts => actions.js} | 0 ...heckup-barcode.tsx => checkup-barcode.jsx} | 0 .../{image-capture.tsx => image-capture.jsx} | 0 .../{image-gallery.tsx => image-gallery.jsx} | 0 .../[id]/imaging/{page.tsx => page.jsx} | 0 .../[id]/invoice/{route.ts => route.js} | 0 .../checkups/[id]/{page.tsx => page.jsx} | 0 .../prescription/{actions.ts => actions.js} | 0 .../[id]/prescription/{page.tsx => page.jsx} | 0 .../prescription/pdf/{route.ts => route.js} | 0 ...composer.tsx => prescription-composer.jsx} | 0 .../[id]/report/{route.ts => route.js} | 0 .../(app)/checkups/{actions.ts => actions.js} | 0 .../{checkup-form.tsx => checkup-form.jsx} | 0 ...p-button.tsx => delete-checkup-button.jsx} | 0 .../(app)/checkups/{layout.tsx => layout.jsx} | 0 .../(app)/dashboard/{page.tsx => page.jsx} | 0 .../{revenue-chart.tsx => revenue-chart.jsx} | 0 app/[locale]/(app)/{layout.tsx => layout.jsx} | 0 .../(app)/{loading.tsx => loading.jsx} | 0 .../patients/[id]/edit/{page.tsx => page.jsx} | 0 .../patients/[id]/{page.tsx => page.jsx} | 0 .../(app)/patients/{actions.ts => actions.js} | 0 .../(app)/patients/{layout.tsx => layout.jsx} | 0 .../(app)/patients/new/{page.tsx => page.jsx} | 0 .../(app)/patients/{page.tsx => page.jsx} | 0 .../{patient-form.tsx => patient-form.jsx} | 0 .../(app)/queue/{actions.ts => actions.js} | 0 ...l-next-button.tsx => call-next-button.jsx} | 0 .../{counter-form.tsx => counter-form.jsx} | 0 .../(app)/queue/{layout.tsx => layout.jsx} | 0 .../(app)/queue/{page.tsx => page.jsx} | 0 ...{queue-realtime.tsx => queue-realtime.jsx} | 0 .../{register-form.tsx => register-form.jsx} | 0 .../reminders/{layout.tsx => layout.jsx} | 0 .../(app)/reminders/{page.tsx => page.jsx} | 0 .../reports/catalog/{route.ts => route.js} | 0 .../reports/patients/{route.ts => route.js} | 0 .../reports/revenue/{route.ts => route.js} | 0 .../reports/visits/{route.ts => route.js} | 0 .../(auth)/{layout.tsx => layout.jsx} | 0 .../(auth)/sign-in/{actions.ts => actions.js} | 0 .../(auth)/sign-in/{page.tsx => page.jsx} | 0 .../{sign-in-form.tsx => sign-in-form.jsx} | 0 app/[locale]/{error.tsx => error.jsx} | 0 app/[locale]/{layout.tsx => layout.jsx} | 0 app/[locale]/{not-found.tsx => not-found.jsx} | 0 app/[locale]/{page.tsx => page.jsx} | 0 app/api/cron/nightly/{route.ts => route.js} | 0 app/{layout.tsx => layout.jsx} | 0 app/{not-found.tsx => not-found.jsx} | 0 components.json | 2 +- ...pp-shell-frame.tsx => app-shell-frame.jsx} | 0 .../{app-shell.tsx => app-shell.jsx} | 0 ...ocale-switcher.tsx => locale-switcher.jsx} | 0 .../{sidebar-nav.tsx => sidebar-nav.jsx} | 0 .../app-shell/{sidebar.tsx => sidebar.jsx} | 0 ...ign-out-button.tsx => sign-out-button.jsx} | 0 components/ui/{badge.tsx => badge.jsx} | 0 components/ui/{button.tsx => button.jsx} | 0 components/ui/{input.tsx => input.jsx} | 0 components/ui/{label.tsx => label.jsx} | 0 .../ui/{separator.tsx => separator.jsx} | 0 components/ui/{sonner.tsx => sonner.jsx} | 0 docs/supabase-shared-config.md | 2 +- eslint.config.mjs | 54 +- i18n/{navigation.ts => navigation.js} | 0 i18n/{request.ts => request.js} | 0 i18n/{routing.ts => routing.js} | 0 tsconfig.json => jsconfig.json | 23 +- ...erver-session.ts => get-server-session.js} | 0 .../{invite-schema.ts => invite-schema.js} | 0 lib/auth/{require-role.ts => require-role.js} | 0 lib/auth/{role-menu.ts => role-menu.js} | 0 lib/auth/{schemas.ts => schemas.js} | 0 ...sion-provider.tsx => session-provider.jsx} | 0 .../{dose-presets.ts => dose-presets.js} | 0 ...ption-schema.ts => prescription-schema.js} | 0 lib/billing/{totals.ts => totals.js} | 0 ...{medicine-schema.ts => medicine-schema.js} | 0 .../{service-schema.ts => service-schema.js} | 0 .../{checkup-schema.ts => checkup-schema.js} | 0 ...gs-schema.ts => clinic-settings-schema.js} | 0 ...{customer-schema.ts => customer-schema.js} | 0 lib/db/{roles.ts => roles.js} | 0 .../{doctor-schema.ts => doctor-schema.js} | 0 lib/env/{client.ts => client.js} | 0 lib/env/{server.ts => server.js} | 0 .../{image-schema.ts => image-schema.js} | 0 lib/pdf/{fonts.ts => fonts.js} | 0 ...oice-document.tsx => invoice-document.jsx} | 0 lib/pdf/{patient-info.ts => patient-info.js} | 0 ...document.tsx => prescription-document.jsx} | 0 ...d-document.tsx => ultrasound-document.jsx} | 0 .../{copy-cookies.ts => copy-cookies.js} | 0 lib/supabase/{admin.ts => admin.js} | 0 lib/supabase/{client.ts => client.js} | 0 lib/supabase/{server.ts => server.js} | 0 lib/supabase/{session.ts => session.js} | 0 ...{template-schema.ts => template-schema.js} | 0 lib/{upstash.ts => upstash.js} | 0 lib/{utils.ts => utils.js} | 0 next.config.ts => next.config.mjs | 6 +- package-lock.json | 796 ++++++------------ package.json | 10 +- playwright.config.ts => playwright.config.js | 0 proxy.ts => proxy.js | 0 ...ght-supabase.ts => preflight-supabase.mjs} | 20 +- scripts/{seed-geo.ts => seed-geo.mjs} | 30 +- .../{auth-gate.spec.ts => auth-gate.spec.js} | 0 ...n-and-404.spec.ts => i18n-and-404.spec.js} | 0 ...n-in-page.spec.ts => sign-in-page.spec.js} | 0 ...-schema.test.ts => checkup-schema.test.js} | 0 ...schema.test.ts => customer-schema.test.js} | 0 ...schema.test.ts => medicine-schema.test.js} | 0 .../mocks/{server-only.ts => server-only.js} | 0 ...ient-info.test.ts => patient-info.test.js} | 0 ...-schema.test.ts => service-schema.test.js} | 0 ...schema.test.ts => template-schema.test.js} | 0 tests/unit/{totals.test.ts => totals.test.js} | 0 types/{supabase-bsk.ts => supabase-bsk.d.ts} | 0 vitest.config.ts => vitest.config.js | 8 +- 149 files changed, 348 insertions(+), 609 deletions(-) rename app/[locale]/(app)/admin/doctors/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/doctors/{add-doctor-form.tsx => add-doctor-form.jsx} (100%) rename app/[locale]/(app)/admin/doctors/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/invite/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/invite/{invite-user-form.tsx => invite-user-form.jsx} (100%) rename app/[locale]/(app)/admin/invite/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/admin/medicines/[id]/edit/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/medicines/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/medicines/{medicine-form.tsx => medicine-form.jsx} (100%) rename app/[locale]/(app)/admin/medicines/new/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/medicines/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/reports/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/services/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/services/{add-service-form.tsx => add-service-form.jsx} (100%) rename app/[locale]/(app)/admin/services/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/settings/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/settings/{clinic-settings-form.tsx => clinic-settings-form.jsx} (100%) rename app/[locale]/(app)/admin/settings/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/staff/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/staff/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/templates/[id]/edit/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/templates/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/admin/templates/new/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/templates/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/admin/templates/{template-form.tsx => template-form.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/imaging/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/checkups/[id]/imaging/{checkup-barcode.tsx => checkup-barcode.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/imaging/{image-capture.tsx => image-capture.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/imaging/{image-gallery.tsx => image-gallery.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/imaging/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/invoice/{route.ts => route.js} (100%) rename app/[locale]/(app)/checkups/[id]/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/prescription/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/checkups/[id]/prescription/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/prescription/pdf/{route.ts => route.js} (100%) rename app/[locale]/(app)/checkups/[id]/prescription/{prescription-composer.tsx => prescription-composer.jsx} (100%) rename app/[locale]/(app)/checkups/[id]/report/{route.ts => route.js} (100%) rename app/[locale]/(app)/checkups/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/checkups/{checkup-form.tsx => checkup-form.jsx} (100%) rename app/[locale]/(app)/checkups/{delete-checkup-button.tsx => delete-checkup-button.jsx} (100%) rename app/[locale]/(app)/checkups/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/dashboard/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/dashboard/{revenue-chart.tsx => revenue-chart.jsx} (100%) rename app/[locale]/(app)/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/{loading.tsx => loading.jsx} (100%) rename app/[locale]/(app)/patients/[id]/edit/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/patients/[id]/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/patients/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/patients/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/patients/new/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/patients/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/patients/{patient-form.tsx => patient-form.jsx} (100%) rename app/[locale]/(app)/queue/{actions.ts => actions.js} (100%) rename app/[locale]/(app)/queue/{call-next-button.tsx => call-next-button.jsx} (100%) rename app/[locale]/(app)/queue/{counter-form.tsx => counter-form.jsx} (100%) rename app/[locale]/(app)/queue/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/queue/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/queue/{queue-realtime.tsx => queue-realtime.jsx} (100%) rename app/[locale]/(app)/queue/{register-form.tsx => register-form.jsx} (100%) rename app/[locale]/(app)/reminders/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(app)/reminders/{page.tsx => page.jsx} (100%) rename app/[locale]/(app)/reports/catalog/{route.ts => route.js} (100%) rename app/[locale]/(app)/reports/patients/{route.ts => route.js} (100%) rename app/[locale]/(app)/reports/revenue/{route.ts => route.js} (100%) rename app/[locale]/(app)/reports/visits/{route.ts => route.js} (100%) rename app/[locale]/(auth)/{layout.tsx => layout.jsx} (100%) rename app/[locale]/(auth)/sign-in/{actions.ts => actions.js} (100%) rename app/[locale]/(auth)/sign-in/{page.tsx => page.jsx} (100%) rename app/[locale]/(auth)/sign-in/{sign-in-form.tsx => sign-in-form.jsx} (100%) rename app/[locale]/{error.tsx => error.jsx} (100%) rename app/[locale]/{layout.tsx => layout.jsx} (100%) rename app/[locale]/{not-found.tsx => not-found.jsx} (100%) rename app/[locale]/{page.tsx => page.jsx} (100%) rename app/api/cron/nightly/{route.ts => route.js} (100%) rename app/{layout.tsx => layout.jsx} (100%) rename app/{not-found.tsx => not-found.jsx} (100%) rename components/app-shell/{app-shell-frame.tsx => app-shell-frame.jsx} (100%) rename components/app-shell/{app-shell.tsx => app-shell.jsx} (100%) rename components/app-shell/{locale-switcher.tsx => locale-switcher.jsx} (100%) rename components/app-shell/{sidebar-nav.tsx => sidebar-nav.jsx} (100%) rename components/app-shell/{sidebar.tsx => sidebar.jsx} (100%) rename components/app-shell/{sign-out-button.tsx => sign-out-button.jsx} (100%) rename components/ui/{badge.tsx => badge.jsx} (100%) rename components/ui/{button.tsx => button.jsx} (100%) rename components/ui/{input.tsx => input.jsx} (100%) rename components/ui/{label.tsx => label.jsx} (100%) rename components/ui/{separator.tsx => separator.jsx} (100%) rename components/ui/{sonner.tsx => sonner.jsx} (100%) rename i18n/{navigation.ts => navigation.js} (100%) rename i18n/{request.ts => request.js} (100%) rename i18n/{routing.ts => routing.js} (100%) rename tsconfig.json => jsconfig.json (67%) rename lib/auth/{get-server-session.ts => get-server-session.js} (100%) rename lib/auth/{invite-schema.ts => invite-schema.js} (100%) rename lib/auth/{require-role.ts => require-role.js} (100%) rename lib/auth/{role-menu.ts => role-menu.js} (100%) rename lib/auth/{schemas.ts => schemas.js} (100%) rename lib/auth/{session-provider.tsx => session-provider.jsx} (100%) rename lib/billing/{dose-presets.ts => dose-presets.js} (100%) rename lib/billing/{prescription-schema.ts => prescription-schema.js} (100%) rename lib/billing/{totals.ts => totals.js} (100%) rename lib/catalog/{medicine-schema.ts => medicine-schema.js} (100%) rename lib/catalog/{service-schema.ts => service-schema.js} (100%) rename lib/checkups/{checkup-schema.ts => checkup-schema.js} (100%) rename lib/clinic/{clinic-settings-schema.ts => clinic-settings-schema.js} (100%) rename lib/customers/{customer-schema.ts => customer-schema.js} (100%) rename lib/db/{roles.ts => roles.js} (100%) rename lib/doctors/{doctor-schema.ts => doctor-schema.js} (100%) rename lib/env/{client.ts => client.js} (100%) rename lib/env/{server.ts => server.js} (100%) rename lib/imaging/{image-schema.ts => image-schema.js} (100%) rename lib/pdf/{fonts.ts => fonts.js} (100%) rename lib/pdf/{invoice-document.tsx => invoice-document.jsx} (100%) rename lib/pdf/{patient-info.ts => patient-info.js} (100%) rename lib/pdf/{prescription-document.tsx => prescription-document.jsx} (100%) rename lib/pdf/{ultrasound-document.tsx => ultrasound-document.jsx} (100%) rename lib/proxy/{copy-cookies.ts => copy-cookies.js} (100%) rename lib/supabase/{admin.ts => admin.js} (100%) rename lib/supabase/{client.ts => client.js} (100%) rename lib/supabase/{server.ts => server.js} (100%) rename lib/supabase/{session.ts => session.js} (100%) rename lib/templates/{template-schema.ts => template-schema.js} (100%) rename lib/{upstash.ts => upstash.js} (100%) rename lib/{utils.ts => utils.js} (100%) rename next.config.ts => next.config.mjs (51%) rename playwright.config.ts => playwright.config.js (100%) rename proxy.ts => proxy.js (100%) rename scripts/{preflight-supabase.ts => preflight-supabase.mjs} (86%) rename scripts/{seed-geo.ts => seed-geo.mjs} (83%) rename tests/e2e/{auth-gate.spec.ts => auth-gate.spec.js} (100%) rename tests/e2e/{i18n-and-404.spec.ts => i18n-and-404.spec.js} (100%) rename tests/e2e/{sign-in-page.spec.ts => sign-in-page.spec.js} (100%) rename tests/unit/{checkup-schema.test.ts => checkup-schema.test.js} (100%) rename tests/unit/{customer-schema.test.ts => customer-schema.test.js} (100%) rename tests/unit/{medicine-schema.test.ts => medicine-schema.test.js} (100%) rename tests/unit/mocks/{server-only.ts => server-only.js} (100%) rename tests/unit/{patient-info.test.ts => patient-info.test.js} (100%) rename tests/unit/{service-schema.test.ts => service-schema.test.js} (100%) rename tests/unit/{template-schema.test.ts => template-schema.test.js} (100%) rename tests/unit/{totals.test.ts => totals.test.js} (100%) rename types/{supabase-bsk.ts => supabase-bsk.d.ts} (100%) rename vitest.config.ts => vitest.config.js (71%) diff --git a/README.md b/README.md index 222208d..f852b06 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ After provisioning Supabase/Upstash/Vercel and `npm run db:push`: ```sql INSERT INTO bsk.admin_allowlist (email) VALUES ('you@example.com'); ``` -2. **Seed Vietnamese geo data** (province/ward address dropdowns): `npm run db:seed-geo` (see `scripts/seed-geo.ts`). +2. **Seed Vietnamese geo data** (province/ward address dropdowns): `npm run db:seed-geo` (see `scripts/seed-geo.mjs`). 3. **Enable Supabase Realtime** on `bsk.checkups` (Database → Replication) for the live queue. 4. **Set `CRON_SECRET`** in Vercel so the nightly media-retention sweep (`/api/cron/nightly`, scheduled in `vercel.json`) can authenticate. @@ -34,7 +34,7 @@ See [docs/supabase-shared-config.md](./docs/supabase-shared-config.md) for the s ## Stack -- **npm** + **Next.js 16** (App Router) + **TypeScript** +- **npm** + **Next.js 16** (App Router) + **JavaScript with JSDoc types** (checked by tsc) - **Supabase** (Postgres + Auth + Storage) — shared across personal projects via schema-per-app - **Upstash** Redis + QStash — shared across personal projects via key prefixes - **Vercel** for hosting @@ -48,7 +48,7 @@ See [docs/supabase-shared-config.md](./docs/supabase-shared-config.md) for the s ## Database -After `npm run db:push`, run `npm run db:gen-types` to refresh `types/supabase-bsk.ts`. +After `npm run db:push`, run `npm run db:gen-types` to refresh `types/supabase-bsk.d.ts`. ## License diff --git a/app/[locale]/(app)/admin/doctors/actions.ts b/app/[locale]/(app)/admin/doctors/actions.js similarity index 100% rename from app/[locale]/(app)/admin/doctors/actions.ts rename to app/[locale]/(app)/admin/doctors/actions.js diff --git a/app/[locale]/(app)/admin/doctors/add-doctor-form.tsx b/app/[locale]/(app)/admin/doctors/add-doctor-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/doctors/add-doctor-form.tsx rename to app/[locale]/(app)/admin/doctors/add-doctor-form.jsx diff --git a/app/[locale]/(app)/admin/doctors/page.tsx b/app/[locale]/(app)/admin/doctors/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/doctors/page.tsx rename to app/[locale]/(app)/admin/doctors/page.jsx diff --git a/app/[locale]/(app)/admin/invite/actions.ts b/app/[locale]/(app)/admin/invite/actions.js similarity index 100% rename from app/[locale]/(app)/admin/invite/actions.ts rename to app/[locale]/(app)/admin/invite/actions.js diff --git a/app/[locale]/(app)/admin/invite/invite-user-form.tsx b/app/[locale]/(app)/admin/invite/invite-user-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/invite/invite-user-form.tsx rename to app/[locale]/(app)/admin/invite/invite-user-form.jsx diff --git a/app/[locale]/(app)/admin/invite/page.tsx b/app/[locale]/(app)/admin/invite/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/invite/page.tsx rename to app/[locale]/(app)/admin/invite/page.jsx diff --git a/app/[locale]/(app)/admin/layout.tsx b/app/[locale]/(app)/admin/layout.jsx similarity index 100% rename from app/[locale]/(app)/admin/layout.tsx rename to app/[locale]/(app)/admin/layout.jsx diff --git a/app/[locale]/(app)/admin/medicines/[id]/edit/page.tsx b/app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/medicines/[id]/edit/page.tsx rename to app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx diff --git a/app/[locale]/(app)/admin/medicines/actions.ts b/app/[locale]/(app)/admin/medicines/actions.js similarity index 100% rename from app/[locale]/(app)/admin/medicines/actions.ts rename to app/[locale]/(app)/admin/medicines/actions.js diff --git a/app/[locale]/(app)/admin/medicines/medicine-form.tsx b/app/[locale]/(app)/admin/medicines/medicine-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/medicines/medicine-form.tsx rename to app/[locale]/(app)/admin/medicines/medicine-form.jsx diff --git a/app/[locale]/(app)/admin/medicines/new/page.tsx b/app/[locale]/(app)/admin/medicines/new/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/medicines/new/page.tsx rename to app/[locale]/(app)/admin/medicines/new/page.jsx diff --git a/app/[locale]/(app)/admin/medicines/page.tsx b/app/[locale]/(app)/admin/medicines/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/medicines/page.tsx rename to app/[locale]/(app)/admin/medicines/page.jsx diff --git a/app/[locale]/(app)/admin/reports/page.tsx b/app/[locale]/(app)/admin/reports/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/reports/page.tsx rename to app/[locale]/(app)/admin/reports/page.jsx diff --git a/app/[locale]/(app)/admin/services/actions.ts b/app/[locale]/(app)/admin/services/actions.js similarity index 100% rename from app/[locale]/(app)/admin/services/actions.ts rename to app/[locale]/(app)/admin/services/actions.js diff --git a/app/[locale]/(app)/admin/services/add-service-form.tsx b/app/[locale]/(app)/admin/services/add-service-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/services/add-service-form.tsx rename to app/[locale]/(app)/admin/services/add-service-form.jsx diff --git a/app/[locale]/(app)/admin/services/page.tsx b/app/[locale]/(app)/admin/services/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/services/page.tsx rename to app/[locale]/(app)/admin/services/page.jsx diff --git a/app/[locale]/(app)/admin/settings/actions.ts b/app/[locale]/(app)/admin/settings/actions.js similarity index 100% rename from app/[locale]/(app)/admin/settings/actions.ts rename to app/[locale]/(app)/admin/settings/actions.js diff --git a/app/[locale]/(app)/admin/settings/clinic-settings-form.tsx b/app/[locale]/(app)/admin/settings/clinic-settings-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/settings/clinic-settings-form.tsx rename to app/[locale]/(app)/admin/settings/clinic-settings-form.jsx diff --git a/app/[locale]/(app)/admin/settings/page.tsx b/app/[locale]/(app)/admin/settings/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/settings/page.tsx rename to app/[locale]/(app)/admin/settings/page.jsx diff --git a/app/[locale]/(app)/admin/staff/actions.ts b/app/[locale]/(app)/admin/staff/actions.js similarity index 100% rename from app/[locale]/(app)/admin/staff/actions.ts rename to app/[locale]/(app)/admin/staff/actions.js diff --git a/app/[locale]/(app)/admin/staff/page.tsx b/app/[locale]/(app)/admin/staff/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/staff/page.tsx rename to app/[locale]/(app)/admin/staff/page.jsx diff --git a/app/[locale]/(app)/admin/templates/[id]/edit/page.tsx b/app/[locale]/(app)/admin/templates/[id]/edit/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/templates/[id]/edit/page.tsx rename to app/[locale]/(app)/admin/templates/[id]/edit/page.jsx diff --git a/app/[locale]/(app)/admin/templates/actions.ts b/app/[locale]/(app)/admin/templates/actions.js similarity index 100% rename from app/[locale]/(app)/admin/templates/actions.ts rename to app/[locale]/(app)/admin/templates/actions.js diff --git a/app/[locale]/(app)/admin/templates/new/page.tsx b/app/[locale]/(app)/admin/templates/new/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/templates/new/page.tsx rename to app/[locale]/(app)/admin/templates/new/page.jsx diff --git a/app/[locale]/(app)/admin/templates/page.tsx b/app/[locale]/(app)/admin/templates/page.jsx similarity index 100% rename from app/[locale]/(app)/admin/templates/page.tsx rename to app/[locale]/(app)/admin/templates/page.jsx diff --git a/app/[locale]/(app)/admin/templates/template-form.tsx b/app/[locale]/(app)/admin/templates/template-form.jsx similarity index 100% rename from app/[locale]/(app)/admin/templates/template-form.tsx rename to app/[locale]/(app)/admin/templates/template-form.jsx diff --git a/app/[locale]/(app)/checkups/[id]/imaging/actions.ts b/app/[locale]/(app)/checkups/[id]/imaging/actions.js similarity index 100% rename from app/[locale]/(app)/checkups/[id]/imaging/actions.ts rename to app/[locale]/(app)/checkups/[id]/imaging/actions.js diff --git a/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.tsx b/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.tsx rename to app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx diff --git a/app/[locale]/(app)/checkups/[id]/imaging/image-capture.tsx b/app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/imaging/image-capture.tsx rename to app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx diff --git a/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.tsx b/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/imaging/image-gallery.tsx rename to app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx diff --git a/app/[locale]/(app)/checkups/[id]/imaging/page.tsx b/app/[locale]/(app)/checkups/[id]/imaging/page.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/imaging/page.tsx rename to app/[locale]/(app)/checkups/[id]/imaging/page.jsx diff --git a/app/[locale]/(app)/checkups/[id]/invoice/route.ts b/app/[locale]/(app)/checkups/[id]/invoice/route.js similarity index 100% rename from app/[locale]/(app)/checkups/[id]/invoice/route.ts rename to app/[locale]/(app)/checkups/[id]/invoice/route.js diff --git a/app/[locale]/(app)/checkups/[id]/page.tsx b/app/[locale]/(app)/checkups/[id]/page.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/page.tsx rename to app/[locale]/(app)/checkups/[id]/page.jsx diff --git a/app/[locale]/(app)/checkups/[id]/prescription/actions.ts b/app/[locale]/(app)/checkups/[id]/prescription/actions.js similarity index 100% rename from app/[locale]/(app)/checkups/[id]/prescription/actions.ts rename to app/[locale]/(app)/checkups/[id]/prescription/actions.js diff --git a/app/[locale]/(app)/checkups/[id]/prescription/page.tsx b/app/[locale]/(app)/checkups/[id]/prescription/page.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/prescription/page.tsx rename to app/[locale]/(app)/checkups/[id]/prescription/page.jsx diff --git a/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.ts b/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js similarity index 100% rename from app/[locale]/(app)/checkups/[id]/prescription/pdf/route.ts rename to app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js diff --git a/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.tsx b/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx similarity index 100% rename from app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.tsx rename to app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx diff --git a/app/[locale]/(app)/checkups/[id]/report/route.ts b/app/[locale]/(app)/checkups/[id]/report/route.js similarity index 100% rename from app/[locale]/(app)/checkups/[id]/report/route.ts rename to app/[locale]/(app)/checkups/[id]/report/route.js diff --git a/app/[locale]/(app)/checkups/actions.ts b/app/[locale]/(app)/checkups/actions.js similarity index 100% rename from app/[locale]/(app)/checkups/actions.ts rename to app/[locale]/(app)/checkups/actions.js diff --git a/app/[locale]/(app)/checkups/checkup-form.tsx b/app/[locale]/(app)/checkups/checkup-form.jsx similarity index 100% rename from app/[locale]/(app)/checkups/checkup-form.tsx rename to app/[locale]/(app)/checkups/checkup-form.jsx diff --git a/app/[locale]/(app)/checkups/delete-checkup-button.tsx b/app/[locale]/(app)/checkups/delete-checkup-button.jsx similarity index 100% rename from app/[locale]/(app)/checkups/delete-checkup-button.tsx rename to app/[locale]/(app)/checkups/delete-checkup-button.jsx diff --git a/app/[locale]/(app)/checkups/layout.tsx b/app/[locale]/(app)/checkups/layout.jsx similarity index 100% rename from app/[locale]/(app)/checkups/layout.tsx rename to app/[locale]/(app)/checkups/layout.jsx diff --git a/app/[locale]/(app)/dashboard/page.tsx b/app/[locale]/(app)/dashboard/page.jsx similarity index 100% rename from app/[locale]/(app)/dashboard/page.tsx rename to app/[locale]/(app)/dashboard/page.jsx diff --git a/app/[locale]/(app)/dashboard/revenue-chart.tsx b/app/[locale]/(app)/dashboard/revenue-chart.jsx similarity index 100% rename from app/[locale]/(app)/dashboard/revenue-chart.tsx rename to app/[locale]/(app)/dashboard/revenue-chart.jsx diff --git a/app/[locale]/(app)/layout.tsx b/app/[locale]/(app)/layout.jsx similarity index 100% rename from app/[locale]/(app)/layout.tsx rename to app/[locale]/(app)/layout.jsx diff --git a/app/[locale]/(app)/loading.tsx b/app/[locale]/(app)/loading.jsx similarity index 100% rename from app/[locale]/(app)/loading.tsx rename to app/[locale]/(app)/loading.jsx diff --git a/app/[locale]/(app)/patients/[id]/edit/page.tsx b/app/[locale]/(app)/patients/[id]/edit/page.jsx similarity index 100% rename from app/[locale]/(app)/patients/[id]/edit/page.tsx rename to app/[locale]/(app)/patients/[id]/edit/page.jsx diff --git a/app/[locale]/(app)/patients/[id]/page.tsx b/app/[locale]/(app)/patients/[id]/page.jsx similarity index 100% rename from app/[locale]/(app)/patients/[id]/page.tsx rename to app/[locale]/(app)/patients/[id]/page.jsx diff --git a/app/[locale]/(app)/patients/actions.ts b/app/[locale]/(app)/patients/actions.js similarity index 100% rename from app/[locale]/(app)/patients/actions.ts rename to app/[locale]/(app)/patients/actions.js diff --git a/app/[locale]/(app)/patients/layout.tsx b/app/[locale]/(app)/patients/layout.jsx similarity index 100% rename from app/[locale]/(app)/patients/layout.tsx rename to app/[locale]/(app)/patients/layout.jsx diff --git a/app/[locale]/(app)/patients/new/page.tsx b/app/[locale]/(app)/patients/new/page.jsx similarity index 100% rename from app/[locale]/(app)/patients/new/page.tsx rename to app/[locale]/(app)/patients/new/page.jsx diff --git a/app/[locale]/(app)/patients/page.tsx b/app/[locale]/(app)/patients/page.jsx similarity index 100% rename from app/[locale]/(app)/patients/page.tsx rename to app/[locale]/(app)/patients/page.jsx diff --git a/app/[locale]/(app)/patients/patient-form.tsx b/app/[locale]/(app)/patients/patient-form.jsx similarity index 100% rename from app/[locale]/(app)/patients/patient-form.tsx rename to app/[locale]/(app)/patients/patient-form.jsx diff --git a/app/[locale]/(app)/queue/actions.ts b/app/[locale]/(app)/queue/actions.js similarity index 100% rename from app/[locale]/(app)/queue/actions.ts rename to app/[locale]/(app)/queue/actions.js diff --git a/app/[locale]/(app)/queue/call-next-button.tsx b/app/[locale]/(app)/queue/call-next-button.jsx similarity index 100% rename from app/[locale]/(app)/queue/call-next-button.tsx rename to app/[locale]/(app)/queue/call-next-button.jsx diff --git a/app/[locale]/(app)/queue/counter-form.tsx b/app/[locale]/(app)/queue/counter-form.jsx similarity index 100% rename from app/[locale]/(app)/queue/counter-form.tsx rename to app/[locale]/(app)/queue/counter-form.jsx diff --git a/app/[locale]/(app)/queue/layout.tsx b/app/[locale]/(app)/queue/layout.jsx similarity index 100% rename from app/[locale]/(app)/queue/layout.tsx rename to app/[locale]/(app)/queue/layout.jsx diff --git a/app/[locale]/(app)/queue/page.tsx b/app/[locale]/(app)/queue/page.jsx similarity index 100% rename from app/[locale]/(app)/queue/page.tsx rename to app/[locale]/(app)/queue/page.jsx diff --git a/app/[locale]/(app)/queue/queue-realtime.tsx b/app/[locale]/(app)/queue/queue-realtime.jsx similarity index 100% rename from app/[locale]/(app)/queue/queue-realtime.tsx rename to app/[locale]/(app)/queue/queue-realtime.jsx diff --git a/app/[locale]/(app)/queue/register-form.tsx b/app/[locale]/(app)/queue/register-form.jsx similarity index 100% rename from app/[locale]/(app)/queue/register-form.tsx rename to app/[locale]/(app)/queue/register-form.jsx diff --git a/app/[locale]/(app)/reminders/layout.tsx b/app/[locale]/(app)/reminders/layout.jsx similarity index 100% rename from app/[locale]/(app)/reminders/layout.tsx rename to app/[locale]/(app)/reminders/layout.jsx diff --git a/app/[locale]/(app)/reminders/page.tsx b/app/[locale]/(app)/reminders/page.jsx similarity index 100% rename from app/[locale]/(app)/reminders/page.tsx rename to app/[locale]/(app)/reminders/page.jsx diff --git a/app/[locale]/(app)/reports/catalog/route.ts b/app/[locale]/(app)/reports/catalog/route.js similarity index 100% rename from app/[locale]/(app)/reports/catalog/route.ts rename to app/[locale]/(app)/reports/catalog/route.js diff --git a/app/[locale]/(app)/reports/patients/route.ts b/app/[locale]/(app)/reports/patients/route.js similarity index 100% rename from app/[locale]/(app)/reports/patients/route.ts rename to app/[locale]/(app)/reports/patients/route.js diff --git a/app/[locale]/(app)/reports/revenue/route.ts b/app/[locale]/(app)/reports/revenue/route.js similarity index 100% rename from app/[locale]/(app)/reports/revenue/route.ts rename to app/[locale]/(app)/reports/revenue/route.js diff --git a/app/[locale]/(app)/reports/visits/route.ts b/app/[locale]/(app)/reports/visits/route.js similarity index 100% rename from app/[locale]/(app)/reports/visits/route.ts rename to app/[locale]/(app)/reports/visits/route.js diff --git a/app/[locale]/(auth)/layout.tsx b/app/[locale]/(auth)/layout.jsx similarity index 100% rename from app/[locale]/(auth)/layout.tsx rename to app/[locale]/(auth)/layout.jsx diff --git a/app/[locale]/(auth)/sign-in/actions.ts b/app/[locale]/(auth)/sign-in/actions.js similarity index 100% rename from app/[locale]/(auth)/sign-in/actions.ts rename to app/[locale]/(auth)/sign-in/actions.js diff --git a/app/[locale]/(auth)/sign-in/page.tsx b/app/[locale]/(auth)/sign-in/page.jsx similarity index 100% rename from app/[locale]/(auth)/sign-in/page.tsx rename to app/[locale]/(auth)/sign-in/page.jsx diff --git a/app/[locale]/(auth)/sign-in/sign-in-form.tsx b/app/[locale]/(auth)/sign-in/sign-in-form.jsx similarity index 100% rename from app/[locale]/(auth)/sign-in/sign-in-form.tsx rename to app/[locale]/(auth)/sign-in/sign-in-form.jsx diff --git a/app/[locale]/error.tsx b/app/[locale]/error.jsx similarity index 100% rename from app/[locale]/error.tsx rename to app/[locale]/error.jsx diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.jsx similarity index 100% rename from app/[locale]/layout.tsx rename to app/[locale]/layout.jsx diff --git a/app/[locale]/not-found.tsx b/app/[locale]/not-found.jsx similarity index 100% rename from app/[locale]/not-found.tsx rename to app/[locale]/not-found.jsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.jsx similarity index 100% rename from app/[locale]/page.tsx rename to app/[locale]/page.jsx diff --git a/app/api/cron/nightly/route.ts b/app/api/cron/nightly/route.js similarity index 100% rename from app/api/cron/nightly/route.ts rename to app/api/cron/nightly/route.js diff --git a/app/layout.tsx b/app/layout.jsx similarity index 100% rename from app/layout.tsx rename to app/layout.jsx diff --git a/app/not-found.tsx b/app/not-found.jsx similarity index 100% rename from app/not-found.tsx rename to app/not-found.jsx diff --git a/components.json b/components.json index 4ee62ee..6dc8745 100644 --- a/components.json +++ b/components.json @@ -2,7 +2,7 @@ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, - "tsx": true, + "tsx": false, "tailwind": { "config": "", "css": "app/globals.css", diff --git a/components/app-shell/app-shell-frame.tsx b/components/app-shell/app-shell-frame.jsx similarity index 100% rename from components/app-shell/app-shell-frame.tsx rename to components/app-shell/app-shell-frame.jsx diff --git a/components/app-shell/app-shell.tsx b/components/app-shell/app-shell.jsx similarity index 100% rename from components/app-shell/app-shell.tsx rename to components/app-shell/app-shell.jsx diff --git a/components/app-shell/locale-switcher.tsx b/components/app-shell/locale-switcher.jsx similarity index 100% rename from components/app-shell/locale-switcher.tsx rename to components/app-shell/locale-switcher.jsx diff --git a/components/app-shell/sidebar-nav.tsx b/components/app-shell/sidebar-nav.jsx similarity index 100% rename from components/app-shell/sidebar-nav.tsx rename to components/app-shell/sidebar-nav.jsx diff --git a/components/app-shell/sidebar.tsx b/components/app-shell/sidebar.jsx similarity index 100% rename from components/app-shell/sidebar.tsx rename to components/app-shell/sidebar.jsx diff --git a/components/app-shell/sign-out-button.tsx b/components/app-shell/sign-out-button.jsx similarity index 100% rename from components/app-shell/sign-out-button.tsx rename to components/app-shell/sign-out-button.jsx diff --git a/components/ui/badge.tsx b/components/ui/badge.jsx similarity index 100% rename from components/ui/badge.tsx rename to components/ui/badge.jsx diff --git a/components/ui/button.tsx b/components/ui/button.jsx similarity index 100% rename from components/ui/button.tsx rename to components/ui/button.jsx diff --git a/components/ui/input.tsx b/components/ui/input.jsx similarity index 100% rename from components/ui/input.tsx rename to components/ui/input.jsx diff --git a/components/ui/label.tsx b/components/ui/label.jsx similarity index 100% rename from components/ui/label.tsx rename to components/ui/label.jsx diff --git a/components/ui/separator.tsx b/components/ui/separator.jsx similarity index 100% rename from components/ui/separator.tsx rename to components/ui/separator.jsx diff --git a/components/ui/sonner.tsx b/components/ui/sonner.jsx similarity index 100% rename from components/ui/sonner.tsx rename to components/ui/sonner.jsx diff --git a/docs/supabase-shared-config.md b/docs/supabase-shared-config.md index 8ed45ad..c14b3f3 100644 --- a/docs/supabase-shared-config.md +++ b/docs/supabase-shared-config.md @@ -120,5 +120,5 @@ Channel names live in a single project-wide namespace. Prefix every channel with Free-tier PITR is project-wide; a restore wipes every app's data to a point in time. Per-app rollback is not supported by Supabase. Mitigations: - Daily `pg_dump --schema=bsk` cron (see `docs/runbooks/restore-from-bad-migration.md`). -- Preflight check on migrations (see `scripts/preflight-supabase.ts`). +- Preflight check on migrations (see `scripts/preflight-supabase.mjs`). - Never run destructive DDL without a recent dump. diff --git a/eslint.config.mjs b/eslint.config.mjs index d8d5d8a..17b8c8b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,18 +1,25 @@ import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; -import nextTypeScript from "eslint-config-next/typescript"; import prettier from "eslint-config-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; const config = [ - { ignores: [".next/**", "node_modules/**", "dist/**", "out/**", ".vercel/**"] }, + { + ignores: [ + ".next/**", + "node_modules/**", + "dist/**", + "out/**", + ".vercel/**", + "coverage/**", + "playwright-report/**", + "test-results/**", + ], + }, ...nextCoreWebVitals, - ...nextTypeScript, prettier, { rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, - ], + "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], // Force every call site through the prefixed / schema-scoped factories. // Raw clients bypass the bsk:{env}: Redis prefix and the schema='bsk' // scoping, which collide with sibling apps sharing the same project. @@ -40,29 +47,48 @@ const config = [ ], }, }, + { + files: ["**/*.{js,jsx,mjs,cjs}"], + plugins: { jsdoc }, + rules: { + "jsdoc/require-jsdoc": [ + "warn", + { + publicOnly: true, + require: { + FunctionDeclaration: true, + FunctionExpression: true, + ArrowFunctionExpression: true, + }, + }, + ], + "jsdoc/require-param-type": "warn", + "jsdoc/require-returns-type": "warn", + }, + }, // Only the named factory files may import the raw infrastructure libs. // Explicit filenames (not a glob) keep the trust boundary tight — adding a // new factory should be a deliberate PR change here, not an accidental // file landing under `lib/supabase/*`. { files: [ - "lib/upstash.ts", - "lib/supabase/server.ts", - "lib/supabase/client.ts", - "lib/supabase/admin.ts", - "lib/supabase/session.ts", + "lib/upstash.js", + "lib/supabase/server.js", + "lib/supabase/client.js", + "lib/supabase/admin.js", + "lib/supabase/session.js", ], rules: { "no-restricted-imports": "off" }, }, // Standalone Node scripts (seed/preflight) run outside Next and legitimately // build their own schema-scoped client from env — not the request factories. { - files: ["scripts/**/*.ts", "scripts/**/*.mjs"], + files: ["scripts/**/*.js", "scripts/**/*.mjs"], rules: { "no-restricted-imports": "off" }, }, // Test files (unit/e2e) and config files are exempt from Next.js rules. { - files: ["tests/**/*.ts", "*.config.ts", "*.config.mjs"], + files: ["tests/**/*.js", "*.config.js", "*.config.mjs"], rules: { "@next/next/no-html-link-for-pages": "off", }, diff --git a/i18n/navigation.ts b/i18n/navigation.js similarity index 100% rename from i18n/navigation.ts rename to i18n/navigation.js diff --git a/i18n/request.ts b/i18n/request.js similarity index 100% rename from i18n/request.ts rename to i18n/request.js diff --git a/i18n/routing.ts b/i18n/routing.js similarity index 100% rename from i18n/routing.ts rename to i18n/routing.js diff --git a/tsconfig.json b/jsconfig.json similarity index 67% rename from tsconfig.json rename to jsconfig.json index ae94a37..ed37be2 100644 --- a/tsconfig.json +++ b/jsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": false, + "allowJs": true, + "checkJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, @@ -18,7 +19,6 @@ "name": "next" } ], - "baseUrl": ".", "paths": { "@/*": ["./*"] }, @@ -26,12 +26,15 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" - ], - "exclude": ["node_modules", ".next/cache", ".next/dev/cache"] + "include": ["next-env.d.ts", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs", "**/*.d.ts"], + "exclude": [ + "node_modules", + ".next", + "out", + "dist", + ".vercel", + "coverage", + "playwright-report", + "test-results" + ] } diff --git a/lib/auth/get-server-session.ts b/lib/auth/get-server-session.js similarity index 100% rename from lib/auth/get-server-session.ts rename to lib/auth/get-server-session.js diff --git a/lib/auth/invite-schema.ts b/lib/auth/invite-schema.js similarity index 100% rename from lib/auth/invite-schema.ts rename to lib/auth/invite-schema.js diff --git a/lib/auth/require-role.ts b/lib/auth/require-role.js similarity index 100% rename from lib/auth/require-role.ts rename to lib/auth/require-role.js diff --git a/lib/auth/role-menu.ts b/lib/auth/role-menu.js similarity index 100% rename from lib/auth/role-menu.ts rename to lib/auth/role-menu.js diff --git a/lib/auth/schemas.ts b/lib/auth/schemas.js similarity index 100% rename from lib/auth/schemas.ts rename to lib/auth/schemas.js diff --git a/lib/auth/session-provider.tsx b/lib/auth/session-provider.jsx similarity index 100% rename from lib/auth/session-provider.tsx rename to lib/auth/session-provider.jsx diff --git a/lib/billing/dose-presets.ts b/lib/billing/dose-presets.js similarity index 100% rename from lib/billing/dose-presets.ts rename to lib/billing/dose-presets.js diff --git a/lib/billing/prescription-schema.ts b/lib/billing/prescription-schema.js similarity index 100% rename from lib/billing/prescription-schema.ts rename to lib/billing/prescription-schema.js diff --git a/lib/billing/totals.ts b/lib/billing/totals.js similarity index 100% rename from lib/billing/totals.ts rename to lib/billing/totals.js diff --git a/lib/catalog/medicine-schema.ts b/lib/catalog/medicine-schema.js similarity index 100% rename from lib/catalog/medicine-schema.ts rename to lib/catalog/medicine-schema.js diff --git a/lib/catalog/service-schema.ts b/lib/catalog/service-schema.js similarity index 100% rename from lib/catalog/service-schema.ts rename to lib/catalog/service-schema.js diff --git a/lib/checkups/checkup-schema.ts b/lib/checkups/checkup-schema.js similarity index 100% rename from lib/checkups/checkup-schema.ts rename to lib/checkups/checkup-schema.js diff --git a/lib/clinic/clinic-settings-schema.ts b/lib/clinic/clinic-settings-schema.js similarity index 100% rename from lib/clinic/clinic-settings-schema.ts rename to lib/clinic/clinic-settings-schema.js diff --git a/lib/customers/customer-schema.ts b/lib/customers/customer-schema.js similarity index 100% rename from lib/customers/customer-schema.ts rename to lib/customers/customer-schema.js diff --git a/lib/db/roles.ts b/lib/db/roles.js similarity index 100% rename from lib/db/roles.ts rename to lib/db/roles.js diff --git a/lib/doctors/doctor-schema.ts b/lib/doctors/doctor-schema.js similarity index 100% rename from lib/doctors/doctor-schema.ts rename to lib/doctors/doctor-schema.js diff --git a/lib/env/client.ts b/lib/env/client.js similarity index 100% rename from lib/env/client.ts rename to lib/env/client.js diff --git a/lib/env/server.ts b/lib/env/server.js similarity index 100% rename from lib/env/server.ts rename to lib/env/server.js diff --git a/lib/imaging/image-schema.ts b/lib/imaging/image-schema.js similarity index 100% rename from lib/imaging/image-schema.ts rename to lib/imaging/image-schema.js diff --git a/lib/pdf/fonts.ts b/lib/pdf/fonts.js similarity index 100% rename from lib/pdf/fonts.ts rename to lib/pdf/fonts.js diff --git a/lib/pdf/invoice-document.tsx b/lib/pdf/invoice-document.jsx similarity index 100% rename from lib/pdf/invoice-document.tsx rename to lib/pdf/invoice-document.jsx diff --git a/lib/pdf/patient-info.ts b/lib/pdf/patient-info.js similarity index 100% rename from lib/pdf/patient-info.ts rename to lib/pdf/patient-info.js diff --git a/lib/pdf/prescription-document.tsx b/lib/pdf/prescription-document.jsx similarity index 100% rename from lib/pdf/prescription-document.tsx rename to lib/pdf/prescription-document.jsx diff --git a/lib/pdf/ultrasound-document.tsx b/lib/pdf/ultrasound-document.jsx similarity index 100% rename from lib/pdf/ultrasound-document.tsx rename to lib/pdf/ultrasound-document.jsx diff --git a/lib/proxy/copy-cookies.ts b/lib/proxy/copy-cookies.js similarity index 100% rename from lib/proxy/copy-cookies.ts rename to lib/proxy/copy-cookies.js diff --git a/lib/supabase/admin.ts b/lib/supabase/admin.js similarity index 100% rename from lib/supabase/admin.ts rename to lib/supabase/admin.js diff --git a/lib/supabase/client.ts b/lib/supabase/client.js similarity index 100% rename from lib/supabase/client.ts rename to lib/supabase/client.js diff --git a/lib/supabase/server.ts b/lib/supabase/server.js similarity index 100% rename from lib/supabase/server.ts rename to lib/supabase/server.js diff --git a/lib/supabase/session.ts b/lib/supabase/session.js similarity index 100% rename from lib/supabase/session.ts rename to lib/supabase/session.js diff --git a/lib/templates/template-schema.ts b/lib/templates/template-schema.js similarity index 100% rename from lib/templates/template-schema.ts rename to lib/templates/template-schema.js diff --git a/lib/upstash.ts b/lib/upstash.js similarity index 100% rename from lib/upstash.ts rename to lib/upstash.js diff --git a/lib/utils.ts b/lib/utils.js similarity index 100% rename from lib/utils.ts rename to lib/utils.js diff --git a/next.config.ts b/next.config.mjs similarity index 51% rename from next.config.ts rename to next.config.mjs index b0eb03c..9423db0 100644 --- a/next.config.ts +++ b/next.config.mjs @@ -1,9 +1,9 @@ -import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; -const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); +const withNextIntl = createNextIntlPlugin("./i18n/request.js"); -const nextConfig: NextConfig = { +/** @type {import('next').NextConfig} */ +const nextConfig = { reactStrictMode: true, typedRoutes: true, }; diff --git a/package-lock.json b/package-lock.json index cc622b1..1f24f5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,11 +46,11 @@ "eslint": "^9.36.0", "eslint-config-next": "^16.2.6", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsdoc": "^64.2.0", "postcss": "^8.5.18", "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.8.0", "tailwindcss": "^4.3.0", - "tsx": "^4.22.3", "typescript": "^5.9.2", "vitest": "^4.1.10" }, @@ -456,446 +456,31 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@es-joy/jsdoccomment": { + "version": "0.95.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.95.0.tgz", + "integrity": "sha512-jbzwtRPuw1Nzld0lacvr1vwzPU/6zEHFGK5YOTstl2MQYMZBuKmSW3HMuEwsq1v4meK5Do4lizxyd/hi25rCZg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@types/estree": "^1.0.9", + "@typescript-eslint/types": "^8.67.0", + "comment-parser": "1.4.8", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~9.1.1" + }, "engines": { - "node": ">=18" + "node": "^22.22.2 || >=24.15.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node": ">=10" } }, "node_modules/@eslint-community/eslint-utils": { @@ -4490,6 +4075,19 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -6225,23 +5823,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6258,6 +5839,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.1.1.tgz", + "integrity": "sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6830,6 +6421,16 @@ "node": ">=18" } }, + "node_modules/comment-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.8.tgz", + "integrity": "sha512-rKZTGo4fzKYna8UcL0isTg5wkBNla7bxTypLwZQXjIdi++IdP1OJ41rI5Mti3/jltkPujbu4i9LIARYA+zpotQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7465,48 +7066,6 @@ "tests/browser-compat" ] }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7775,6 +7334,92 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "64.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.2.0.tgz", + "integrity": "sha512-z3zGmJoPhOdKnxzQ3R+8MZeJjW8vrRW8r7sYp/ErBp8K9+05RIa6vWXtbEGr7D1obBHk64LdKyLHoMLSzHFINA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.95.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.1.1", + "comment-parser": "1.4.8", + "debug": "^4.4.3", + "escape-string-regexp": "^5.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.1", + "parse-imports-exports": "^0.2.4", + "semver": "^7.8.5", + "spdx-expression-parse": "^5.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", @@ -8061,23 +7706,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", - "extraneous": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -8537,6 +8165,23 @@ "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", "license": "ISC" }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9242,6 +8887,19 @@ "js-yaml": "bin/js-yaml.mjs" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-9.1.1.tgz", + "integrity": "sha512-kojSbQb9iQM2bk2PmHiKa3reG0U4v2gN9U8D8+eCQq+4KM5ZXBUyBVeF8KmR0RiwqyrW4+uVWYWTOLnpsavnkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9262,13 +8920,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "extraneous": true, - "license": "MIT" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10068,6 +9719,13 @@ "node": ">=0.10.0" } }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10283,6 +9941,23 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/parse-svg-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", @@ -10917,6 +10592,19 @@ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -11369,6 +11057,31 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-5.0.0.tgz", + "integrity": "sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -11736,6 +11449,23 @@ "node": ">=8.0" } }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -11781,40 +11511,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 4ec602d..088c9c9 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,17 @@ "build": "next build", "start": "next start", "lint": "eslint .", - "typecheck": "tsc --noEmit", + "typecheck": "tsc -p jsconfig.json", "format": "prettier --write .", "format:check": "prettier --check .", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", - "db:preflight": "tsx scripts/preflight-supabase.ts", + "db:preflight": "node scripts/preflight-supabase.mjs", "db:push": "npm run db:preflight && supabase db push", - "db:gen-types": "supabase gen types typescript --schema bsk > types/supabase-bsk.ts", - "db:seed-geo": "tsx scripts/seed-geo.ts", + "db:gen-types": "supabase gen types typescript --schema bsk > types/supabase-bsk.d.ts", + "db:seed-geo": "node scripts/seed-geo.mjs", "check:no-secret-leak": "node scripts/check-no-secret-leak.mjs" }, "dependencies": { @@ -62,11 +62,11 @@ "eslint": "^9.36.0", "eslint-config-next": "^16.2.6", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsdoc": "^64.2.0", "postcss": "^8.5.18", "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.8.0", "tailwindcss": "^4.3.0", - "tsx": "^4.22.3", "typescript": "^5.9.2", "vitest": "^4.1.10" }, diff --git a/playwright.config.ts b/playwright.config.js similarity index 100% rename from playwright.config.ts rename to playwright.config.js diff --git a/proxy.ts b/proxy.js similarity index 100% rename from proxy.ts rename to proxy.js diff --git a/scripts/preflight-supabase.ts b/scripts/preflight-supabase.mjs similarity index 86% rename from scripts/preflight-supabase.ts rename to scripts/preflight-supabase.mjs index f7e8b48..dce01c3 100644 --- a/scripts/preflight-supabase.ts +++ b/scripts/preflight-supabase.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node /** * Preflight guard for `supabase db push` against the shared project. * @@ -9,7 +9,7 @@ * currently-linked Supabase project ref matches BSK's allow-list. * * Wire it in package.json: - * "db:push": "tsx scripts/preflight-supabase.ts && supabase db push" + * "db:push": "node scripts/preflight-supabase.mjs && supabase db push" * * The allow-list is intentionally checked into the repo: it's not secret, * and a PR diff is the right place to notice "wait, why did the ref change?" @@ -20,11 +20,13 @@ import { resolve } from "node:path"; // BSK is only ever linked to these project refs. Add new ones via PR. // Source of truth: Supabase dashboard → Project Settings → General → Reference ID. -const ALLOWED_PROJECT_REFS: ReadonlyArray = [ +/** @type {ReadonlyArray} */ +const ALLOWED_PROJECT_REFS = [ // "abcdefghijklmnopqrst", // example: tiennm99's personal shared project ]; -function readLinkedRef(): string | null { +/** @returns {string | null} */ +function readLinkedRef() { // `supabase link` writes the project ref to supabase/.temp/project-ref. try { const path = resolve(process.cwd(), "supabase/.temp/project-ref"); @@ -34,7 +36,11 @@ function readLinkedRef(): string | null { } } -function die(msg: string): never { +/** + * @param {string} msg + * @returns {never} + */ +function die(msg) { process.stderr.write(`\n[preflight-supabase] ${msg}\n\n`); process.exit(1); } @@ -50,7 +56,7 @@ if (!ref) { if (ALLOWED_PROJECT_REFS.length === 0) { die( - "ALLOWED_PROJECT_REFS is empty in scripts/preflight-supabase.ts.\n" + + "ALLOWED_PROJECT_REFS is empty in scripts/preflight-supabase.mjs.\n" + "Add your BSK Supabase project ref(s) to the allow-list before pushing migrations.", ); } @@ -63,7 +69,7 @@ if (!ALLOWED_PROJECT_REFS.includes(ref)) { `Pushing to the wrong project can require a project-wide PITR to recover —\n` + `which would wipe every other app's data too.\n\n` + `If "${ref}" is legitimately a new BSK project, add it to ALLOWED_PROJECT_REFS\n` + - `in scripts/preflight-supabase.ts via a PR, then re-run.`, + `in scripts/preflight-supabase.mjs via a PR, then re-run.`, ); } diff --git a/scripts/seed-geo.ts b/scripts/seed-geo.mjs similarity index 83% rename from scripts/seed-geo.ts rename to scripts/seed-geo.mjs index 970d415..37de440 100644 --- a/scripts/seed-geo.ts +++ b/scripts/seed-geo.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node /** * Seed Vietnamese geo lookup (bsk.provinces + bsk.wards). * @@ -23,10 +23,14 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { createClient } from "@supabase/supabase-js"; -type Ward = { code: string; name: string }; -type Province = { code: string; name: string; wards: Ward[] }; +/** @typedef {{ code: string, name: string }} Ward */ +/** @typedef {{ code: string, name: string, wards: Ward[] }} Province */ -function die(msg: string): never { +/** + * @param {string} msg + * @returns {never} + */ +function die(msg) { process.stderr.write(`\n[seed-geo] ${msg}\n\n`); process.exit(1); } @@ -38,7 +42,8 @@ if (!url || !key) { } const dataPath = resolve(process.cwd(), "supabase/seed/vn-geo.json"); -let provinces: Province[]; +/** @type {Province[]} */ +let provinces; try { provinces = JSON.parse(readFileSync(dataPath, "utf8")); } catch { @@ -54,14 +59,17 @@ if (!Array.isArray(provinces) || provinces.length === 0) { const supabase = createClient(url, key, { db: { schema: "bsk" }, auth: { persistSession: false } }); -async function upsertBatched( - table: "provinces" | "wards", - rows: T[], - size = 500, -): Promise { +/** + * @template T + * @param {"provinces" | "wards"} table + * @param {T[]} rows + * @param {number} [size] + * @returns {Promise} + */ +async function upsertBatched(table, rows, size = 500) { for (let i = 0; i < rows.length; i += size) { const batch = rows.slice(i, i + size); - const { error } = await supabase.from(table).upsert(batch as never[]); + const { error } = await supabase.from(table).upsert(/** @type {never[]} */ (batch)); if (error) die(`Upsert into ${table} failed at row ${i}: ${error.message}`); process.stdout.write( `[seed-geo] ${table}: ${Math.min(i + size, rows.length)}/${rows.length}\n`, diff --git a/tests/e2e/auth-gate.spec.ts b/tests/e2e/auth-gate.spec.js similarity index 100% rename from tests/e2e/auth-gate.spec.ts rename to tests/e2e/auth-gate.spec.js diff --git a/tests/e2e/i18n-and-404.spec.ts b/tests/e2e/i18n-and-404.spec.js similarity index 100% rename from tests/e2e/i18n-and-404.spec.ts rename to tests/e2e/i18n-and-404.spec.js diff --git a/tests/e2e/sign-in-page.spec.ts b/tests/e2e/sign-in-page.spec.js similarity index 100% rename from tests/e2e/sign-in-page.spec.ts rename to tests/e2e/sign-in-page.spec.js diff --git a/tests/unit/checkup-schema.test.ts b/tests/unit/checkup-schema.test.js similarity index 100% rename from tests/unit/checkup-schema.test.ts rename to tests/unit/checkup-schema.test.js diff --git a/tests/unit/customer-schema.test.ts b/tests/unit/customer-schema.test.js similarity index 100% rename from tests/unit/customer-schema.test.ts rename to tests/unit/customer-schema.test.js diff --git a/tests/unit/medicine-schema.test.ts b/tests/unit/medicine-schema.test.js similarity index 100% rename from tests/unit/medicine-schema.test.ts rename to tests/unit/medicine-schema.test.js diff --git a/tests/unit/mocks/server-only.ts b/tests/unit/mocks/server-only.js similarity index 100% rename from tests/unit/mocks/server-only.ts rename to tests/unit/mocks/server-only.js diff --git a/tests/unit/patient-info.test.ts b/tests/unit/patient-info.test.js similarity index 100% rename from tests/unit/patient-info.test.ts rename to tests/unit/patient-info.test.js diff --git a/tests/unit/service-schema.test.ts b/tests/unit/service-schema.test.js similarity index 100% rename from tests/unit/service-schema.test.ts rename to tests/unit/service-schema.test.js diff --git a/tests/unit/template-schema.test.ts b/tests/unit/template-schema.test.js similarity index 100% rename from tests/unit/template-schema.test.ts rename to tests/unit/template-schema.test.js diff --git a/tests/unit/totals.test.ts b/tests/unit/totals.test.js similarity index 100% rename from tests/unit/totals.test.ts rename to tests/unit/totals.test.js diff --git a/types/supabase-bsk.ts b/types/supabase-bsk.d.ts similarity index 100% rename from types/supabase-bsk.ts rename to types/supabase-bsk.d.ts diff --git a/vitest.config.ts b/vitest.config.js similarity index 71% rename from vitest.config.ts rename to vitest.config.js index 207e296..bd2b32d 100644 --- a/vitest.config.ts +++ b/vitest.config.js @@ -4,18 +4,18 @@ import path from "path"; export default defineConfig({ test: { environment: "node", - include: ["tests/unit/**/*.test.ts"], + include: ["tests/unit/**/*.test.js"], coverage: { provider: "v8", reporter: ["text", "json", "html"], - include: ["lib/**/*.ts"], - exclude: ["lib/**/*.d.ts", "lib/**/*.config.ts"], + include: ["lib/**/*.js"], + exclude: ["lib/**/*.d.ts", "lib/**/*.config.js"], }, }, resolve: { alias: { "@": path.resolve(__dirname, "."), - "server-only": path.resolve(__dirname, "tests/unit/mocks/server-only.ts"), + "server-only": path.resolve(__dirname, "tests/unit/mocks/server-only.js"), }, }, }); From b47d8fc3d1342c7e76d7fbd3f36f27134e88ae1b Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 17 Aug 2026 23:12:26 +0700 Subject: [PATCH 2/3] refactor: convert TypeScript sources to JavaScript with JSDoc types - app/, components/, lib/, i18n/, tests/, proxy renamed via git mv - types carried into JSDoc: @template generics, @typedef aliases, /** @type */ casts where TS had as-casts or non-null assertions - role tuple keeps its const assertion; the db-enum drift guard now checks the tuple values against the generated enum type - use client/use server directives and server-only imports preserved - prettier-formatted; vitest suite unchanged --- app/[locale]/(app)/admin/doctors/actions.js | 28 ++-- .../(app)/admin/doctors/add-doctor-form.jsx | 15 ++- app/[locale]/(app)/admin/doctors/page.jsx | 6 +- app/[locale]/(app)/admin/invite/actions.js | 18 ++- .../(app)/admin/invite/invite-user-form.jsx | 21 ++- app/[locale]/(app)/admin/invite/page.jsx | 1 + app/[locale]/(app)/admin/layout.jsx | 13 +- .../(app)/admin/medicines/[id]/edit/page.jsx | 10 +- app/[locale]/(app)/admin/medicines/actions.js | 51 +++++--- .../(app)/admin/medicines/medicine-form.jsx | 48 ++++--- .../(app)/admin/medicines/new/page.jsx | 6 +- app/[locale]/(app)/admin/medicines/page.jsx | 16 ++- app/[locale]/(app)/admin/reports/page.jsx | 6 +- app/[locale]/(app)/admin/services/actions.js | 28 ++-- .../(app)/admin/services/add-service-form.jsx | 15 +-- app/[locale]/(app)/admin/services/page.jsx | 6 +- app/[locale]/(app)/admin/settings/actions.js | 22 ++-- .../admin/settings/clinic-settings-form.jsx | 23 ++-- app/[locale]/(app)/admin/settings/page.jsx | 10 +- app/[locale]/(app)/admin/staff/actions.js | 12 +- app/[locale]/(app)/admin/staff/page.jsx | 6 +- .../(app)/admin/templates/[id]/edit/page.jsx | 10 +- app/[locale]/(app)/admin/templates/actions.js | 52 +++++--- .../(app)/admin/templates/new/page.jsx | 6 +- app/[locale]/(app)/admin/templates/page.jsx | 13 +- .../(app)/admin/templates/template-form.jsx | 43 +++--- .../(app)/checkups/[id]/imaging/actions.js | 33 +++-- .../checkups/[id]/imaging/checkup-barcode.jsx | 7 +- .../checkups/[id]/imaging/image-capture.jsx | 52 +++++--- .../checkups/[id]/imaging/image-gallery.jsx | 14 +- .../(app)/checkups/[id]/imaging/page.jsx | 10 +- .../(app)/checkups/[id]/invoice/route.js | 19 ++- app/[locale]/(app)/checkups/[id]/page.jsx | 31 +++-- .../checkups/[id]/prescription/actions.js | 50 ++++--- .../(app)/checkups/[id]/prescription/page.jsx | 10 +- .../checkups/[id]/prescription/pdf/route.js | 19 +-- .../prescription/prescription-composer.jsx | 123 ++++++++++-------- .../(app)/checkups/[id]/report/route.js | 49 ++++--- app/[locale]/(app)/checkups/actions.js | 41 +++--- app/[locale]/(app)/checkups/checkup-form.jsx | 94 +++++++------ .../(app)/checkups/delete-checkup-button.jsx | 9 +- app/[locale]/(app)/checkups/layout.jsx | 13 +- app/[locale]/(app)/dashboard/page.jsx | 12 +- .../(app)/dashboard/revenue-chart.jsx | 3 +- app/[locale]/(app)/layout.jsx | 13 +- app/[locale]/(app)/loading.jsx | 1 + .../(app)/patients/[id]/edit/page.jsx | 10 +- app/[locale]/(app)/patients/[id]/page.jsx | 19 +-- app/[locale]/(app)/patients/actions.js | 67 ++++++---- app/[locale]/(app)/patients/layout.jsx | 13 +- app/[locale]/(app)/patients/new/page.jsx | 6 +- app/[locale]/(app)/patients/page.jsx | 44 +++---- app/[locale]/(app)/patients/patient-form.jsx | 63 ++++----- app/[locale]/(app)/queue/actions.js | 58 +++++---- app/[locale]/(app)/queue/call-next-button.jsx | 22 ++-- app/[locale]/(app)/queue/counter-form.jsx | 13 +- app/[locale]/(app)/queue/layout.jsx | 13 +- app/[locale]/(app)/queue/page.jsx | 19 ++- app/[locale]/(app)/queue/queue-realtime.jsx | 7 +- app/[locale]/(app)/queue/register-form.jsx | 29 ++--- app/[locale]/(app)/reminders/layout.jsx | 13 +- app/[locale]/(app)/reminders/page.jsx | 6 +- app/[locale]/(app)/reports/catalog/route.js | 3 +- app/[locale]/(app)/reports/patients/route.js | 19 +-- app/[locale]/(app)/reports/revenue/route.js | 41 ++++-- app/[locale]/(app)/reports/visits/route.js | 29 +++-- app/[locale]/(auth)/layout.jsx | 7 +- app/[locale]/(auth)/sign-in/actions.js | 19 ++- app/[locale]/(auth)/sign-in/page.jsx | 10 +- app/[locale]/(auth)/sign-in/sign-in-form.jsx | 10 +- app/[locale]/error.jsx | 5 +- app/[locale]/layout.jsx | 21 ++- app/[locale]/not-found.jsx | 3 + app/[locale]/page.jsx | 6 +- app/api/cron/nightly/route.js | 5 +- app/layout.jsx | 7 +- app/not-found.jsx | 1 + components/app-shell/app-shell-frame.jsx | 31 +++-- components/app-shell/app-shell.jsx | 20 +-- components/app-shell/locale-switcher.jsx | 6 +- components/app-shell/sidebar-nav.jsx | 17 +-- components/app-shell/sidebar.jsx | 17 +-- components/app-shell/sign-out-button.jsx | 1 + components/ui/badge.jsx | 15 +-- components/ui/button.jsx | 19 +-- components/ui/input.jsx | 5 +- components/ui/label.jsx | 4 +- components/ui/separator.jsx | 9 +- components/ui/sonner.jsx | 11 +- i18n/routing.js | 4 +- lib/auth/get-server-session.js | 35 +++-- lib/auth/invite-schema.js | 32 ++--- lib/auth/require-role.js | 17 ++- lib/auth/role-menu.js | 21 ++- lib/auth/schemas.js | 32 ++--- lib/auth/session-provider.jsx | 14 +- lib/billing/dose-presets.js | 3 +- lib/billing/prescription-schema.js | 20 ++- lib/billing/totals.js | 17 ++- lib/catalog/medicine-schema.js | 11 +- lib/catalog/service-schema.js | 13 +- lib/checkups/checkup-schema.js | 58 ++++++--- lib/clinic/clinic-settings-schema.js | 13 +- lib/customers/customer-schema.js | 17 ++- lib/db/roles.js | 24 ++-- lib/doctors/doctor-schema.js | 15 ++- lib/env/client.js | 6 +- lib/env/server.js | 6 +- lib/imaging/image-schema.js | 24 +++- lib/pdf/fonts.js | 3 +- lib/pdf/invoice-document.jsx | 90 +++++++------ lib/pdf/patient-info.js | 8 +- lib/pdf/prescription-document.jsx | 94 ++++++------- lib/pdf/ultrasound-document.jsx | 70 +++++----- lib/proxy/copy-cookies.js | 8 +- lib/supabase/client.js | 1 + lib/supabase/session.js | 16 ++- lib/templates/template-schema.js | 43 ++++-- lib/upstash.js | 55 ++++++-- lib/utils.js | 5 +- proxy.js | 14 +- tests/unit/checkup-schema.test.js | 8 +- tests/unit/customer-schema.test.js | 2 +- 123 files changed, 1550 insertions(+), 1080 deletions(-) diff --git a/app/[locale]/(app)/admin/doctors/actions.js b/app/[locale]/(app)/admin/doctors/actions.js index f86a571..956e200 100644 --- a/app/[locale]/(app)/admin/doctors/actions.js +++ b/app/[locale]/(app)/admin/doctors/actions.js @@ -15,7 +15,9 @@ import { revalidatePath } from "next/cache"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { DoctorSchema, type DoctorFormState } from "@/lib/doctors/doctor-schema"; +import { DoctorSchema } from "@/lib/doctors/doctor-schema"; + +/** @typedef {import('@/lib/doctors/doctor-schema').DoctorFormState} DoctorFormState */ async function revalidateDoctors() { const locale = await getLocale(); @@ -23,10 +25,12 @@ async function revalidateDoctors() { } // ── Create (useActionState-compatible) ─────────────────────────────────────── -export async function createDoctorAction( - _prev: DoctorFormState, - formData: FormData, -): Promise { +/** + * @param {DoctorFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function createDoctorAction(_prev, formData) { const t = await getTranslations("admin.doctors"); const session = await getServerSession(); @@ -41,7 +45,7 @@ export async function createDoctorAction( if (!parsed.success) { return { status: "error", - fieldErrors: parsed.error.flatten().fieldErrors as Record, + fieldErrors: /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: null, }; } @@ -70,7 +74,11 @@ export async function createDoctorAction( } // ── Update ─────────────────────────────────────────────────────────────────── -export async function updateDoctorAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateDoctorAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; @@ -98,7 +106,11 @@ export async function updateDoctorAction(formData: FormData): Promise { } // ── Soft-delete (deactivate) ───────────────────────────────────────────────── -export async function deactivateDoctorAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function deactivateDoctorAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; diff --git a/app/[locale]/(app)/admin/doctors/add-doctor-form.jsx b/app/[locale]/(app)/admin/doctors/add-doctor-form.jsx index d551a53..e39aceb 100644 --- a/app/[locale]/(app)/admin/doctors/add-doctor-form.jsx +++ b/app/[locale]/(app)/admin/doctors/add-doctor-form.jsx @@ -14,18 +14,21 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { DoctorSchema, type DoctorInput, type DoctorFormState } from "@/lib/doctors/doctor-schema"; +import { DoctorSchema } from "@/lib/doctors/doctor-schema"; import { createDoctorAction } from "./actions"; +/** @typedef {import('@/lib/doctors/doctor-schema').DoctorInput} DoctorInput */ +/** @typedef {import('@/lib/doctors/doctor-schema').DoctorFormState} DoctorFormState */ + +/** Admin form to add a doctor row. @returns {import("react").JSX.Element} */ export function AddDoctorForm() { const t = useTranslations("admin.doctors"); - const [state, dispatchAction, isPending] = useActionState( - createDoctorAction, - { status: "idle" }, - ); + const [state, dispatchAction, isPending] = useActionState(createDoctorAction, { + status: "idle", + }); - const form = useForm({ + const form = useForm({ resolver: zodResolver(DoctorSchema), mode: "onBlur", defaultValues: { firstName: "", lastName: "" }, diff --git a/app/[locale]/(app)/admin/doctors/page.jsx b/app/[locale]/(app)/admin/doctors/page.jsx index 7499438..317308e 100644 --- a/app/[locale]/(app)/admin/doctors/page.jsx +++ b/app/[locale]/(app)/admin/doctors/page.jsx @@ -14,7 +14,11 @@ import { Input } from "@/components/ui/input"; import { AddDoctorForm } from "./add-doctor-form"; import { updateDoctorAction, deactivateDoctorAction } from "./actions"; -export default async function DoctorsPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function DoctorsPage({ params }) { await params; // Next.js 16: params is async. const t = await getTranslations("admin.doctors"); diff --git a/app/[locale]/(app)/admin/invite/actions.js b/app/[locale]/(app)/admin/invite/actions.js index c1f2ed8..e23c3e9 100644 --- a/app/[locale]/(app)/admin/invite/actions.js +++ b/app/[locale]/(app)/admin/invite/actions.js @@ -24,15 +24,19 @@ import { getTranslations } from "next-intl/server"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseAdminClient } from "@/lib/supabase/admin"; import { createRateLimiter } from "@/lib/upstash"; -import { InviteUserSchema, type InviteUserState } from "@/lib/auth/invite-schema"; +import { InviteUserSchema } from "@/lib/auth/invite-schema"; + +/** @typedef {import('@/lib/auth/invite-schema').InviteUserState} InviteUserState */ // Bound SMTP spend on the SHARED project: cap invites per admin. 20 / hour. const inviteLimiter = createRateLimiter("invite", 20, 3600); -export async function inviteUserAction( - _prevState: InviteUserState, - formData: FormData, -): Promise { +/** + * @param {InviteUserState} _prevState + * @param {FormData} formData + * @returns {Promise} + */ +export async function inviteUserAction(_prevState, formData) { const t = await getTranslations("admin.invite"); // ── Caller-role check (defense-in-depth) ────────────────────────────────── @@ -59,7 +63,7 @@ export async function inviteUserAction( const flat = parsed.error.flatten(); return { status: "error", - fieldErrors: flat.fieldErrors as Record, + fieldErrors: /** @type {Record} */ (flat.fieldErrors), formError: null, }; } @@ -75,7 +79,7 @@ export async function inviteUserAction( // An existing email (often a sibling-app user on the shared auth pool) // makes inviteUserByEmail fail — report it as "already enrolled/known" // rather than a generic error so the admin understands what happened. - const code = (inviteError as { code?: string } | null)?.code; + const code = /** @type {{ code?: string } | null} */ (inviteError)?.code; const msg = inviteError?.message?.toLowerCase() ?? ""; const emailExists = code === "email_exists" || diff --git a/app/[locale]/(app)/admin/invite/invite-user-form.jsx b/app/[locale]/(app)/admin/invite/invite-user-form.jsx index 820052f..59d0c5d 100644 --- a/app/[locale]/(app)/admin/invite/invite-user-form.jsx +++ b/app/[locale]/(app)/admin/invite/invite-user-form.jsx @@ -23,30 +23,29 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { appRoles } from "@/lib/db/roles"; -import { - InviteUserSchema, - type InviteUserInput, - type InviteUserState, -} from "@/lib/auth/invite-schema"; +import { InviteUserSchema } from "@/lib/auth/invite-schema"; import { inviteUserAction } from "./actions"; +/** @typedef {import('@/lib/auth/invite-schema').InviteUserInput} InviteUserInput */ +/** @typedef {import('@/lib/auth/invite-schema').InviteUserState} InviteUserState */ + // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- +/** Admin form that invites a user by email with a role. @returns {import("react").JSX.Element} */ export function InviteUserForm() { const t = useTranslations("admin.invite"); const tRoles = useTranslations("roles"); - const [state, dispatchAction, isPending] = useActionState( - inviteUserAction, - { status: "idle" }, - ); + const [state, dispatchAction, isPending] = useActionState(inviteUserAction, { + status: "idle", + }); - const form = useForm({ + const form = useForm({ resolver: zodResolver(InviteUserSchema), mode: "onBlur", - defaultValues: { email: "", role: "patient" }, + defaultValues: { email: "", role: /** @type {InviteUserInput["role"]} */ ("patient") }, }); const { errors: fieldErrors } = form.formState; diff --git a/app/[locale]/(app)/admin/invite/page.jsx b/app/[locale]/(app)/admin/invite/page.jsx index 9563921..2cc0040 100644 --- a/app/[locale]/(app)/admin/invite/page.jsx +++ b/app/[locale]/(app)/admin/invite/page.jsx @@ -12,6 +12,7 @@ import { getTranslations } from "next-intl/server"; import { InviteUserForm } from "./invite-user-form"; +/** @returns {Promise} */ export default async function AdminInvitePage() { const t = await getTranslations("admin.invite"); diff --git a/app/[locale]/(app)/admin/layout.jsx b/app/[locale]/(app)/admin/layout.jsx index e326549..7d36713 100644 --- a/app/[locale]/(app)/admin/layout.jsx +++ b/app/[locale]/(app)/admin/layout.jsx @@ -11,16 +11,13 @@ * receiving a 404, which would confirm that restricted admin routes exist. */ -import type { ReactNode } from "react"; import { requireRole } from "@/lib/auth/require-role"; -export default async function AdminLayout({ - children, - params, -}: { - children: ReactNode; - params: Promise<{ locale: string }>; -}) { +/** + * @param {{ children: import("react").ReactNode, params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function AdminLayout({ children, params }) { const { locale } = await params; // Redirects to /dashboard if role !== 'admin'. diff --git a/app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx b/app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx index 894c2f6..e3372d0 100644 --- a/app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx +++ b/app/[locale]/(app)/admin/medicines/[id]/edit/page.jsx @@ -4,11 +4,11 @@ import { createSupabaseServerClient } from "@/lib/supabase/server"; import { MedicineForm } from "../../medicine-form"; import { updateMedicineAction } from "../../actions"; -export default async function EditMedicinePage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string, id: string }> }} props + * @returns {Promise} + */ +export default async function EditMedicinePage({ params }) { const { id } = await params; const t = await getTranslations("admin.medicines"); const medId = Number(id); diff --git a/app/[locale]/(app)/admin/medicines/actions.js b/app/[locale]/(app)/admin/medicines/actions.js index 3a5fa77..2063405 100644 --- a/app/[locale]/(app)/admin/medicines/actions.js +++ b/app/[locale]/(app)/admin/medicines/actions.js @@ -8,14 +8,15 @@ import { revalidatePath } from "next/cache"; import { redirect } from "@/i18n/navigation"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { - MedicineSchema, - type MedicineFormState, - type MedicineInput, -} from "@/lib/catalog/medicine-schema"; +import { MedicineSchema } from "@/lib/catalog/medicine-schema"; -function readForm(formData: FormData) { - const get = (k: string) => String(formData.get(k) ?? ""); +/** @typedef {import('@/lib/catalog/medicine-schema').MedicineFormState} MedicineFormState */ +/** @typedef {import('@/lib/catalog/medicine-schema').MedicineInput} MedicineInput */ + +/** @param {FormData} formData */ +function readForm(formData) { + /** @param {string} k */ + const get = (k) => String(formData.get(k) ?? ""); return { name: get("name"), unit: get("unit"), @@ -26,7 +27,8 @@ function readForm(formData: FormData) { }; } -function toRow(d: MedicineInput) { +/** @param {MedicineInput} d */ +function toRow(d) { const cost = d.costPrice.trim() ? Number(d.costPrice) : null; return { name: d.name, @@ -38,16 +40,19 @@ function toRow(d: MedicineInput) { }; } -async function finish(): Promise { +/** @returns {Promise} */ +async function finish() { const locale = await getLocale(); revalidatePath(`/${locale}/admin/medicines`); return redirect({ href: `/${locale}/admin/medicines`, locale }); } -export async function createMedicineAction( - _prev: MedicineFormState, - formData: FormData, -): Promise { +/** + * @param {MedicineFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function createMedicineAction(_prev, formData) { const t = await getTranslations("admin.medicines"); const session = await getServerSession(); if (session?.role !== "admin") @@ -57,7 +62,7 @@ export async function createMedicineAction( if (!parsed.success) { return { status: "error", - fieldErrors: parsed.error.flatten().fieldErrors as Record, + fieldErrors: /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: null, }; } @@ -78,10 +83,12 @@ export async function createMedicineAction( return finish(); } -export async function updateMedicineAction( - _prev: MedicineFormState, - formData: FormData, -): Promise { +/** + * @param {MedicineFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateMedicineAction(_prev, formData) { const t = await getTranslations("admin.medicines"); const session = await getServerSession(); if (session?.role !== "admin") @@ -94,7 +101,7 @@ export async function updateMedicineAction( status: "error", fieldErrors: parsed.success ? {} - : (parsed.error.flatten().fieldErrors as Record), + : /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: parsed.success ? t("errorGeneric") : null, }; } @@ -111,7 +118,11 @@ export async function updateMedicineAction( return finish(); } -export async function deactivateMedicineAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function deactivateMedicineAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; diff --git a/app/[locale]/(app)/admin/medicines/medicine-form.jsx b/app/[locale]/(app)/admin/medicines/medicine-form.jsx index 87fc0fe..0ff25f7 100644 --- a/app/[locale]/(app)/admin/medicines/medicine-form.jsx +++ b/app/[locale]/(app)/admin/medicines/medicine-form.jsx @@ -8,35 +8,41 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import type { MedicineFormState } from "@/lib/catalog/medicine-schema"; -export type MedicineDefaults = { - id?: number; - name: string; - unit: string; - salePrice: string; - costPrice: string; - company: string; - route: string; -}; +/** @typedef {import('@/lib/catalog/medicine-schema').MedicineFormState} MedicineFormState */ -export function MedicineForm({ - mode, - action, - defaults, -}: { - mode: "create" | "edit"; - action: (prev: MedicineFormState, formData: FormData) => Promise; - defaults: MedicineDefaults; -}) { +/** + * @typedef {Object} MedicineDefaults + * @property {number} [id] + * @property {string} name + * @property {string} unit + * @property {string} salePrice + * @property {string} costPrice + * @property {string} company + * @property {string} route + */ + +/** + * @param {{ + * mode: "create" | "edit", + * action: (prev: MedicineFormState, formData: FormData) => Promise, + * defaults: MedicineDefaults, + * }} props + */ +export function MedicineForm({ mode, action, defaults }) { const t = useTranslations("admin.medicines"); - const [state, dispatch, isPending] = useActionState(action, { + const [state, dispatch, isPending] = useActionState(action, { status: "idle", }); const fe = state.status === "error" ? state.fieldErrors : {}; const formError = state.status === "error" ? state.formError : null; - const field = (name: keyof MedicineDefaults, label: string, type = "text") => ( + /** + * @param {keyof MedicineDefaults} name + * @param {string} label + * @param {string} [type] + */ + const field = (name, label, type = "text") => (
}) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function NewMedicinePage({ params }) { await params; const t = await getTranslations("admin.medicines"); return ( diff --git a/app/[locale]/(app)/admin/medicines/page.jsx b/app/[locale]/(app)/admin/medicines/page.jsx index 7ad849e..f51bb45 100644 --- a/app/[locale]/(app)/admin/medicines/page.jsx +++ b/app/[locale]/(app)/admin/medicines/page.jsx @@ -6,9 +6,14 @@ import { createSupabaseServerClient } from "@/lib/supabase/server"; import { Button } from "@/components/ui/button"; import { deactivateMedicineAction } from "./actions"; -const vnd = (n: number) => `${new Intl.NumberFormat("vi-VN").format(n)} ₫`; +/** @param {number} n */ +const vnd = (n) => `${new Intl.NumberFormat("vi-VN").format(n)} ₫`; -export default async function MedicinesPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function MedicinesPage({ params }) { const { locale } = await params; const t = await getTranslations("admin.medicines"); @@ -25,7 +30,7 @@ export default async function MedicinesPage({ params }: { params: Promise<{ loca

{t("title")}

@@ -45,7 +50,10 @@ export default async function MedicinesPage({ params }: { params: Promise<{ loca
diff --git a/app/[locale]/(app)/admin/reports/page.jsx b/app/[locale]/(app)/admin/reports/page.jsx index 2456196..9c24d7b 100644 --- a/app/[locale]/(app)/admin/reports/page.jsx +++ b/app/[locale]/(app)/admin/reports/page.jsx @@ -8,7 +8,11 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { routing } from "@/i18n/routing"; -export default async function ReportsPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function ReportsPage({ params }) { const { locale } = await params; const t = await getTranslations("reports"); diff --git a/app/[locale]/(app)/admin/services/actions.js b/app/[locale]/(app)/admin/services/actions.js index d5e26b4..a74e17a 100644 --- a/app/[locale]/(app)/admin/services/actions.js +++ b/app/[locale]/(app)/admin/services/actions.js @@ -7,17 +7,21 @@ import { revalidatePath } from "next/cache"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { ServiceSchema, type ServiceFormState } from "@/lib/catalog/service-schema"; +import { ServiceSchema } from "@/lib/catalog/service-schema"; + +/** @typedef {import('@/lib/catalog/service-schema').ServiceFormState} ServiceFormState */ async function revalidateServices() { const locale = await getLocale(); revalidatePath(`/${locale}/admin/services`); } -export async function createServiceAction( - _prev: ServiceFormState, - formData: FormData, -): Promise { +/** + * @param {ServiceFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function createServiceAction(_prev, formData) { const t = await getTranslations("admin.services"); const session = await getServerSession(); if (session?.role !== "admin") @@ -30,7 +34,7 @@ export async function createServiceAction( if (!parsed.success) { return { status: "error", - fieldErrors: parsed.error.flatten().fieldErrors as Record, + fieldErrors: /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: null, }; } @@ -52,7 +56,11 @@ export async function createServiceAction( return { status: "success", serviceName: parsed.data.name }; } -export async function updateServiceAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateServiceAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; @@ -78,7 +86,11 @@ export async function updateServiceAction(formData: FormData): Promise { } } -export async function deactivateServiceAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function deactivateServiceAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; diff --git a/app/[locale]/(app)/admin/services/add-service-form.jsx b/app/[locale]/(app)/admin/services/add-service-form.jsx index 417e4a4..dfd467d 100644 --- a/app/[locale]/(app)/admin/services/add-service-form.jsx +++ b/app/[locale]/(app)/admin/services/add-service-form.jsx @@ -12,18 +12,17 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import type { ServiceFormState } from "@/lib/catalog/service-schema"; import { createServiceAction } from "./actions"; +/** @typedef {import('@/lib/catalog/service-schema').ServiceFormState} ServiceFormState */ + +/** Admin form to add a service row. @returns {import("react").JSX.Element} */ export function AddServiceForm() { const t = useTranslations("admin.services"); - const formRef = useRef(null); - const [state, dispatch, isPending] = useActionState( - createServiceAction, - { - status: "idle", - }, - ); + const formRef = useRef(/** @type {HTMLFormElement | null} */ (null)); + const [state, dispatch, isPending] = useActionState(createServiceAction, { + status: "idle", + }); useEffect(() => { if (state.status === "success") formRef.current?.reset(); diff --git a/app/[locale]/(app)/admin/services/page.jsx b/app/[locale]/(app)/admin/services/page.jsx index 0cf9e4e..5ca5799 100644 --- a/app/[locale]/(app)/admin/services/page.jsx +++ b/app/[locale]/(app)/admin/services/page.jsx @@ -7,7 +7,11 @@ import { Input } from "@/components/ui/input"; import { AddServiceForm } from "./add-service-form"; import { updateServiceAction, deactivateServiceAction } from "./actions"; -export default async function ServicesPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function ServicesPage({ params }) { await params; const t = await getTranslations("admin.services"); diff --git a/app/[locale]/(app)/admin/settings/actions.js b/app/[locale]/(app)/admin/settings/actions.js index 6698aa5..02267e8 100644 --- a/app/[locale]/(app)/admin/settings/actions.js +++ b/app/[locale]/(app)/admin/settings/actions.js @@ -11,17 +11,19 @@ import { revalidatePath } from "next/cache"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { - ClinicSettingsSchema, - type ClinicSettingsState, -} from "@/lib/clinic/clinic-settings-schema"; +import { ClinicSettingsSchema } from "@/lib/clinic/clinic-settings-schema"; -const emptyToNull = (s: string) => (s.length > 0 ? s : null); +/** @typedef {import('@/lib/clinic/clinic-settings-schema').ClinicSettingsState} ClinicSettingsState */ -export async function updateClinicSettingsAction( - _prev: ClinicSettingsState, - formData: FormData, -): Promise { +/** @param {string} s */ +const emptyToNull = (s) => (s.length > 0 ? s : null); + +/** + * @param {ClinicSettingsState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateClinicSettingsAction(_prev, formData) { const t = await getTranslations("admin.settings"); const session = await getServerSession(); @@ -38,7 +40,7 @@ export async function updateClinicSettingsAction( if (!parsed.success) { return { status: "error", - fieldErrors: parsed.error.flatten().fieldErrors as Record, + fieldErrors: /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: null, }; } diff --git a/app/[locale]/(app)/admin/settings/clinic-settings-form.jsx b/app/[locale]/(app)/admin/settings/clinic-settings-form.jsx index 117443d..16d6274 100644 --- a/app/[locale]/(app)/admin/settings/clinic-settings-form.jsx +++ b/app/[locale]/(app)/admin/settings/clinic-settings-form.jsx @@ -13,24 +13,23 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - ClinicSettingsSchema, - type ClinicSettingsInput, - type ClinicSettingsState, -} from "@/lib/clinic/clinic-settings-schema"; +import { ClinicSettingsSchema } from "@/lib/clinic/clinic-settings-schema"; import { updateClinicSettingsAction } from "./actions"; -const FIELDS = ["name", "address", "phone", "prefix"] as const; +/** @typedef {import('@/lib/clinic/clinic-settings-schema').ClinicSettingsInput} ClinicSettingsInput */ +/** @typedef {import('@/lib/clinic/clinic-settings-schema').ClinicSettingsState} ClinicSettingsState */ -export function ClinicSettingsForm({ defaults }: { defaults: ClinicSettingsInput }) { +const FIELDS = /** @type {const} */ (["name", "address", "phone", "prefix"]); + +/** @param {{ defaults: ClinicSettingsInput }} props */ +export function ClinicSettingsForm({ defaults }) { const t = useTranslations("admin.settings"); - const [state, dispatchAction, isPending] = useActionState( - updateClinicSettingsAction, - { status: "idle" }, - ); + const [state, dispatchAction, isPending] = useActionState(updateClinicSettingsAction, { + status: "idle", + }); - const form = useForm({ + const form = useForm({ resolver: zodResolver(ClinicSettingsSchema), mode: "onBlur", defaultValues: defaults, diff --git a/app/[locale]/(app)/admin/settings/page.jsx b/app/[locale]/(app)/admin/settings/page.jsx index 08fb609..0efbc64 100644 --- a/app/[locale]/(app)/admin/settings/page.jsx +++ b/app/[locale]/(app)/admin/settings/page.jsx @@ -9,11 +9,11 @@ import { getTranslations } from "next-intl/server"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import { ClinicSettingsForm } from "./clinic-settings-form"; -export default async function ClinicSettingsPage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function ClinicSettingsPage({ params }) { await params; // Next.js 16: params is async. const t = await getTranslations("admin.settings"); diff --git a/app/[locale]/(app)/admin/staff/actions.js b/app/[locale]/(app)/admin/staff/actions.js index 92474bf..8cedb37 100644 --- a/app/[locale]/(app)/admin/staff/actions.js +++ b/app/[locale]/(app)/admin/staff/actions.js @@ -22,7 +22,11 @@ async function revalidateStaff() { revalidatePath(`/${locale}/admin/staff`); } -export async function updateStaffRoleAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateStaffRoleAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; @@ -43,7 +47,11 @@ export async function updateStaffRoleAction(formData: FormData): Promise { } } -export async function removeStaffAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function removeStaffAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; diff --git a/app/[locale]/(app)/admin/staff/page.jsx b/app/[locale]/(app)/admin/staff/page.jsx index 5baec94..6599eab 100644 --- a/app/[locale]/(app)/admin/staff/page.jsx +++ b/app/[locale]/(app)/admin/staff/page.jsx @@ -13,7 +13,11 @@ import { appRoles } from "@/lib/db/roles"; import { Button } from "@/components/ui/button"; import { updateStaffRoleAction, removeStaffAction } from "./actions"; -export default async function StaffPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function StaffPage({ params }) { await params; const t = await getTranslations("admin.staff"); const tRoles = await getTranslations("roles"); diff --git a/app/[locale]/(app)/admin/templates/[id]/edit/page.jsx b/app/[locale]/(app)/admin/templates/[id]/edit/page.jsx index a2a526b..db923f6 100644 --- a/app/[locale]/(app)/admin/templates/[id]/edit/page.jsx +++ b/app/[locale]/(app)/admin/templates/[id]/edit/page.jsx @@ -7,11 +7,11 @@ import { fieldsJsonToText } from "@/lib/templates/template-schema"; import { TemplateForm } from "../../template-form"; import { updateTemplateAction } from "../../actions"; -export default async function EditTemplatePage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string, id: string }> }} props + * @returns {Promise} + */ +export default async function EditTemplatePage({ params }) { const { id } = await params; const t = await getTranslations("admin.templates"); const templateId = Number(id); diff --git a/app/[locale]/(app)/admin/templates/actions.js b/app/[locale]/(app)/admin/templates/actions.js index 70fac41..da514ee 100644 --- a/app/[locale]/(app)/admin/templates/actions.js +++ b/app/[locale]/(app)/admin/templates/actions.js @@ -12,15 +12,15 @@ import { revalidatePath } from "next/cache"; import { redirect } from "@/i18n/navigation"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { - TemplateSchema, - fieldsTextToJson, - type TemplateFormState, - type TemplateInput, -} from "@/lib/templates/template-schema"; +import { TemplateSchema, fieldsTextToJson } from "@/lib/templates/template-schema"; -function readForm(formData: FormData) { - const get = (k: string) => String(formData.get(k) ?? ""); +/** @typedef {import('@/lib/templates/template-schema').TemplateFormState} TemplateFormState */ +/** @typedef {import('@/lib/templates/template-schema').TemplateInput} TemplateInput */ + +/** @param {FormData} formData */ +function readForm(formData) { + /** @param {string} k */ + const get = (k) => String(formData.get(k) ?? ""); return { name: get("name"), title: get("title"), @@ -30,7 +30,8 @@ function readForm(formData: FormData) { }; } -function toRow(d: TemplateInput) { +/** @param {TemplateInput} d */ +function toRow(d) { return { name: d.name, title: d.title.trim().length ? d.title.trim() : null, @@ -40,16 +41,19 @@ function toRow(d: TemplateInput) { }; } -async function finish(): Promise { +/** @returns {Promise} */ +async function finish() { const locale = await getLocale(); revalidatePath(`/${locale}/admin/templates`); return redirect({ href: `/${locale}/admin/templates`, locale }); } -export async function createTemplateAction( - _prev: TemplateFormState, - formData: FormData, -): Promise { +/** + * @param {TemplateFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function createTemplateAction(_prev, formData) { const t = await getTranslations("admin.templates"); const session = await getServerSession(); @@ -61,7 +65,7 @@ export async function createTemplateAction( if (!parsed.success) { return { status: "error", - fieldErrors: parsed.error.flatten().fieldErrors as Record, + fieldErrors: /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: null, }; } @@ -84,10 +88,12 @@ export async function createTemplateAction( return finish(); } -export async function updateTemplateAction( - _prev: TemplateFormState, - formData: FormData, -): Promise { +/** + * @param {TemplateFormState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function updateTemplateAction(_prev, formData) { const t = await getTranslations("admin.templates"); const session = await getServerSession(); @@ -102,7 +108,7 @@ export async function updateTemplateAction( status: "error", fieldErrors: parsed.success ? {} - : (parsed.error.flatten().fieldErrors as Record), + : /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: parsed.success ? t("errorGeneric") : null, }; } @@ -124,7 +130,11 @@ export async function updateTemplateAction( return finish(); } -export async function deactivateTemplateAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function deactivateTemplateAction(formData) { const session = await getServerSession(); if (session?.role !== "admin") return; diff --git a/app/[locale]/(app)/admin/templates/new/page.jsx b/app/[locale]/(app)/admin/templates/new/page.jsx index 1cc5066..854bda4 100644 --- a/app/[locale]/(app)/admin/templates/new/page.jsx +++ b/app/[locale]/(app)/admin/templates/new/page.jsx @@ -4,7 +4,11 @@ import { getTranslations } from "next-intl/server"; import { TemplateForm } from "../template-form"; import { createTemplateAction } from "../actions"; -export default async function NewTemplatePage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function NewTemplatePage({ params }) { await params; const t = await getTranslations("admin.templates"); diff --git a/app/[locale]/(app)/admin/templates/page.jsx b/app/[locale]/(app)/admin/templates/page.jsx index 3eb7346..7c5a862 100644 --- a/app/[locale]/(app)/admin/templates/page.jsx +++ b/app/[locale]/(app)/admin/templates/page.jsx @@ -10,7 +10,11 @@ import { createSupabaseServerClient } from "@/lib/supabase/server"; import { Button } from "@/components/ui/button"; import { deactivateTemplateAction } from "./actions"; -export default async function TemplatesPage({ params }: { params: Promise<{ locale: string }> }) { +/** + * @param {{ params: Promise<{ locale: string }> }} props + * @returns {Promise} + */ +export default async function TemplatesPage({ params }) { const { locale } = await params; const t = await getTranslations("admin.templates"); @@ -27,7 +31,7 @@ export default async function TemplatesPage({ params }: { params: Promise<{ loca

{t("title")}

@@ -45,7 +49,10 @@ export default async function TemplatesPage({ params }: { params: Promise<{ loca
diff --git a/app/[locale]/(app)/admin/templates/template-form.jsx b/app/[locale]/(app)/admin/templates/template-form.jsx index f6ae6d2..73598a0 100644 --- a/app/[locale]/(app)/admin/templates/template-form.jsx +++ b/app/[locale]/(app)/admin/templates/template-form.jsx @@ -12,37 +12,40 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { templateGenders, type TemplateFormState } from "@/lib/templates/template-schema"; +import { templateGenders } from "@/lib/templates/template-schema"; -export type TemplateDefaults = { - id?: number; - name: string; - title: string; - gender: string; - photoNum: number; - fieldsText: string; -}; +/** @typedef {import('@/lib/templates/template-schema').TemplateFormState} TemplateFormState */ + +/** + * @typedef {Object} TemplateDefaults + * @property {number} [id] + * @property {string} name + * @property {string} title + * @property {string} gender + * @property {number} photoNum + * @property {string} fieldsText + */ const CONTROL = "border-input bg-background text-foreground focus-visible:ring-ring w-full rounded-md border px-3 text-sm focus:outline-none focus-visible:ring-2 disabled:opacity-50"; -export function TemplateForm({ - mode, - action, - defaults, -}: { - mode: "create" | "edit"; - action: (prev: TemplateFormState, formData: FormData) => Promise; - defaults: TemplateDefaults; -}) { +/** + * @param {{ + * mode: "create" | "edit", + * action: (prev: TemplateFormState, formData: FormData) => Promise, + * defaults: TemplateDefaults, + * }} props + */ +export function TemplateForm({ mode, action, defaults }) { const t = useTranslations("admin.templates"); - const [state, dispatch, isPending] = useActionState(action, { + const [state, dispatch, isPending] = useActionState(action, { status: "idle", }); const fe = state.status === "error" ? state.fieldErrors : {}; const formError = state.status === "error" ? state.formError : null; - const err = (f: string) => + /** @param {string} f */ + const err = (f) => fe[f]?.length ? (

{fe[f][0]} diff --git a/app/[locale]/(app)/checkups/[id]/imaging/actions.js b/app/[locale]/(app)/checkups/[id]/imaging/actions.js index 948da1a..beb538a 100644 --- a/app/[locale]/(app)/checkups/[id]/imaging/actions.js +++ b/app/[locale]/(app)/checkups/[id]/imaging/actions.js @@ -18,22 +18,27 @@ import { revalidatePath } from "next/cache"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import type { AppRole } from "@/lib/db/roles"; import { CHECKUP_MEDIA_BUCKET, DeleteImageSchema, RecordImageSchema, isValidStoragePath, - type ImageActionState, } from "@/lib/imaging/image-schema"; -const CLINICAL: AppRole[] = ["admin", "receptionist", "doctor", "nurse"]; -const isClinical = (r: AppRole | null | undefined) => !!r && CLINICAL.includes(r); +/** @typedef {import('@/lib/db/roles').AppRole} AppRole */ +/** @typedef {import('@/lib/imaging/image-schema').ImageActionState} ImageActionState */ -export async function recordImageAction( - checkupId: number, - storagePath: string, -): Promise { +/** @type {AppRole[]} */ +const CLINICAL = ["admin", "receptionist", "doctor", "nurse"]; +/** @param {AppRole | null | undefined} r */ +const isClinical = (r) => !!r && CLINICAL.includes(r); + +/** + * @param {number} checkupId + * @param {string} storagePath + * @returns {Promise} + */ +export async function recordImageAction(checkupId, storagePath) { const t = await getTranslations("imaging"); const session = await getServerSession(); @@ -65,11 +70,13 @@ export async function recordImageAction( return { status: "success" }; } -export async function deleteImageAction( - checkupId: number, - imageId: number, - storagePath: string, -): Promise { +/** + * @param {number} checkupId + * @param {number} imageId + * @param {string} storagePath + * @returns {Promise} + */ +export async function deleteImageAction(checkupId, imageId, storagePath) { const t = await getTranslations("imaging"); const session = await getServerSession(); diff --git a/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx b/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx index e7cdb0e..7a0fc1f 100644 --- a/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx +++ b/app/[locale]/(app)/checkups/[id]/imaging/checkup-barcode.jsx @@ -14,9 +14,12 @@ import bwipjs from "bwip-js/browser"; import { Button } from "@/components/ui/button"; -export function CheckupBarcode({ checkupId }: { checkupId: number }) { +/** + * @param {{ checkupId: number }} props + */ +export function CheckupBarcode({ checkupId }) { const t = useTranslations("imaging"); - const canvasRef = useRef(null); + const canvasRef = useRef(/** @type {HTMLCanvasElement | null} */ (null)); const [error, setError] = useState(false); useEffect(() => { diff --git a/app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx b/app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx index 0478d4c..04b96ec 100644 --- a/app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx +++ b/app/[locale]/(app)/checkups/[id]/imaging/image-capture.jsx @@ -28,18 +28,21 @@ import { } from "@/lib/imaging/image-schema"; import { recordImageAction } from "./actions"; -const QUALITY_STEPS = [0.9, 0.8, 0.7, 0.6, 0.5, 0.4] as const; +/** @type {readonly [0.9, 0.8, 0.7, 0.6, 0.5, 0.4]} */ +const QUALITY_STEPS = [0.9, 0.8, 0.7, 0.6, 0.5, 0.4]; // Downscale the longest side to this before quality-stepping — a full-res phone // photo (e.g. 4000×3000) never fits 200KB on JPEG quality alone, so we must // reduce resolution first. 1280px keeps ultrasound/clinic detail legible. const MAX_DIMENSION = 1280; -/** Draw a source (video frame or image) onto a canvas scaled to fit MAX_DIMENSION. */ -function makeScaledCanvas( - source: CanvasImageSource, - w: number, - h: number, -): HTMLCanvasElement | null { +/** + * Draw a source (video frame or image) onto a canvas scaled to fit MAX_DIMENSION. + * @param {CanvasImageSource} source + * @param {number} w + * @param {number} h + * @returns {HTMLCanvasElement | null} + */ +function makeScaledCanvas(source, w, h) { const scale = Math.min(1, MAX_DIMENSION / Math.max(w, h)); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(w * scale)); @@ -58,13 +61,23 @@ const getCameraSupportSnapshot = () => typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia; const getCameraSupportServerSnapshot = () => false; -function canvasToBlob(canvas: HTMLCanvasElement, quality: number): Promise { +/** + * @param {HTMLCanvasElement} canvas + * @param {number} quality + * @returns {Promise} + */ +function canvasToBlob(canvas, quality) { return new Promise((resolve) => canvas.toBlob((b) => resolve(b), "image/jpeg", quality)); } -/** Steps JPEG quality down from 0.9 to 0.4 until the blob fits MAX_IMAGE_BYTES. */ -async function compressToJpeg(canvas: HTMLCanvasElement): Promise { - let smallest: Blob | null = null; +/** + * Steps JPEG quality down from 0.9 to 0.4 until the blob fits MAX_IMAGE_BYTES. + * @param {HTMLCanvasElement} canvas + * @returns {Promise} + */ +async function compressToJpeg(canvas) { + /** @type {Blob | null} */ + let smallest = null; for (const q of QUALITY_STEPS) { const blob = await canvasToBlob(canvas, q); if (!blob) continue; @@ -74,12 +87,15 @@ async function compressToJpeg(canvas: HTMLCanvasElement): Promise { return smallest && smallest.size <= MAX_IMAGE_BYTES ? smallest : null; } -export function ImageCapture({ checkupId }: { checkupId: number }) { +/** + * @param {{ checkupId: number }} props + */ +export function ImageCapture({ checkupId }) { const t = useTranslations("imaging"); const router = useRouter(); - const videoRef = useRef(null); - const streamRef = useRef(null); + const videoRef = useRef(/** @type {HTMLVideoElement | null} */ (null)); + const streamRef = useRef(/** @type {MediaStream | null} */ (null)); // Browser-only feature detection without an Effect: getServerSnapshot // returns false so SSR/hydration render the same (no-camera) markup, then @@ -91,7 +107,7 @@ export function ImageCapture({ checkupId }: { checkupId: number }) { ); const [streaming, setStreaming] = useState(false); const [uploading, setUploading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(/** @type {string | null} */ (null)); // Stop any open camera stream on unmount. useEffect(() => { @@ -122,7 +138,8 @@ export function ImageCapture({ checkupId }: { checkupId: number }) { setStreaming(false); } - async function uploadFromCanvas(canvas: HTMLCanvasElement) { + /** @param {HTMLCanvasElement} canvas */ + async function uploadFromCanvas(canvas) { setUploading(true); setError(null); try { @@ -169,7 +186,8 @@ export function ImageCapture({ checkupId }: { checkupId: number }) { void uploadFromCanvas(canvas); } - function handleFileChange(e: React.ChangeEvent) { + /** @param {import("react").ChangeEvent} e */ + function handleFileChange(e) { const file = e.target.files?.[0]; e.target.value = ""; // allow re-selecting the same file if (!file) return; diff --git a/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx b/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx index 39503f0..d1ec714 100644 --- a/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx +++ b/app/[locale]/(app)/checkups/[id]/imaging/image-gallery.jsx @@ -15,16 +15,20 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { deleteImageAction } from "./actions"; -type GalleryImage = { id: number; storagePath: string; url: string | null }; +/** @typedef {{ id: number, storagePath: string, url: string | null }} GalleryImage */ -export function ImageGallery({ checkupId, images }: { checkupId: number; images: GalleryImage[] }) { +/** + * @param {{ checkupId: number, images: GalleryImage[] }} props + */ +export function ImageGallery({ checkupId, images }) { const t = useTranslations("imaging"); const router = useRouter(); const [isPending, startTransition] = useTransition(); - const [pendingId, setPendingId] = useState(null); - const [error, setError] = useState(null); + const [pendingId, setPendingId] = useState(/** @type {number | null} */ (null)); + const [error, setError] = useState(/** @type {string | null} */ (null)); - function handleDelete(image: GalleryImage) { + /** @param {GalleryImage} image */ + function handleDelete(image) { setError(null); setPendingId(image.id); startTransition(async () => { diff --git a/app/[locale]/(app)/checkups/[id]/imaging/page.jsx b/app/[locale]/(app)/checkups/[id]/imaging/page.jsx index 4a37792..fd18756 100644 --- a/app/[locale]/(app)/checkups/[id]/imaging/page.jsx +++ b/app/[locale]/(app)/checkups/[id]/imaging/page.jsx @@ -18,11 +18,11 @@ import { ImageCapture } from "./image-capture"; import { ImageGallery } from "./image-gallery"; import { CheckupBarcode } from "./checkup-barcode"; -export default async function ImagingPage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string, id: string }> }} props + * @returns {Promise} + */ +export default async function ImagingPage({ params }) { const { locale, id } = await params; const t = await getTranslations("imaging"); const tReports = await getTranslations("reports"); diff --git a/app/[locale]/(app)/checkups/[id]/invoice/route.js b/app/[locale]/(app)/checkups/[id]/invoice/route.js index ff9343f..61f0ee1 100644 --- a/app/[locale]/(app)/checkups/[id]/invoice/route.js +++ b/app/[locale]/(app)/checkups/[id]/invoice/route.js @@ -7,16 +7,19 @@ import { getTranslations } from "next-intl/server"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import { renderInvoicePdf, type InvoiceLine } from "@/lib/pdf/invoice-document"; +import { renderInvoicePdf } from "@/lib/pdf/invoice-document"; import { sumLineTotals } from "@/lib/billing/totals"; +/** @typedef {import('@/lib/pdf/invoice-document').InvoiceLine} InvoiceLine */ + export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -export async function GET( - _req: Request, - { params }: { params: Promise<{ locale: string; id: string }> }, -) { +/** + * @param {Request} _req + * @param {{ params: Promise<{ locale: string, id: string }> }} context + */ +export async function GET(_req, { params }) { const { id } = await params; const checkupId = Number(id); if (!Number.isFinite(checkupId)) return new Response("Not found", { status: 404 }); @@ -69,13 +72,15 @@ export async function GET( const medName = new Map((meds ?? []).map((m) => [m.id, m.name])); const svcName = new Map((services ?? []).map((x) => [x.id, x.name])); - const medicines: InvoiceLine[] = (items ?? []).map((i) => ({ + /** @type {InvoiceLine[]} */ + const medicines = (items ?? []).map((i) => ({ name: medName.get(i.medicine_id) ?? "—", quantity: i.quantity, unitPrice: i.unit_price, lineTotal: i.line_total, })); - const serviceLines: InvoiceLine[] = (svcs ?? []).map((x) => ({ + /** @type {InvoiceLine[]} */ + const serviceLines = (svcs ?? []).map((x) => ({ name: svcName.get(x.service_id) ?? "—", quantity: x.quantity, unitPrice: x.unit_price, diff --git a/app/[locale]/(app)/checkups/[id]/page.jsx b/app/[locale]/(app)/checkups/[id]/page.jsx index 9e2659b..61344b2 100644 --- a/app/[locale]/(app)/checkups/[id]/page.jsx +++ b/app/[locale]/(app)/checkups/[id]/page.jsx @@ -9,13 +9,14 @@ import { Button } from "@/components/ui/button"; import { CheckupForm } from "../checkup-form"; import { DeleteCheckupButton } from "../delete-checkup-button"; -const str = (v: string | number | null) => (v == null ? "" : String(v)); +/** @param {string | number | null} v */ +const str = (v) => (v == null ? "" : String(v)); -export default async function CheckupPage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string, id: string }> }} props + * @returns {Promise} + */ +export default async function CheckupPage({ params }) { const { locale, id } = await params; const t = await getTranslations("checkups"); const tBilling = await getTranslations("billing"); @@ -58,7 +59,9 @@ export default async function CheckupPage({ // De-dupe + trim recent diagnoses for the quick-pick, capped at 50 entries. const diagnosisSuggestions = [ ...new Set( - (recentDiagnoses ?? []).map((r) => r.diagnosis?.trim()).filter((d): d is string => !!d), + (recentDiagnoses ?? []) + .map((r) => r.diagnosis?.trim()) + .filter(/** @returns {d is string} */ (d) => !!d), ), ].slice(0, 50); @@ -71,8 +74,11 @@ export default async function CheckupPage({ : (templates ?? []); const templateValues = Array.isArray(c.template_values) - ? (c.template_values as unknown[]) - .filter((v): v is { label: unknown; value: unknown } => !!v && typeof v === "object") + ? /** @type {unknown[]} */ (c.template_values) + .filter( + /** @returns {v is { label: unknown, value: unknown }} */ + (v) => !!v && typeof v === "object", + ) .map((v) => ({ label: String(v.label ?? ""), value: String(v.value ?? "") })) : []; @@ -102,12 +108,15 @@ export default async function CheckupPage({

diff --git a/app/[locale]/(app)/checkups/[id]/prescription/actions.js b/app/[locale]/(app)/checkups/[id]/prescription/actions.js index 7e74f36..0a808ad 100644 --- a/app/[locale]/(app)/checkups/[id]/prescription/actions.js +++ b/app/[locale]/(app)/checkups/[id]/prescription/actions.js @@ -16,35 +16,47 @@ import { revalidatePath } from "next/cache"; import { redirect } from "@/i18n/navigation"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import type { AppRole } from "@/lib/db/roles"; import { MedicineLinesSchema, ServiceLinesSchema, MarkPaidSchema, - type PrescriptionSaveState, - type MarkPaidState, } from "@/lib/billing/prescription-schema"; -const CLINICAL: AppRole[] = ["admin", "receptionist", "doctor", "nurse"]; -const BILLING: AppRole[] = ["admin", "cashier"]; -const isClinical = (r: AppRole | null | undefined) => !!r && CLINICAL.includes(r); -const isBilling = (r: AppRole | null | undefined) => !!r && BILLING.includes(r); +/** @typedef {import('@/lib/db/roles').AppRole} AppRole */ +/** @typedef {import('@/lib/billing/prescription-schema').PrescriptionSaveState} PrescriptionSaveState */ +/** @typedef {import('@/lib/billing/prescription-schema').MarkPaidState} MarkPaidState */ -/** Parses a JSON-array form field; returns [] on any malformed input. */ -function parseJsonArray(raw: FormDataEntryValue | null): unknown[] { +/** @type {AppRole[]} */ +const CLINICAL = ["admin", "receptionist", "doctor", "nurse"]; +/** @type {AppRole[]} */ +const BILLING = ["admin", "cashier"]; +/** @param {AppRole | null | undefined} r */ +const isClinical = (r) => !!r && CLINICAL.includes(r); +/** @param {AppRole | null | undefined} r */ +const isBilling = (r) => !!r && BILLING.includes(r); + +/** + * Parses a JSON-array form field; returns [] on any malformed input. + * @param {FormDataEntryValue | null} raw + * @returns {unknown[]} + */ +function parseJsonArray(raw) { if (typeof raw !== "string") return []; try { - const v: unknown = JSON.parse(raw); + /** @type {unknown} */ + const v = JSON.parse(raw); return Array.isArray(v) ? v : []; } catch { return []; } } -export async function savePrescriptionAction( - _prev: PrescriptionSaveState, - formData: FormData, -): Promise { +/** + * @param {PrescriptionSaveState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function savePrescriptionAction(_prev, formData) { const t = await getTranslations("billing"); const session = await getServerSession(); @@ -95,10 +107,12 @@ export async function savePrescriptionAction( return redirect({ href: `/${locale}/checkups/${checkupId}`, locale }); } -export async function markPaidAction( - _prev: MarkPaidState, - formData: FormData, -): Promise { +/** + * @param {MarkPaidState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function markPaidAction(_prev, formData) { const t = await getTranslations("billing"); const session = await getServerSession(); diff --git a/app/[locale]/(app)/checkups/[id]/prescription/page.jsx b/app/[locale]/(app)/checkups/[id]/prescription/page.jsx index 45f40e5..8bde153 100644 --- a/app/[locale]/(app)/checkups/[id]/prescription/page.jsx +++ b/app/[locale]/(app)/checkups/[id]/prescription/page.jsx @@ -16,11 +16,11 @@ import { Button } from "@/components/ui/button"; import { routing } from "@/i18n/routing"; import { PrescriptionComposer } from "./prescription-composer"; -export default async function PrescriptionPage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +/** + * @param {{ params: Promise<{ locale: string, id: string }> }} props + * @returns {Promise} + */ +export default async function PrescriptionPage({ params }) { const { locale, id } = await params; const t = await getTranslations("billing"); const tReports = await getTranslations("reports"); diff --git a/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js b/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js index 48ebac5..649aa5f 100644 --- a/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js +++ b/app/[locale]/(app)/checkups/[id]/prescription/pdf/route.js @@ -9,18 +9,18 @@ import { getTranslations } from "next-intl/server"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import { computeAge } from "@/lib/pdf/patient-info"; -import { - renderPrescriptionPdf, - type PrescriptionMedicineLine, -} from "@/lib/pdf/prescription-document"; +import { renderPrescriptionPdf } from "@/lib/pdf/prescription-document"; + +/** @typedef {import('@/lib/pdf/prescription-document').PrescriptionMedicineLine} PrescriptionMedicineLine */ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -export async function GET( - _req: Request, - { params }: { params: Promise<{ locale: string; id: string }> }, -) { +/** + * @param {Request} _req + * @param {{ params: Promise<{ locale: string, id: string }> }} context + */ +export async function GET(_req, { params }) { const { id } = await params; const checkupId = Number(id); if (!Number.isFinite(checkupId)) return new Response("Not found", { status: 404 }); @@ -71,7 +71,8 @@ export async function GET( : { data: [] }; const medById = new Map((meds ?? []).map((m) => [m.id, m])); - const medicines: PrescriptionMedicineLine[] = (items ?? []).map((i) => { + /** @type {PrescriptionMedicineLine[]} */ + const medicines = (items ?? []).map((i) => { const med = medById.get(i.medicine_id); return { name: med?.name ?? "—", diff --git a/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx b/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx index c1a7a71..1a9e68f 100644 --- a/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx +++ b/app/[locale]/(app)/checkups/[id]/prescription/prescription-composer.jsx @@ -18,38 +18,51 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { dosePresets } from "@/lib/billing/dose-presets"; -import { - paymentMethods, - type MarkPaidState, - type PrescriptionSaveState, -} from "@/lib/billing/prescription-schema"; +import { paymentMethods } from "@/lib/billing/prescription-schema"; import { markPaidAction, savePrescriptionAction } from "./actions"; +/** @typedef {import('@/lib/billing/prescription-schema').MarkPaidState} MarkPaidState */ +/** @typedef {import('@/lib/billing/prescription-schema').PrescriptionSaveState} PrescriptionSaveState */ + const DOSE_PRESETS_LIST_ID = "dose-presets"; -const vnd = (n: number) => `${new Intl.NumberFormat("vi-VN").format(n)} ₫`; +/** @param {number} n */ +const vnd = (n) => `${new Intl.NumberFormat("vi-VN").format(n)} ₫`; const SELECT = "border-input bg-background text-foreground focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm focus:outline-none focus-visible:ring-2 disabled:opacity-50"; -type Medicine = { id: number; name: string; unit: string | null; sale_price: number }; -type Service = { id: number; name: string; price: number }; +/** @typedef {{ id: number, name: string, unit: string | null, sale_price: number }} Medicine */ +/** @typedef {{ id: number, name: string, price: number }} Service */ -type MedicineRow = { - key: string; - medicineId: number; - quantity: number; - dosage: string; - notes: string; -}; -type ServiceRow = { key: string; serviceId: number; quantity: number }; +/** + * @typedef {object} MedicineRow + * @property {string} key + * @property {number} medicineId + * @property {number} quantity + * @property {string} dosage + * @property {string} notes + */ +/** @typedef {{ key: string, serviceId: number, quantity: number }} ServiceRow */ -type Payment = { - status: "unpaid" | "paid"; - method: string | null; - paidAt: string | null; -}; +/** + * @typedef {object} Payment + * @property {"unpaid" | "paid"} status + * @property {string | null} method + * @property {string | null} paidAt + */ +/** + * @param {{ + * checkupId: number, + * medicines: Medicine[], + * services: Service[], + * initialMedicineLines: { medicineId: number, quantity: number, dosage: string, notes: string }[], + * initialServiceLines: { serviceId: number, quantity: number }[], + * payment: Payment, + * canMarkPaid: boolean, + * }} props + */ export function PrescriptionComposer({ checkupId, medicines, @@ -58,34 +71,24 @@ export function PrescriptionComposer({ initialServiceLines, payment, canMarkPaid, -}: { - checkupId: number; - medicines: Medicine[]; - services: Service[]; - initialMedicineLines: { medicineId: number; quantity: number; dosage: string; notes: string }[]; - initialServiceLines: { serviceId: number; quantity: number }[]; - payment: Payment; - canMarkPaid: boolean; }) { const t = useTranslations("billing"); - const [medicineRows, setMedicineRows] = useState(() => - initialMedicineLines.map((l, i) => ({ ...l, key: `m-init-${i}` })), + const [medicineRows, setMedicineRows] = useState( + /** @type {MedicineRow[]} */ ( + initialMedicineLines.map((l, i) => ({ ...l, key: `m-init-${i}` })) + ), ); - const [serviceRows, setServiceRows] = useState(() => - initialServiceLines.map((l, i) => ({ ...l, key: `s-init-${i}` })), + const [serviceRows, setServiceRows] = useState( + /** @type {ServiceRow[]} */ (initialServiceLines.map((l, i) => ({ ...l, key: `s-init-${i}` }))), ); - const [saveState, saveDispatch, isSaving] = useActionState( - savePrescriptionAction, - { status: "idle" }, - ); - const [payState, payDispatch, isPaying] = useActionState( - markPaidAction, - { - status: "idle", - }, - ); + const [saveState, saveDispatch, isSaving] = useActionState(savePrescriptionAction, { + status: "idle", + }); + const [payState, payDispatch, isPaying] = useActionState(markPaidAction, { + status: "idle", + }); const medMap = new Map(medicines.map((m) => [m.id, m.sale_price])); const svcMap = new Map(services.map((s) => [s.id, s.price])); @@ -101,26 +104,32 @@ export function PrescriptionComposer({ notes: "", }, ]); - const removeMedicineRow = (key: string) => - setMedicineRows((rows) => rows.filter((r) => r.key !== key)); - const updateMedicineRow = ( - key: string, - field: K, - value: MedicineRow[K], - ) => setMedicineRows((rows) => rows.map((r) => (r.key === key ? { ...r, [field]: value } : r))); + /** @param {string} key */ + const removeMedicineRow = (key) => setMedicineRows((rows) => rows.filter((r) => r.key !== key)); + /** + * @template {keyof MedicineRow} K + * @param {string} key + * @param {K} field + * @param {MedicineRow[K]} value + */ + const updateMedicineRow = (key, field, value) => + setMedicineRows((rows) => rows.map((r) => (r.key === key ? { ...r, [field]: value } : r))); const addServiceRow = () => setServiceRows((rows) => [ ...rows, { key: `s-${crypto.randomUUID()}`, serviceId: services[0]?.id ?? 0, quantity: 1 }, ]); - const removeServiceRow = (key: string) => - setServiceRows((rows) => rows.filter((r) => r.key !== key)); - const updateServiceRow = ( - key: string, - field: K, - value: ServiceRow[K], - ) => setServiceRows((rows) => rows.map((r) => (r.key === key ? { ...r, [field]: value } : r))); + /** @param {string} key */ + const removeServiceRow = (key) => setServiceRows((rows) => rows.filter((r) => r.key !== key)); + /** + * @template {keyof ServiceRow} K + * @param {string} key + * @param {K} field + * @param {ServiceRow[K]} value + */ + const updateServiceRow = (key, field, value) => + setServiceRows((rows) => rows.map((r) => (r.key === key ? { ...r, [field]: value } : r))); const medicineTotal = medicineRows.reduce( (sum, r) => sum + (medMap.get(r.medicineId) ?? 0) * (r.quantity || 0), diff --git a/app/[locale]/(app)/checkups/[id]/report/route.js b/app/[locale]/(app)/checkups/[id]/report/route.js index f6dcc6a..3d944b8 100644 --- a/app/[locale]/(app)/checkups/[id]/report/route.js +++ b/app/[locale]/(app)/checkups/[id]/report/route.js @@ -14,17 +14,20 @@ import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import { CHECKUP_MEDIA_BUCKET, SIGNED_URL_TTL_SECONDS } from "@/lib/imaging/image-schema"; import { computeAge } from "@/lib/pdf/patient-info"; -import { renderUltrasoundPdf, type UltrasoundImage } from "@/lib/pdf/ultrasound-document"; +import { renderUltrasoundPdf } from "@/lib/pdf/ultrasound-document"; + +/** @typedef {import('@/lib/pdf/ultrasound-document').UltrasoundImage} UltrasoundImage */ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const MAX_REPORT_IMAGES = 4; -export async function GET( - _req: Request, - { params }: { params: Promise<{ locale: string; id: string }> }, -) { +/** + * @param {Request} _req + * @param {{ params: Promise<{ locale: string, id: string }> }} context + */ +export async function GET(_req, { params }) { const { id } = await params; const checkupId = Number(id); if (!Number.isFinite(checkupId)) return new Response("Not found", { status: 404 }); @@ -75,23 +78,27 @@ export async function GET( const tPatients = await getTranslations("patients"); const downloaded = await Promise.all( - (images ?? []).map(async (img): Promise => { - const { data: signed } = await supabase.storage - .from(CHECKUP_MEDIA_BUCKET) - .createSignedUrl(img.storage_path, SIGNED_URL_TTL_SECONDS); - if (!signed?.signedUrl) return null; - try { - const res = await fetch(signed.signedUrl); - if (!res.ok) return null; - const arrayBuffer = await res.arrayBuffer(); - return { data: Buffer.from(arrayBuffer), format: "jpg" }; - } catch { - return null; - } - }), + (images ?? []).map( + /** @returns {Promise} */ + async (img) => { + const { data: signed } = await supabase.storage + .from(CHECKUP_MEDIA_BUCKET) + .createSignedUrl(img.storage_path, SIGNED_URL_TTL_SECONDS); + if (!signed?.signedUrl) return null; + try { + const res = await fetch(signed.signedUrl); + if (!res.ok) return null; + const arrayBuffer = await res.arrayBuffer(); + return { data: Buffer.from(arrayBuffer), format: "jpg" }; + } catch { + return null; + } + }, + ), ); - const reportImages: UltrasoundImage[] = downloaded.filter( - (img): img is UltrasoundImage => img != null, + /** @type {UltrasoundImage[]} */ + const reportImages = downloaded.filter( + /** @returns {img is UltrasoundImage} */ (img) => img != null, ); const buffer = await renderUltrasoundPdf({ diff --git a/app/[locale]/(app)/checkups/actions.js b/app/[locale]/(app)/checkups/actions.js index 6cd6def..b345c1b 100644 --- a/app/[locale]/(app)/checkups/actions.js +++ b/app/[locale]/(app)/checkups/actions.js @@ -12,22 +12,24 @@ import { revalidatePath } from "next/cache"; import { redirect } from "@/i18n/navigation"; import { getServerSession } from "@/lib/auth/get-server-session"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import type { AppRole } from "@/lib/db/roles"; -import { - CheckupSaveSchema, - parseNum, - parseTemplateValues, - type CheckupSaveState, -} from "@/lib/checkups/checkup-schema"; +import { CheckupSaveSchema, parseNum, parseTemplateValues } from "@/lib/checkups/checkup-schema"; -const CLINICAL: AppRole[] = ["admin", "receptionist", "doctor", "nurse"]; -const isClinical = (r: AppRole | null | undefined) => !!r && CLINICAL.includes(r); -const nullIfBlank = (s: string) => (s.trim().length ? s.trim() : null); +/** @typedef {import('@/lib/db/roles').AppRole} AppRole */ +/** @typedef {import('@/lib/checkups/checkup-schema').CheckupSaveState} CheckupSaveState */ -export async function saveCheckupAction( - _prev: CheckupSaveState, - formData: FormData, -): Promise { +/** @type {AppRole[]} */ +const CLINICAL = ["admin", "receptionist", "doctor", "nurse"]; +/** @param {AppRole | null | undefined} r */ +const isClinical = (r) => !!r && CLINICAL.includes(r); +/** @param {string} s */ +const nullIfBlank = (s) => (s.trim().length ? s.trim() : null); + +/** + * @param {CheckupSaveState} _prev + * @param {FormData} formData + * @returns {Promise} + */ +export async function saveCheckupAction(_prev, formData) { const t = await getTranslations("checkups"); const session = await getServerSession(); @@ -36,7 +38,8 @@ export async function saveCheckupAction( } const id = Number(formData.get("id")); - const get = (k: string) => String(formData.get(k) ?? ""); + /** @param {string} k */ + const get = (k) => String(formData.get(k) ?? ""); const parsed = CheckupSaveSchema.safeParse({ heartBeat: get("heartBeat"), bloodPressure: get("bloodPressure"), @@ -56,7 +59,7 @@ export async function saveCheckupAction( status: "error", fieldErrors: parsed.success ? {} - : (parsed.error.flatten().fieldErrors as Record), + : /** @type {Record} */ (parsed.error.flatten().fieldErrors), formError: parsed.success ? t("errorGeneric") : null, }; } @@ -104,7 +107,11 @@ export async function saveCheckupAction( } // ── Soft-delete ─────────────────────────────────────────────────────────────── -export async function deleteCheckupAction(formData: FormData): Promise { +/** + * @param {FormData} formData + * @returns {Promise} + */ +export async function deleteCheckupAction(formData) { const session = await getServerSession(); if (!isClinical(session?.role)) return; diff --git a/app/[locale]/(app)/checkups/checkup-form.jsx b/app/[locale]/(app)/checkups/checkup-form.jsx index 770ce98..36c119b 100644 --- a/app/[locale]/(app)/checkups/checkup-form.jsx +++ b/app/[locale]/(app)/checkups/checkup-form.jsx @@ -6,56 +6,59 @@ * (redirects to the queue on success). */ -import { useActionState, useEffect, useRef, useState, type RefObject } from "react"; +import { useActionState, useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { checkupStatuses, type CheckupSaveState } from "@/lib/checkups/checkup-schema"; +import { checkupStatuses } from "@/lib/checkups/checkup-schema"; import { saveCheckupAction } from "./actions"; +/** @typedef {import('@/lib/checkups/checkup-schema').CheckupSaveState} CheckupSaveState */ + const CONTROL = "border-input bg-background text-foreground focus-visible:ring-ring w-full rounded-md border px-3 text-sm focus:outline-none focus-visible:ring-2 disabled:opacity-50"; -export type CheckupDefaults = { - id: number; - heartBeat: string; - bloodPressure: string; - temperature: string; - weight: string; - height: string; - symptoms: string; - diagnosis: string; - conclusion: string; - notes: string; - recheckDate: string; - status: string; -}; +/** + * @typedef {object} CheckupDefaults + * @property {number} id + * @property {string} heartBeat + * @property {string} bloodPressure + * @property {string} temperature + * @property {string} weight + * @property {string} height + * @property {string} symptoms + * @property {string} diagnosis + * @property {string} conclusion + * @property {string} notes + * @property {string} recheckDate + * @property {string} status + */ -export type CheckupTemplateOption = { id: number; name: string; labels: string[] }; -type TemplateValue = { label: string; value: string }; +/** @typedef {{ id: number, name: string, labels: string[] }} CheckupTemplateOption */ +/** @typedef {{ label: string, value: string }} TemplateValue */ +/** + * @param {{ + * defaults: CheckupDefaults, + * templates: CheckupTemplateOption[], + * initialTemplateId: number | null, + * initialTemplateValues: TemplateValue[], + * diagnosisSuggestions?: string[], + * }} props + */ export function CheckupForm({ defaults, templates, initialTemplateId, initialTemplateValues, diagnosisSuggestions = [], -}: { - defaults: CheckupDefaults; - templates: CheckupTemplateOption[]; - initialTemplateId: number | null; - initialTemplateValues: TemplateValue[]; - diagnosisSuggestions?: string[]; }) { const t = useTranslations("checkups"); - const [state, dispatch, isPending] = useActionState( - saveCheckupAction, - { - status: "idle", - }, - ); + const [state, dispatch, isPending] = useActionState(saveCheckupAction, { + status: "idle", + }); const formError = state.status === "error" ? state.formError : null; // Unsaved-changes guard: any edit flips `dirty`; saving clears it so a @@ -63,11 +66,12 @@ export function CheckupForm({ // beforeunload doesn't fire for client-side route changes, so we also show // an in-app marker near the Save button. const [dirty, setDirty] = useState(false); - const diagnosisRef = useRef(null); + const diagnosisRef = useRef(/** @type {HTMLTextAreaElement | null} */ (null)); useEffect(() => { if (!dirty || isPending) return; - function onBeforeUnload(e: BeforeUnloadEvent) { + /** @param {BeforeUnloadEvent} e */ + function onBeforeUnload(e) { e.preventDefault(); e.returnValue = ""; } @@ -80,7 +84,8 @@ export function CheckupForm({ // appends the chosen text into the (uncontrolled) textarea via ref — the // least disruptive option since it doesn't require converting the field to // controlled state. - function applyDiagnosisSuggestion(text: string) { + /** @param {string} text */ + function applyDiagnosisSuggestion(text) { if (!text) return; const el = diagnosisRef.current; if (!el) return; @@ -95,8 +100,10 @@ export function CheckupForm({ const [templateId, setTemplateId] = useState( initialTemplateId != null ? String(initialTemplateId) : "", ); - const [templateFieldValues, setTemplateFieldValues] = useState>(() => - Object.fromEntries(initialTemplateValues.map((v) => [v.label, v.value])), + const [templateFieldValues, setTemplateFieldValues] = useState( + /** @type {Record} */ ( + Object.fromEntries(initialTemplateValues.map((v) => [v.label, v.value])) + ), ); const selectedTemplate = templates.find((tpl) => String(tpl.id) === templateId) ?? null; @@ -107,18 +114,23 @@ export function CheckupForm({ })), ); - const textField = (name: keyof CheckupDefaults, label: string) => ( + /** + * @param {keyof CheckupDefaults} name + * @param {string} label + */ + const textField = (name, label) => (
); - const area = ( - name: keyof CheckupDefaults, - label: string, - ref?: RefObject, - ) => ( + /** + * @param {keyof CheckupDefaults} name + * @param {string} label + * @param {import("react").RefObject} [ref] + */ + const area = (name, label, ref) => (