From 5ce1b4728b1e6119c735a65b5b00b2dd00675a70 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 2 Dec 2025 17:00:17 +0100 Subject: [PATCH] feat: add kitchen dashboard, format codebase --- package.json | 2 + pnpm-lock.yaml | 91 +- src/access/isSuperAdmin.ts | 12 +- src/access/roles.ts | 63 +- src/app/(app)/caregiver/dashboard/page.tsx | 536 +++++---- src/app/(app)/caregiver/login/page.tsx | 153 +-- src/app/(app)/caregiver/orders/[id]/page.tsx | 929 +++++++++------ src/app/(app)/caregiver/orders/new/page.tsx | 1028 +++++++++++++---- src/app/(app)/caregiver/orders/page.tsx | 112 +- src/app/(app)/caregiver/residents/page.tsx | 91 +- src/app/(app)/kitchen/dashboard/page.tsx | 730 ++++++++++++ src/app/(app)/layout.tsx | 30 +- src/app/(app)/login/page.tsx | 179 +++ src/app/(app)/page.tsx | 5 +- .../admin/[[...segments]]/not-found.tsx | 29 +- .../(payload)/admin/[[...segments]]/page.tsx | 29 +- .../admin/views/KitchenDashboard/index.tsx | 238 ++-- .../admin/views/KitchenDashboard/styles.scss | 3 +- src/app/(payload)/api/[...slug]/route.ts | 20 +- .../(payload)/api/graphql-playground/route.ts | 6 +- src/app/(payload)/api/graphql/route.ts | 8 +- src/app/(payload)/layout.tsx | 34 +- src/app/api/analyze-form/route.ts | 264 ++--- src/app/globals.css | 4 +- src/collections/MealOrders/index.ts | 167 +-- .../Meals/endpoints/kitchenReport.ts | 279 +++-- src/collections/Meals/hooks/generateTitle.ts | 47 +- src/collections/Meals/hooks/setCreatedBy.ts | 16 +- src/collections/Meals/index.ts | 569 +++++---- src/collections/Media/index.ts | 68 +- src/collections/Residents/index.ts | 98 +- src/collections/Tenants/access/byTenant.ts | 22 +- .../Tenants/access/updateAndDelete.ts | 16 +- src/collections/Tenants/index.ts | 50 +- src/collections/Users/access/create.ts | 30 +- .../Users/access/isAccessingSelf.ts | 14 +- src/collections/Users/access/read.ts | 42 +- .../Users/access/updateAndDelete.ts | 22 +- .../Users/endpoints/externalUsersLogin.ts | 70 +- .../Users/hooks/ensureUniqueUsername.ts | 53 +- .../Users/hooks/setCookieBasedOnDomain.ts | 31 +- src/collections/Users/index.ts | 98 +- src/components/caregiver/CheckboxOption.tsx | 41 +- src/components/caregiver/EmptyState.tsx | 37 +- src/components/caregiver/LoadingSpinner.tsx | 40 +- src/components/caregiver/MealTypeIcon.tsx | 45 +- src/components/caregiver/MealTypeSelector.tsx | 46 +- src/components/caregiver/PageHeader.tsx | 30 +- src/components/caregiver/StatCard.tsx | 36 +- src/components/caregiver/StatusBadge.tsx | 35 +- src/components/caregiver/index.ts | 16 +- src/components/ui/alert-dialog.tsx | 40 +- src/components/ui/alert.tsx | 22 +- src/components/ui/badge.tsx | 18 +- src/components/ui/button.tsx | 20 +- src/components/ui/card.tsx | 26 +- src/components/ui/checkbox.tsx | 16 +- src/components/ui/collapsible.tsx | 33 + src/components/ui/dialog.tsx | 40 +- src/components/ui/input.tsx | 10 +- src/components/ui/label.tsx | 14 +- src/components/ui/progress.tsx | 14 +- src/components/ui/radio-group.tsx | 18 +- src/components/ui/select.tsx | 46 +- src/components/ui/separator.tsx | 14 +- src/components/ui/sheet.tsx | 38 +- src/components/ui/table.tsx | 32 +- src/components/ui/tooltip.tsx | 20 +- src/hooks/useAuth.ts | 92 ++ src/lib/auth.ts | 82 ++ src/lib/constants/meal-options.ts | 266 +++-- src/lib/constants/meal.ts | 149 ++- src/lib/utils.ts | 6 +- src/migrations/20251202_123751.json | 220 +--- src/migrations/20251202_123751.ts | 14 +- src/migrations/index.ts | 4 +- src/payload.config.ts | 93 +- src/seed.ts | 301 ++--- src/utilities/dateFormat.ts | 20 +- src/utilities/extractID.ts | 16 +- src/utilities/getCollectionIDType.ts | 20 +- src/utilities/getUserTenantIDs.ts | 22 +- 82 files changed, 5206 insertions(+), 3134 deletions(-) create mode 100644 src/app/(app)/kitchen/dashboard/page.tsx create mode 100644 src/app/(app)/login/page.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/lib/auth.ts diff --git a/package.json b/package.json index b8d89c2..170e55b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@payloadcms/ui": "3.65.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", @@ -62,6 +63,7 @@ "@types/react-dom": "19.0.1", "eslint": "^8.57.0", "eslint-config-next": "^15.0.0", + "prettier": "^3.7.3", "tsx": "^4.16.2", "tw-animate-css": "^1.4.0", "typescript": "5.5.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50ca8ad..e676402 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.3 version: 1.3.3(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -119,7 +122,7 @@ importers: devDependencies: '@payloadcms/eslint-config': specifier: ^3.28.0 - version: 3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2)) + version: 3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2)) '@payloadcms/graphql': specifier: latest version: 3.65.0(graphql@16.12.0)(payload@3.65.0(graphql@16.12.0)(typescript@5.5.2))(typescript@5.5.2) @@ -138,6 +141,9 @@ importers: eslint-config-next: specifier: ^15.0.0 version: 15.5.6(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2) + prettier: + specifier: ^3.7.3 + version: 3.7.3 tsx: specifier: ^4.16.2 version: 4.21.0 @@ -1551,6 +1557,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -6962,17 +6981,17 @@ snapshots: - sql.js - sqlite3 - '@payloadcms/eslint-config@3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2))': + '@payloadcms/eslint-config@3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2))': dependencies: '@eslint-react/eslint-plugin': 1.31.0(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.5.2))(typescript@5.7.3) '@eslint/js': 9.22.0 - '@payloadcms/eslint-plugin': 3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2)) + '@payloadcms/eslint-plugin': 3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2)) '@types/eslint': 9.6.1 '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) eslint: 9.22.0(jiti@2.6.1) eslint-config-prettier: 10.1.1(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-import-x: 4.6.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) - eslint-plugin-jest: 28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) + eslint-plugin-jest: 28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) eslint-plugin-jest-dom: 5.5.0(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-perfectionist: 3.9.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) @@ -6993,7 +7012,7 @@ snapshots: - ts-api-utils - vue-eslint-parser - '@payloadcms/eslint-plugin@3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2))': + '@payloadcms/eslint-plugin@3.28.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(jiti@2.6.1)(ts-api-utils@2.1.0(typescript@5.5.2))': dependencies: '@eslint-react/eslint-plugin': 1.31.0(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.5.2))(typescript@5.7.3) '@eslint/js': 9.22.0 @@ -7002,7 +7021,7 @@ snapshots: eslint: 9.22.0(jiti@2.6.1) eslint-config-prettier: 10.1.1(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-import-x: 4.6.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) - eslint-plugin-jest: 28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) + eslint-plugin-jest: 28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) eslint-plugin-jest-dom: 5.5.0(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.6.1)) eslint-plugin-perfectionist: 3.9.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) @@ -7227,6 +7246,22 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.1)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.1 + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.1)(react@19.0.0) @@ -8121,7 +8156,7 @@ snapshots: dependencies: '@types/node': 24.10.1 - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) @@ -8138,24 +8173,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) - '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/type-utils': 8.48.0(eslint@8.57.1)(typescript@5.5.2) - '@typescript-eslint/utils': 8.48.0(eslint@8.57.1)(typescript@5.5.2) - '@typescript-eslint/visitor-keys': 8.48.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.5.2) - typescript: 5.5.2 - transitivePeerDependencies: - - supports-color - optional: true - '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9094,8 +9111,8 @@ snapshots: '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.5.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -9118,7 +9135,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -9129,19 +9146,19 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import-x: 4.6.1(eslint@8.57.1)(typescript@5.5.2) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.5.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -9186,7 +9203,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9197,7 +9214,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.6.1(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9209,7 +9226,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.5.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -9221,12 +9238,12 @@ snapshots: eslint: 9.22.0(jiti@2.6.1) requireindex: 1.2.0 - eslint-plugin-jest@28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3): + eslint-plugin-jest@28.11.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3): dependencies: '@typescript-eslint/utils': 8.48.0(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) eslint: 9.22.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2) + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1)(typescript@5.5.2) transitivePeerDependencies: - supports-color - typescript @@ -11502,7 +11519,7 @@ snapshots: typescript-eslint@8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.5.2))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3))(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.7.3) eslint: 9.22.0(jiti@2.6.1) diff --git a/src/access/isSuperAdmin.ts b/src/access/isSuperAdmin.ts index f449243..37db052 100644 --- a/src/access/isSuperAdmin.ts +++ b/src/access/isSuperAdmin.ts @@ -1,10 +1,10 @@ -import type { Access } from 'payload' -import { User } from '../payload-types' +import type { Access } from "payload"; +import { User } from "../payload-types"; export const isSuperAdminAccess: Access = ({ req }): boolean => { - return isSuperAdmin(req.user) -} + return isSuperAdmin(req.user); +}; export const isSuperAdmin = (user: User | null): boolean => { - return Boolean(user?.roles?.includes('super-admin')) -} + return Boolean(user?.roles?.includes("super-admin")); +}; diff --git a/src/access/roles.ts b/src/access/roles.ts index 1c66f21..dd06f59 100644 --- a/src/access/roles.ts +++ b/src/access/roles.ts @@ -1,18 +1,21 @@ -import type { User, Tenant } from '../payload-types' -import { extractID } from '../utilities/extractID' +import type { User, Tenant } from "../payload-types"; +import { extractID } from "../utilities/extractID"; /** * Tenant role types for care home staff */ -export type TenantRole = 'admin' | 'caregiver' | 'kitchen' +export type TenantRole = "admin" | "caregiver" | "kitchen"; /** * Check if user has a specific tenant role in any tenant */ -export const hasTenantRole = (user: User | null | undefined, role: TenantRole): boolean => { - if (!user?.tenants) return false - return user.tenants.some((t) => t.roles?.includes(role)) -} +export const hasTenantRole = ( + user: User | null | undefined, + role: TenantRole, +): boolean => { + if (!user?.tenants) return false; + return user.tenants.some((t) => t.roles?.includes(role)); +}; /** * Check if user has a specific tenant role in a specific tenant @@ -22,12 +25,12 @@ export const hasTenantRoleInTenant = ( role: TenantRole, tenantId: number | string, ): boolean => { - if (!user?.tenants) return false - const targetId = String(tenantId) + if (!user?.tenants) return false; + const targetId = String(tenantId); return user.tenants.some( (t) => String(extractID(t.tenant)) === targetId && t.roles?.includes(role), - ) -} + ); +}; /** * Get tenant IDs where user has a specific role @@ -35,55 +38,57 @@ export const hasTenantRoleInTenant = ( export const getTenantIDsWithRole = ( user: User | null | undefined, role: TenantRole, -): Tenant['id'][] => { - if (!user?.tenants) return [] +): Tenant["id"][] => { + if (!user?.tenants) return []; return user.tenants .filter((t) => t.roles?.includes(role)) .map((t) => extractID(t.tenant)) - .filter((id): id is Tenant['id'] => id !== null && id !== undefined) -} + .filter((id): id is Tenant["id"] => id !== null && id !== undefined); +}; /** * Get all tenant IDs for a user (regardless of role) */ -export const getAllUserTenantIDs = (user: User | null | undefined): Tenant['id'][] => { - if (!user?.tenants) return [] +export const getAllUserTenantIDs = ( + user: User | null | undefined, +): Tenant["id"][] => { + if (!user?.tenants) return []; return user.tenants .map((t) => extractID(t.tenant)) - .filter((id): id is Tenant['id'] => id !== null && id !== undefined) -} + .filter((id): id is Tenant["id"] => id !== null && id !== undefined); +}; /** * Check if user is a tenant admin in any tenant */ export const isTenantAdmin = (user: User | null | undefined): boolean => { - return hasTenantRole(user, 'admin') -} + return hasTenantRole(user, "admin"); +}; /** * Check if user is a caregiver in any tenant */ export const isCaregiver = (user: User | null | undefined): boolean => { - return hasTenantRole(user, 'caregiver') -} + return hasTenantRole(user, "caregiver"); +}; /** * Check if user is kitchen staff in any tenant */ export const isKitchenStaff = (user: User | null | undefined): boolean => { - return hasTenantRole(user, 'kitchen') -} + return hasTenantRole(user, "kitchen"); +}; /** * Check if user can access kitchen features (admin or kitchen role) */ export const canAccessKitchen = (user: User | null | undefined): boolean => { - return hasTenantRole(user, 'admin') || hasTenantRole(user, 'kitchen') -} + return hasTenantRole(user, "admin") || hasTenantRole(user, "kitchen"); +}; /** * Check if user can create meal orders (admin or caregiver role) */ export const canCreateOrders = (user: User | null | undefined): boolean => { - return hasTenantRole(user, 'admin') || hasTenantRole(user, 'caregiver') -} + return hasTenantRole(user, "admin") || hasTenantRole(user, "caregiver"); +}; diff --git a/src/app/(app)/caregiver/dashboard/page.tsx b/src/app/(app)/caregiver/dashboard/page.tsx index a131db0..f271887 100644 --- a/src/app/(app)/caregiver/dashboard/page.tsx +++ b/src/app/(app)/caregiver/dashboard/page.tsx @@ -1,9 +1,9 @@ -'use client' +"use client"; -import React, { useState, useEffect, useCallback } from 'react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { format, parseISO } from 'date-fns' +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { format, parseISO } from "date-fns"; import { LogOut, ClipboardList, @@ -19,20 +19,26 @@ import { UserCheck, UserX, Check, -} from 'lucide-react' +} from "lucide-react"; -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Progress } from '@/components/ui/progress' +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select' +} from "@/components/ui/select"; import { Table, TableBody, @@ -40,7 +46,7 @@ import { TableHead, TableHeader, TableRow, -} from '@/components/ui/table' +} from "@/components/ui/table"; import { Dialog, DialogContent, @@ -48,14 +54,14 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/components/ui/dialog' +} from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from '@/components/ui/tooltip' -import { cn } from '@/lib/utils' +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import { LoadingSpinner, @@ -64,76 +70,73 @@ import { StatCard, EmptyState, MealTypeSelector, -} from '@/components/caregiver' +} from "@/components/caregiver"; import { ORDER_STATUSES, getMealTypeLabel, type MealType, type OrderStatus, -} from '@/lib/constants/meal' - -interface User { - id: number - name?: string - email: string - tenants?: Array<{ - tenant: { id: number; name: string } | number - roles?: string[] - }> -} +} from "@/lib/constants/meal"; +import { useAuth } from "@/hooks/useAuth"; interface MealOrder { - id: number - title: string - date: string - mealType: MealType - status: OrderStatus - mealCount: number - createdAt: string + id: number; + title: string; + date: string; + mealType: MealType; + status: OrderStatus; + mealCount: number; + createdAt: string; } interface Resident { - id: number - name: string - room: string + id: number; + name: string; + room: string; } interface Meal { - id: number - resident: number | { id: number; name: string } + id: number; + resident: number | { id: number; name: string }; } interface OrderStats { - draft: number - submitted: number - preparing: number - completed: number - total: number + draft: number; + submitted: number; + preparing: number; + completed: number; + total: number; } interface PaginationInfo { - totalDocs: number - totalPages: number - page: number - limit: number - hasNextPage: boolean - hasPrevPage: boolean + totalDocs: number; + totalPages: number; + page: number; + limit: number; + hasNextPage: boolean; + hasPrevPage: boolean; } -const ITEMS_PER_PAGE = 10 +const ITEMS_PER_PAGE = 10; export default function CaregiverDashboardPage() { - const router = useRouter() - const [user, setUser] = useState(null) - const [orders, setOrders] = useState([]) - const [residents, setResidents] = useState([]) + const router = useRouter(); + const { + user, + loading: authLoading, + tenantName, + logout, + } = useAuth({ requiredRole: "caregiver" }); + + const [orders, setOrders] = useState([]); + const [residents, setResidents] = useState([]); const [stats, setStats] = useState({ draft: 0, submitted: 0, preparing: 0, completed: 0, total: 0, - }) + }); const [pagination, setPagination] = useState({ totalDocs: 0, totalPages: 1, @@ -141,39 +144,42 @@ export default function CaregiverDashboardPage() { limit: ITEMS_PER_PAGE, hasNextPage: false, hasPrevPage: false, - }) - const [loading, setLoading] = useState(true) - const [ordersLoading, setOrdersLoading] = useState(false) + }); + const [dataLoading, setDataLoading] = useState(true); + const [ordersLoading, setOrdersLoading] = useState(false); - const [dateFilter, setDateFilter] = useState('') - const [statusFilter, setStatusFilter] = useState('all') + const [dateFilter, setDateFilter] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); - const [coverageDialogOpen, setCoverageDialogOpen] = useState(false) - const [selectedOrder, setSelectedOrder] = useState(null) - const [orderMeals, setOrderMeals] = useState([]) - const [coverageLoading, setCoverageLoading] = useState(false) + const [coverageDialogOpen, setCoverageDialogOpen] = useState(false); + const [selectedOrder, setSelectedOrder] = useState(null); + const [orderMeals, setOrderMeals] = useState([]); + const [coverageLoading, setCoverageLoading] = useState(false); - const [createDialogOpen, setCreateDialogOpen] = useState(false) - const [newOrderDate, setNewOrderDate] = useState(() => format(new Date(), 'yyyy-MM-dd')) - const [newOrderMealType, setNewOrderMealType] = useState('breakfast') - const [creating, setCreating] = useState(false) + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [newOrderDate, setNewOrderDate] = useState(() => + format(new Date(), "yyyy-MM-dd"), + ); + const [newOrderMealType, setNewOrderMealType] = + useState("breakfast"); + const [creating, setCreating] = useState(false); const fetchOrders = useCallback( async (page: number = 1) => { - setOrdersLoading(true) + setOrdersLoading(true); try { - let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0` + let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0`; if (dateFilter) { - url += `&where[date][equals]=${dateFilter}` + url += `&where[date][equals]=${dateFilter}`; } - if (statusFilter !== 'all') { - url += `&where[status][equals]=${statusFilter}` + if (statusFilter !== "all") { + url += `&where[status][equals]=${statusFilter}`; } - const res = await fetch(url, { credentials: 'include' }) + const res = await fetch(url, { credentials: "include" }); if (res.ok) { - const data = await res.json() - setOrders(data.docs || []) + const data = await res.json(); + setOrders(data.docs || []); setPagination({ totalDocs: data.totalDocs || 0, totalPages: data.totalPages || 1, @@ -181,183 +187,186 @@ export default function CaregiverDashboardPage() { limit: data.limit || ITEMS_PER_PAGE, hasNextPage: data.hasNextPage || false, hasPrevPage: data.hasPrevPage || false, - }) + }); } else if (res.status === 401) { - router.push('/caregiver/login') + router.push("/login"); } } catch (err) { - console.error('Error fetching orders:', err) + console.error("Error fetching orders:", err); } finally { - setOrdersLoading(false) + setOrdersLoading(false); } }, [router, dateFilter, statusFilter], - ) + ); useEffect(() => { const fetchInitialData = async () => { + if (authLoading || !user) return; + try { - const userRes = await fetch('/api/users/me', { credentials: 'include' }) - if (!userRes.ok) { - router.push('/caregiver/login') - return - } - const userData = await userRes.json() - if (!userData.user) { - router.push('/caregiver/login') - return - } - setUser(userData.user) - - const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', { - credentials: 'include', - }) + const residentsRes = await fetch( + "/api/residents?where[active][equals]=true&limit=500", + { + credentials: "include", + }, + ); if (residentsRes.ok) { - const residentsData = await residentsRes.json() - setResidents(residentsData.docs || []) + const residentsData = await residentsRes.json(); + setResidents(residentsData.docs || []); } - const weekAgo = new Date() - weekAgo.setDate(weekAgo.getDate() - 7) + const weekAgo = new Date(); + weekAgo.setDate(weekAgo.getDate() - 7); const statsRes = await fetch( - `/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split('T')[0]}&limit=1000&depth=0`, - { credentials: 'include' }, - ) + `/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split("T")[0]}&limit=1000&depth=0`, + { credentials: "include" }, + ); if (statsRes.ok) { - const statsData = await statsRes.json() - const allOrders = statsData.docs || [] + const statsData = await statsRes.json(); + const allOrders = statsData.docs || []; setStats({ - draft: allOrders.filter((o: MealOrder) => o.status === 'draft').length, - submitted: allOrders.filter((o: MealOrder) => o.status === 'submitted').length, - preparing: allOrders.filter((o: MealOrder) => o.status === 'preparing').length, - completed: allOrders.filter((o: MealOrder) => o.status === 'completed').length, + draft: allOrders.filter((o: MealOrder) => o.status === "draft") + .length, + submitted: allOrders.filter( + (o: MealOrder) => o.status === "submitted", + ).length, + preparing: allOrders.filter( + (o: MealOrder) => o.status === "preparing", + ).length, + completed: allOrders.filter( + (o: MealOrder) => o.status === "completed", + ).length, total: allOrders.length, - }) + }); } } catch (error) { - console.error('Error fetching data:', error) + console.error("Error fetching data:", error); } finally { - setLoading(false) + setDataLoading(false); } - } - fetchInitialData() - }, [router]) + }; + fetchInitialData(); + }, [authLoading, user]); useEffect(() => { - if (!loading) { - fetchOrders(1) + if (!authLoading && user && !dataLoading) { + fetchOrders(1); } - }, [loading, fetchOrders]) - - const handleLogout = async () => { - await fetch('/api/users/logout', { - method: 'POST', - credentials: 'include', - }) - router.push('/caregiver/login') - } + }, [authLoading, user, dataLoading, fetchOrders]); const handleCreateOrder = async () => { - setCreating(true) + setCreating(true); try { - const res = await fetch('/api/meal-orders', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch("/api/meal-orders", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: newOrderDate, mealType: newOrderMealType, - status: 'draft', + status: "draft", }), - credentials: 'include', - }) + credentials: "include", + }); if (res.ok) { - const data = await res.json() - setCreateDialogOpen(false) - router.push(`/caregiver/orders/${data.doc.id}`) + const data = await res.json(); + setCreateDialogOpen(false); + router.push(`/caregiver/orders/${data.doc.id}`); } } catch (err) { - console.error('Error creating order:', err) + console.error("Error creating order:", err); } finally { - setCreating(false) + setCreating(false); } - } + }; const handleViewCoverage = async (order: MealOrder) => { - setSelectedOrder(order) - setCoverageDialogOpen(true) - setCoverageLoading(true) + setSelectedOrder(order); + setCoverageDialogOpen(true); + setCoverageLoading(true); try { - const res = await fetch(`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, { - credentials: 'include', - }) + const res = await fetch( + `/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, + { + credentials: "include", + }, + ); if (res.ok) { - const data = await res.json() - setOrderMeals(data.docs || []) + const data = await res.json(); + setOrderMeals(data.docs || []); } } catch (err) { - console.error('Error fetching meals:', err) + console.error("Error fetching meals:", err); } finally { - setCoverageLoading(false) + setCoverageLoading(false); } - } + }; const getCoverageInfo = (order: MealOrder) => { - const totalResidents = residents.length - const coveredCount = order.mealCount - const percentage = totalResidents > 0 ? Math.round((coveredCount / totalResidents) * 100) : 0 - return { coveredCount, totalResidents, percentage } - } + const totalResidents = residents.length; + const coveredCount = order.mealCount; + const percentage = + totalResidents > 0 + ? Math.round((coveredCount / totalResidents) * 100) + : 0; + return { coveredCount, totalResidents, percentage }; + }; const getCoverageColor = (percentage: number) => { - if (percentage === 100) return 'bg-green-500' - if (percentage >= 75) return 'bg-blue-500' - if (percentage >= 50) return 'bg-yellow-500' - return 'bg-red-500' - } + if (percentage === 100) return "bg-green-500"; + if (percentage >= 75) return "bg-blue-500"; + if (percentage >= 50) return "bg-yellow-500"; + return "bg-red-500"; + }; const formatDate = (dateStr: string) => { - return format(parseISO(dateStr), 'EEE, MMM d') - } + return format(parseISO(dateStr), "EEE, MMM d"); + }; - if (loading) { - return + if (authLoading || dataLoading) { + return ; } - const tenantName = - user?.tenants?.[0]?.tenant && typeof user.tenants[0].tenant === 'object' - ? user.tenants[0].tenant.name - : 'Care Home' - const coveredResidentIds = new Set( - orderMeals.map((m) => (typeof m.resident === 'object' ? m.resident.id : m.resident)), - ) - const coveredResidents = residents.filter((r) => coveredResidentIds.has(r.id)) - const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id)) + orderMeals.map((m) => + typeof m.resident === "object" ? m.resident.id : m.resident, + ), + ); + const coveredResidents = residents.filter((r) => + coveredResidentIds.has(r.id), + ); + const uncoveredResidents = residents.filter( + (r) => !coveredResidentIds.has(r.id), + ); return (
-
-

{tenantName}

-
- {user?.name || user?.email} -
-
-
+
+
-

Dashboard

-

Manage meal orders for your care home

+

Dashboard

+

+ Manage meal orders for your care home +

- @@ -430,7 +439,8 @@ export default function CaregiverDashboardPage() {
Meal Orders - View and manage all meal orders. Click on coverage to see resident details. + View and manage all meal orders. Click on coverage to see + resident details.
@@ -458,13 +468,13 @@ export default function CaregiverDashboardPage() { ))} - {(dateFilter || statusFilter !== 'all') && ( + {(dateFilter || statusFilter !== "all") && ( - ) + ); })} @@ -573,9 +589,12 @@ export default function CaregiverDashboardPage() { {pagination.totalPages > 1 && (
- Showing {(pagination.page - 1) * pagination.limit + 1} to{' '} - {Math.min(pagination.page * pagination.limit, pagination.totalDocs)} of{' '} - {pagination.totalDocs} orders + Showing {(pagination.page - 1) * pagination.limit + 1} to{" "} + {Math.min( + pagination.page * pagination.limit, + pagination.totalDocs, + )}{" "} + of {pagination.totalDocs} orders
- ) - })} + {Array.from( + { length: Math.min(5, pagination.totalPages) }, + (_, i) => { + let pageNum: number; + if (pagination.totalPages <= 5) { + pageNum = i + 1; + } else if (pagination.page <= 3) { + pageNum = i + 1; + } else if ( + pagination.page >= + pagination.totalPages - 2 + ) { + pageNum = pagination.totalPages - 4 + i; + } else { + pageNum = pagination.page - 2 + i; + } + return ( + + ); + }, + )}
@@ -678,7 +716,8 @@ export default function CaregiverDashboardPage() { {selectedOrder && ( <> - {getMealTypeLabel(selectedOrder.mealType)} - {formatDate(selectedOrder.date)} + {getMealTypeLabel(selectedOrder.mealType)} -{" "} + {formatDate(selectedOrder.date)} )} @@ -691,21 +730,25 @@ export default function CaregiverDashboardPage() {
- Coverage Progress + + Coverage Progress + {coveredResidents.length}/{residents.length} residents
0 ? (coveredResidents.length / residents.length) * 100 : 0 + residents.length > 0 + ? (coveredResidents.length / residents.length) * 100 + : 0 } className="h-3" />
0 ? (coveredResidents.length / residents.length) * 100 @@ -714,7 +757,9 @@ export default function CaregiverDashboardPage() { )} > {residents.length > 0 - ? Math.round((coveredResidents.length / residents.length) * 100) + ? Math.round( + (coveredResidents.length / residents.length) * 100, + ) : 0} %
@@ -735,8 +780,12 @@ export default function CaregiverDashboardPage() { className="flex items-center justify-between p-3 bg-red-50 border border-red-200 rounded-lg" >
-
{resident.name}
-
Room {resident.room}
+
+ {resident.name} +
+
+ Room {resident.room} +
@@ -760,8 +809,12 @@ export default function CaregiverDashboardPage() { className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg" >
-
{resident.name}
-
Room {resident.room}
+
+ {resident.name} +
+
+ Room {resident.room} +
@@ -773,7 +826,7 @@ export default function CaregiverDashboardPage() { )} - {selectedOrder?.status === 'draft' && ( + {selectedOrder?.status === "draft" && ( )} -
- ) + ); } diff --git a/src/app/(app)/caregiver/login/page.tsx b/src/app/(app)/caregiver/login/page.tsx index ce6bcd4..b717357 100644 --- a/src/app/(app)/caregiver/login/page.tsx +++ b/src/app/(app)/caregiver/login/page.tsx @@ -1,154 +1,5 @@ -'use client' - -import React, { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { Loader2 } from 'lucide-react' - -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Alert, AlertDescription } from '@/components/ui/alert' +import { redirect } from "next/navigation"; export default function CaregiverLoginPage() { - const router = useRouter() - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState(null) - const [loading, setLoading] = useState(false) - const [checking, setChecking] = useState(true) - - useEffect(() => { - const checkAuth = async () => { - try { - const res = await fetch('/api/users/me', { credentials: 'include' }) - if (res.ok) { - const data = await res.json() - if (data.user) { - router.push('/caregiver/dashboard') - return - } - } - } catch { - // Not logged in - } - setChecking(false) - } - checkAuth() - }, [router]) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError(null) - setLoading(true) - - try { - const res = await fetch('/api/users/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - credentials: 'include', - }) - - const data = await res.json() - - if (!res.ok) { - throw new Error(data.errors?.[0]?.message || 'Login failed') - } - - const user = data.user - const hasCaregiverRole = - user?.roles?.includes('super-admin') || - user?.tenants?.some( - (t: { roles?: string[] }) => - t.roles?.includes('caregiver') || t.roles?.includes('admin'), - ) - - if (!hasCaregiverRole) { - await fetch('/api/users/logout', { - method: 'POST', - credentials: 'include', - }) - throw new Error('You do not have caregiver access') - } - - router.push('/caregiver/dashboard') - } catch (err) { - setError(err instanceof Error ? err.message : 'Login failed') - } finally { - setLoading(false) - } - } - - if (checking) { - return ( -
- -
- ) - } - - return ( -
-
-
-

Meal Planner

-

Caregiver Portal

-
- - - - Login - Enter your credentials to access the caregiver portal - - - {error && ( - - {error} - - )} - -
-
- - setEmail(e.target.value)} - placeholder="Enter your email" - required - autoComplete="email" - /> -
- -
- - setPassword(e.target.value)} - placeholder="Enter your password" - required - autoComplete="current-password" - /> -
- - -
-
-
-
-
- ) + redirect("/login"); } diff --git a/src/app/(app)/caregiver/orders/[id]/page.tsx b/src/app/(app)/caregiver/orders/[id]/page.tsx index 4a9dcca..bf0c291 100644 --- a/src/app/(app)/caregiver/orders/[id]/page.tsx +++ b/src/app/(app)/caregiver/orders/[id]/page.tsx @@ -1,9 +1,9 @@ -'use client' +"use client"; -import React, { useState, useEffect, useCallback } from 'react' -import { useRouter, useParams } from 'next/navigation' -import Link from 'next/link' -import { formatISO, format, parseISO } from 'date-fns' +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; +import { formatISO, format, parseISO } from "date-fns"; import { Search, Plus, @@ -22,16 +22,22 @@ import { Sparkles, Camera, Loader2, -} from 'lucide-react' +} from "lucide-react"; -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Badge } from '@/components/ui/badge' -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Separator } from '@/components/ui/separator' +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Separator } from "@/components/ui/separator"; import { Table, TableBody, @@ -39,7 +45,7 @@ import { TableHead, TableHeader, TableRow, -} from '@/components/ui/table' +} from "@/components/ui/table"; import { Dialog, DialogContent, @@ -47,14 +53,14 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/components/ui/dialog' +} from "@/components/ui/dialog"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, -} from '@/components/ui/sheet' +} from "@/components/ui/sheet"; import { AlertDialog, AlertDialogAction, @@ -64,8 +70,8 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { cn } from '@/lib/utils' +} from "@/components/ui/alert-dialog"; +import { cn } from "@/lib/utils"; import { PageHeader, @@ -73,8 +79,12 @@ import { StatusBadge, StatCard, CheckboxOption, -} from '@/components/caregiver' -import { getMealTypeLabel, type MealType, type OrderStatus } from '@/lib/constants/meal' +} from "@/components/caregiver"; +import { + getMealTypeLabel, + type MealType, + type OrderStatus, +} from "@/lib/constants/meal"; import { type BreakfastOptions, type LunchOptions, @@ -86,269 +96,296 @@ import { LUNCH_CONFIG, DINNER_CONFIG, getGridColsClass, -} from '@/lib/constants/meal-options' +} from "@/lib/constants/meal-options"; interface Resident { - id: number - name: string - room: string - table?: string - station?: string - highCaloric?: boolean - aversions?: string - notes?: string + id: number; + name: string; + room: string; + table?: string; + station?: string; + highCaloric?: boolean; + aversions?: string; + notes?: string; } interface MediaFile { - id: number - url: string - alt?: string - filename?: string - thumbnailURL?: string + id: number; + url: string; + alt?: string; + filename?: string; + thumbnailURL?: string; } interface Meal { - id: number - resident: Resident | number - mealType: MealType - status: 'pending' | 'preparing' | 'prepared' - formImage?: MediaFile | number - breakfast?: BreakfastOptions - lunch?: LunchOptions - dinner?: DinnerOptions + id: number; + resident: Resident | number; + mealType: MealType; + status: "pending" | "preparing" | "prepared"; + formImage?: MediaFile | number; + breakfast?: BreakfastOptions; + lunch?: LunchOptions; + dinner?: DinnerOptions; } interface MealOrder { - id: number - title: string - date: string - mealType: MealType - status: OrderStatus - mealCount: number - notes?: string + id: number; + title: string; + date: string; + mealType: MealType; + status: OrderStatus; + mealCount: number; + notes?: string; } export default function OrderDetailPage() { - const router = useRouter() - const params = useParams() - const orderId = params.id as string + const router = useRouter(); + const params = useParams(); + const orderId = params.id as string; - const [order, setOrder] = useState(null) - const [meals, setMeals] = useState([]) - const [residents, setResidents] = useState([]) - const [loading, setLoading] = useState(true) - const [searchQuery, setSearchQuery] = useState('') - const [showSummary, setShowSummary] = useState(false) - const [submitting, setSubmitting] = useState(false) + const [order, setOrder] = useState(null); + const [meals, setMeals] = useState([]); + const [residents, setResidents] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [showSummary, setShowSummary] = useState(false); + const [submitting, setSubmitting] = useState(false); - const [editingMeal, setEditingMeal] = useState(null) - const [selectedResident, setSelectedResident] = useState(null) - const [showMealForm, setShowMealForm] = useState(false) - const [breakfast, setBreakfast] = useState(DEFAULT_BREAKFAST) - const [lunch, setLunch] = useState(DEFAULT_LUNCH) - const [dinner, setDinner] = useState(DEFAULT_DINNER) - const [savingMeal, setSavingMeal] = useState(false) + const [editingMeal, setEditingMeal] = useState(null); + const [selectedResident, setSelectedResident] = useState( + null, + ); + const [showMealForm, setShowMealForm] = useState(false); + const [breakfast, setBreakfast] = + useState(DEFAULT_BREAKFAST); + const [lunch, setLunch] = useState(DEFAULT_LUNCH); + const [dinner, setDinner] = useState(DEFAULT_DINNER); + const [savingMeal, setSavingMeal] = useState(false); - const [formImageFile, setFormImageFile] = useState(null) - const [formImagePreview, setFormImagePreview] = useState(null) - const [formImageId, setFormImageId] = useState(null) - const [existingFormImage, setExistingFormImage] = useState(null) - const [uploadingImage, setUploadingImage] = useState(false) - const [analyzingImage, setAnalyzingImage] = useState(false) - const [analysisError, setAnalysisError] = useState(null) + const [formImageFile, setFormImageFile] = useState(null); + const [formImagePreview, setFormImagePreview] = useState(null); + const [formImageId, setFormImageId] = useState(null); + const [existingFormImage, setExistingFormImage] = useState( + null, + ); + const [uploadingImage, setUploadingImage] = useState(false); + const [analyzingImage, setAnalyzingImage] = useState(false); + const [analysisError, setAnalysisError] = useState(null); - const [mealToDelete, setMealToDelete] = useState(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [deletingMeal, setDeletingMeal] = useState(false) + const [mealToDelete, setMealToDelete] = useState(null); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [deletingMeal, setDeletingMeal] = useState(false); const fetchData = useCallback(async () => { - setLoading(true) + setLoading(true); try { - const orderRes = await fetch(`/api/meal-orders/${orderId}`, { credentials: 'include' }) + const orderRes = await fetch(`/api/meal-orders/${orderId}`, { + credentials: "include", + }); if (!orderRes.ok) { if (orderRes.status === 401) { - router.push('/caregiver/login') - return + router.push("/login"); + return; } - throw new Error('Failed to fetch order') + throw new Error("Failed to fetch order"); } - const orderData = await orderRes.json() - setOrder(orderData) + const orderData = await orderRes.json(); + setOrder(orderData); - const mealsRes = await fetch(`/api/meals?where[order][equals]=${orderId}&depth=1&limit=100`, { - credentials: 'include', - }) + const mealsRes = await fetch( + `/api/meals?where[order][equals]=${orderId}&depth=1&limit=100`, + { + credentials: "include", + }, + ); if (mealsRes.ok) { - const mealsData = await mealsRes.json() - setMeals(mealsData.docs || []) + const mealsData = await mealsRes.json(); + setMeals(mealsData.docs || []); } const residentsRes = await fetch( - '/api/residents?where[active][equals]=true&limit=100&sort=name', - { credentials: 'include' }, - ) + "/api/residents?where[active][equals]=true&limit=100&sort=name", + { credentials: "include" }, + ); if (residentsRes.ok) { - const residentsData = await residentsRes.json() - setResidents(residentsData.docs || []) + const residentsData = await residentsRes.json(); + setResidents(residentsData.docs || []); } } catch (err) { - console.error('Error fetching data:', err) + console.error("Error fetching data:", err); } finally { - setLoading(false) + setLoading(false); } - }, [orderId, router]) + }, [orderId, router]); useEffect(() => { - fetchData() - }, [fetchData]) + fetchData(); + }, [fetchData]); const getResidentMeal = (residentId: number): Meal | undefined => { return meals.find((meal) => { - const mealResidentId = typeof meal.resident === 'object' ? meal.resident.id : meal.resident - return mealResidentId === residentId - }) - } + const mealResidentId = + typeof meal.resident === "object" ? meal.resident.id : meal.resident; + return mealResidentId === residentId; + }); + }; const filteredResidents = residents.filter( (r) => r.name.toLowerCase().includes(searchQuery.toLowerCase()) || r.room.toLowerCase().includes(searchQuery.toLowerCase()), - ) + ); - const coveredResidents = residents.filter((r) => getResidentMeal(r.id)) - const uncoveredResidents = residents.filter((r) => !getResidentMeal(r.id)) + const coveredResidents = residents.filter((r) => getResidentMeal(r.id)); + const uncoveredResidents = residents.filter((r) => !getResidentMeal(r.id)); const coveragePercent = - residents.length > 0 ? Math.round((coveredResidents.length / residents.length) * 100) : 0 + residents.length > 0 + ? Math.round((coveredResidents.length / residents.length) * 100) + : 0; const openMealForm = (resident: Resident, existingMeal?: Meal) => { - setSelectedResident(resident) - setEditingMeal(existingMeal || null) + setSelectedResident(resident); + setEditingMeal(existingMeal || null); if (existingMeal) { - if (existingMeal.breakfast) setBreakfast(existingMeal.breakfast) - if (existingMeal.lunch) setLunch(existingMeal.lunch) - if (existingMeal.dinner) setDinner(existingMeal.dinner) - if (existingMeal.formImage && typeof existingMeal.formImage === 'object') { - setExistingFormImage(existingMeal.formImage) - setFormImageId(existingMeal.formImage.id) - } else if (typeof existingMeal.formImage === 'number') { - setFormImageId(existingMeal.formImage) + if (existingMeal.breakfast) setBreakfast(existingMeal.breakfast); + if (existingMeal.lunch) setLunch(existingMeal.lunch); + if (existingMeal.dinner) setDinner(existingMeal.dinner); + if ( + existingMeal.formImage && + typeof existingMeal.formImage === "object" + ) { + setExistingFormImage(existingMeal.formImage); + setFormImageId(existingMeal.formImage.id); + } else if (typeof existingMeal.formImage === "number") { + setFormImageId(existingMeal.formImage); } } else { - setBreakfast(DEFAULT_BREAKFAST) - setLunch(DEFAULT_LUNCH) - setDinner(DEFAULT_DINNER) + setBreakfast(DEFAULT_BREAKFAST); + setLunch(DEFAULT_LUNCH); + setDinner(DEFAULT_DINNER); } - setShowMealForm(true) - } + setShowMealForm(true); + }; const closeMealForm = () => { - setShowMealForm(false) - setSelectedResident(null) - setEditingMeal(null) - setFormImageFile(null) - setFormImagePreview(null) - setFormImageId(null) - setExistingFormImage(null) - setAnalysisError(null) - } + setShowMealForm(false); + setSelectedResident(null); + setEditingMeal(null); + setFormImageFile(null); + setFormImagePreview(null); + setFormImageId(null); + setExistingFormImage(null); + setAnalysisError(null); + }; const handleImageSelect = (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file) return + const file = e.target.files?.[0]; + if (!file) return; - setFormImageFile(file) - setExistingFormImage(null) - setAnalysisError(null) + setFormImageFile(file); + setExistingFormImage(null); + setAnalysisError(null); - const reader = new FileReader() + const reader = new FileReader(); reader.onload = (event) => { - setFormImagePreview(event.target?.result as string) - } - reader.readAsDataURL(file) - } + setFormImagePreview(event.target?.result as string); + }; + reader.readAsDataURL(file); + }; const handleRemoveImage = () => { - setFormImageFile(null) - setFormImagePreview(null) - setFormImageId(null) - setExistingFormImage(null) - setAnalysisError(null) - } + setFormImageFile(null); + setFormImagePreview(null); + setFormImageId(null); + setExistingFormImage(null); + setAnalysisError(null); + }; const handleUploadImage = async (): Promise => { - if (!formImageFile) return formImageId + if (!formImageFile) return formImageId; - setUploadingImage(true) + setUploadingImage(true); try { - const formData = new FormData() - formData.append('file', formImageFile) - formData.append('alt', `Meal order form for ${selectedResident?.name}`) + const formData = new FormData(); + formData.append("file", formImageFile); + formData.append("alt", `Meal order form for ${selectedResident?.name}`); - const res = await fetch('/api/media', { - method: 'POST', + const res = await fetch("/api/media", { + method: "POST", body: formData, - credentials: 'include', - }) + credentials: "include", + }); if (res.ok) { - const data = await res.json() - setFormImageId(data.doc.id) - return data.doc.id + const data = await res.json(); + setFormImageId(data.doc.id); + return data.doc.id; } - return null + return null; } catch (err) { - console.error('Error uploading image:', err) - return null + console.error("Error uploading image:", err); + return null; } finally { - setUploadingImage(false) + setUploadingImage(false); } - } + }; const handleAnalyzeImage = async () => { - if (!formImagePreview && !existingFormImage) return + if (!formImagePreview && !existingFormImage) return; - setAnalyzingImage(true) - setAnalysisError(null) + setAnalyzingImage(true); + setAnalysisError(null); try { - const imageData: { imageBase64?: string; imageUrl?: string } = {} + const imageData: { imageBase64?: string; imageUrl?: string } = {}; if (formImagePreview) { - const base64Match = formImagePreview.match(/^data:image\/[^;]+;base64,(.+)$/) + const base64Match = formImagePreview.match( + /^data:image\/[^;]+;base64,(.+)$/, + ); if (base64Match) { - imageData.imageBase64 = base64Match[1] + imageData.imageBase64 = base64Match[1]; } } else if (existingFormImage?.url) { - imageData.imageUrl = existingFormImage.url + imageData.imageUrl = existingFormImage.url; } - const res = await fetch('/api/analyze-form', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch("/api/analyze-form", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...imageData, mealType: order?.mealType, }), - credentials: 'include', - }) + credentials: "include", + }); if (res.ok) { - const analysis = await res.json() + const analysis = await res.json(); if (analysis.confidence > 50) { - if (order?.mealType === 'breakfast' && analysis.breakfast) { + if (order?.mealType === "breakfast" && analysis.breakfast) { setBreakfast((prev) => ({ ...prev, ...analysis.breakfast, bread: { ...prev.bread, ...analysis.breakfast?.bread }, - preparation: { ...prev.preparation, ...analysis.breakfast?.preparation }, + preparation: { + ...prev.preparation, + ...analysis.breakfast?.preparation, + }, spreads: { ...prev.spreads, ...analysis.breakfast?.spreads }, - beverages: { ...prev.beverages, ...analysis.breakfast?.beverages }, - additions: { ...prev.additions, ...analysis.breakfast?.additions }, - })) - } else if (order?.mealType === 'lunch' && analysis.lunch) { + beverages: { + ...prev.beverages, + ...analysis.breakfast?.beverages, + }, + additions: { + ...prev.additions, + ...analysis.breakfast?.additions, + }, + })); + } else if (order?.mealType === "lunch" && analysis.lunch) { setLunch((prev) => ({ ...prev, ...analysis.lunch, @@ -356,44 +393,50 @@ export default function OrderDetailPage() { ...prev.specialPreparations, ...analysis.lunch?.specialPreparations, }, - restrictions: { ...prev.restrictions, ...analysis.lunch?.restrictions }, - })) - } else if (order?.mealType === 'dinner' && analysis.dinner) { + restrictions: { + ...prev.restrictions, + ...analysis.lunch?.restrictions, + }, + })); + } else if (order?.mealType === "dinner" && analysis.dinner) { setDinner((prev) => ({ ...prev, ...analysis.dinner, bread: { ...prev.bread, ...analysis.dinner?.bread }, - preparation: { ...prev.preparation, ...analysis.dinner?.preparation }, + preparation: { + ...prev.preparation, + ...analysis.dinner?.preparation, + }, spreads: { ...prev.spreads, ...analysis.dinner?.spreads }, beverages: { ...prev.beverages, ...analysis.dinner?.beverages }, additions: { ...prev.additions, ...analysis.dinner?.additions }, - })) + })); } } else { setAnalysisError( `Low confidence (${analysis.confidence}%). Please check the options manually.`, - ) + ); } } else { - const error = await res.json() - setAnalysisError(error.error || 'Failed to analyze image') + const error = await res.json(); + setAnalysisError(error.error || "Failed to analyze image"); } } catch (err) { - console.error('Error analyzing image:', err) - setAnalysisError('Failed to analyze the form image') + console.error("Error analyzing image:", err); + setAnalysisError("Failed to analyze the form image"); } finally { - setAnalyzingImage(false) + setAnalyzingImage(false); } - } + }; const handleSaveMeal = async () => { - if (!selectedResident || !order) return + if (!selectedResident || !order) return; - setSavingMeal(true) + setSavingMeal(true); try { - let imageId = formImageId + let imageId = formImageId; if (formImageFile) { - imageId = await handleUploadImage() + imageId = await handleUploadImage(); } const mealData: Record = { @@ -401,153 +444,155 @@ export default function OrderDetailPage() { resident: selectedResident.id, date: order.date, mealType: order.mealType, - status: 'pending', + status: "pending", formImage: imageId || undefined, + }; + + if (order.mealType === "breakfast") { + mealData.breakfast = breakfast; + } else if (order.mealType === "lunch") { + mealData.lunch = lunch; + } else if (order.mealType === "dinner") { + mealData.dinner = dinner; } - if (order.mealType === 'breakfast') { - mealData.breakfast = breakfast - } else if (order.mealType === 'lunch') { - mealData.lunch = lunch - } else if (order.mealType === 'dinner') { - mealData.dinner = dinner - } - - let res + let res; if (editingMeal) { res = await fetch(`/api/meals/${editingMeal.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + method: "PATCH", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(mealData), - credentials: 'include', - }) + credentials: "include", + }); } else { - res = await fetch('/api/meals', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + res = await fetch("/api/meals", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(mealData), - credentials: 'include', - }) + credentials: "include", + }); } if (res.ok) { await fetch(`/api/meal-orders/${order.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mealCount: editingMeal ? meals.length : meals.length + 1 }), - credentials: 'include', - }) + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mealCount: editingMeal ? meals.length : meals.length + 1, + }), + credentials: "include", + }); - closeMealForm() - fetchData() + closeMealForm(); + fetchData(); } } catch (err) { - console.error('Error saving meal:', err) + console.error("Error saving meal:", err); } finally { - setSavingMeal(false) + setSavingMeal(false); } - } + }; const handleDeleteMeal = async (mealId: number) => { - setMealToDelete(mealId) - setShowDeleteDialog(true) - } + setMealToDelete(mealId); + setShowDeleteDialog(true); + }; const confirmDeleteMeal = async () => { - if (!order || !mealToDelete) return + if (!order || !mealToDelete) return; - setDeletingMeal(true) + setDeletingMeal(true); try { const res = await fetch(`/api/meals/${mealToDelete}`, { - method: 'DELETE', - credentials: 'include', - }) + method: "DELETE", + credentials: "include", + }); if (res.ok) { await fetch(`/api/meal-orders/${order.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + method: "PATCH", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mealCount: meals.length - 1 }), - credentials: 'include', - }) - fetchData() + credentials: "include", + }); + fetchData(); } } catch (err) { - console.error('Error deleting meal:', err) + console.error("Error deleting meal:", err); } finally { - setDeletingMeal(false) - setShowDeleteDialog(false) - setMealToDelete(null) + setDeletingMeal(false); + setShowDeleteDialog(false); + setMealToDelete(null); } - } + }; const handleSubmitToKitchen = async () => { - if (!order || meals.length === 0) return + if (!order || meals.length === 0) return; - setSubmitting(true) + setSubmitting(true); try { const res = await fetch(`/api/meal-orders/${order.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + method: "PATCH", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - status: 'submitted', + status: "submitted", submittedAt: formatISO(new Date()), }), - credentials: 'include', - }) + credentials: "include", + }); if (res.ok) { - setShowSummary(false) - router.push('/caregiver/orders') + setShowSummary(false); + router.push("/caregiver/orders"); } } catch (err) { - console.error('Error submitting order:', err) + console.error("Error submitting order:", err); } finally { - setSubmitting(false) + setSubmitting(false); } - } + }; const getMealSummary = (meal: Meal) => { - const items: string[] = [] + const items: string[] = []; if (meal.breakfast) { - if (meal.breakfast.accordingToPlan) items.push('According to Plan') + if (meal.breakfast.accordingToPlan) items.push("According to Plan"); const breads = Object.entries(meal.breakfast.bread) .filter(([, v]) => v) - .map(([k]) => k) - if (breads.length) items.push(`Bread: ${breads.length} types`) - if (meal.breakfast.porridge) items.push('Porridge') + .map(([k]) => k); + if (breads.length) items.push(`Bread: ${breads.length} types`); + if (meal.breakfast.porridge) items.push("Porridge"); const beverages = Object.entries(meal.breakfast.beverages) .filter(([, v]) => v) - .map(([k]) => k) - if (beverages.length) items.push(`Beverages: ${beverages.join(', ')}`) + .map(([k]) => k); + if (beverages.length) items.push(`Beverages: ${beverages.join(", ")}`); } if (meal.lunch) { - items.push(`Portion: ${meal.lunch.portionSize}`) - if (meal.lunch.soup) items.push('Soup') - if (meal.lunch.dessert) items.push('Dessert') + items.push(`Portion: ${meal.lunch.portionSize}`); + if (meal.lunch.soup) items.push("Soup"); + if (meal.lunch.dessert) items.push("Dessert"); const preps = Object.entries(meal.lunch.specialPreparations) .filter(([, v]) => v) - .map(([k]) => k) - if (preps.length) items.push(`Special: ${preps.join(', ')}`) + .map(([k]) => k); + if (preps.length) items.push(`Special: ${preps.join(", ")}`); } if (meal.dinner) { - if (meal.dinner.accordingToPlan) items.push('According to Plan') + if (meal.dinner.accordingToPlan) items.push("According to Plan"); const breads = Object.entries(meal.dinner.bread) .filter(([, v]) => v) - .map(([k]) => k) - if (breads.length) items.push(`Bread: ${breads.length} types`) - if (meal.dinner.soup) items.push('Soup') - if (meal.dinner.porridge) items.push('Porridge') + .map(([k]) => k); + if (breads.length) items.push(`Bread: ${breads.length} types`); + if (meal.dinner.soup) items.push("Soup"); + if (meal.dinner.porridge) items.push("Porridge"); } - return items.length > 0 ? items.slice(0, 3).join(', ') : 'Basic meal' - } + return items.length > 0 ? items.slice(0, 3).join(", ") : "Basic meal"; + }; if (loading) { - return + return ; } if (!order) { @@ -566,16 +611,16 @@ export default function OrderDetailPage() {
- ) + ); } - const isDraft = order.status === 'draft' + const isDraft = order.status === "draft"; return (
} /> @@ -619,8 +664,8 @@ export default function OrderDetailPage() {
@@ -635,8 +680,8 @@ export default function OrderDetailPage() { Residents {isDraft - ? 'Add meals for each resident before submitting to kitchen' - : 'View meals for all residents'} + ? "Add meals for each resident before submitting to kitchen" + : "View meals for all residents"}
{isDraft && meals.length > 0 && ( @@ -672,21 +717,29 @@ export default function OrderDetailPage() { {filteredResidents.map((resident) => { - const meal = getResidentMeal(resident.id) + const meal = getResidentMeal(resident.id); return ( - {resident.name} + + {resident.name} + {resident.room} - {resident.station || '-'} + {resident.station || "-"}
{resident.highCaloric && ( - + High Cal )} {resident.aversions && ( - + Aversions )} @@ -694,12 +747,18 @@ export default function OrderDetailPage() { {meal ? ( - + Meal Created ) : ( - + No Meal @@ -750,7 +809,7 @@ export default function OrderDetailPage() { ) : null} - ) + ); })} @@ -759,10 +818,14 @@ export default function OrderDetailPage() {
- + - {editingMeal ? 'Edit' : 'Add'} {getMealTypeLabel(order.mealType)} Meal + {editingMeal ? "Edit" : "Add"} {getMealTypeLabel(order.mealType)}{" "} + Meal {selectedResident?.name} - Room {selectedResident?.room} @@ -818,7 +881,9 @@ export default function OrderDetailPage() {
- {formImageFile?.name || existingFormImage?.filename || 'Uploaded image'} + {formImageFile?.name || + existingFormImage?.filename || + "Uploaded image"}
) : ( @@ -827,9 +892,12 @@ export default function OrderDetailPage() {
- Upload paper form photo + + Upload paper form photo +

- Take a photo of the paper meal order form to auto-fill options + Take a photo of the paper meal order form to auto-fill + options

Notes for {selectedResident?.name} {selectedResident?.highCaloric && ( -
High caloric requirement
+
+ High caloric requirement +
)} {selectedResident?.aversions && (
Aversions: {selectedResident.aversions}
)} - {selectedResident?.notes &&
{selectedResident.notes}
} + {selectedResident?.notes && ( +
{selectedResident.notes}
+ )}
)} - {order.mealType === 'breakfast' && ( + {order.mealType === "breakfast" && ( <>

General

@@ -903,7 +975,9 @@ export default function OrderDetailPage() { id="accordingToPlan" label="According to Plan (lt. Plan)" checked={breakfast.accordingToPlan} - onCheckedChange={(v) => setBreakfast({ ...breakfast, accordingToPlan: v })} + onCheckedChange={(v) => + setBreakfast({ ...breakfast, accordingToPlan: v }) + } />
@@ -912,14 +986,26 @@ export default function OrderDetailPage() {

{section.title}

-
+
{section.options.map(({ key: optKey, label }) => { - const isTopLevel = optKey === 'porridge' + const isTopLevel = optKey === "porridge"; const checked = isTopLevel ? breakfast.porridge - : key === 'preparation' && (optKey === 'sliced' || optKey === 'spread') - ? breakfast.preparation[optKey as keyof typeof breakfast.preparation] - : (breakfast[key as keyof BreakfastOptions] as Record)?.[optKey] + : key === "preparation" && + (optKey === "sliced" || optKey === "spread") + ? breakfast.preparation[ + optKey as keyof typeof breakfast.preparation + ] + : ( + breakfast[ + key as keyof BreakfastOptions + ] as Record + )?.[optKey]; return ( { if (isTopLevel) { - setBreakfast({ ...breakfast, porridge: v }) - } else if (key === 'preparation' && (optKey === 'sliced' || optKey === 'spread')) { + setBreakfast({ ...breakfast, porridge: v }); + } else if ( + key === "preparation" && + (optKey === "sliced" || optKey === "spread") + ) { setBreakfast({ ...breakfast, - preparation: { ...breakfast.preparation, [optKey]: v }, - }) + preparation: { + ...breakfast.preparation, + [optKey]: v, + }, + }); } else { setBreakfast({ ...breakfast, [key]: { - ...(breakfast[key as keyof BreakfastOptions] as Record), + ...(breakfast[ + key as keyof BreakfastOptions + ] as Record), [optKey]: v, }, - }) + }); } }} /> - ) + ); })}
@@ -954,14 +1048,17 @@ export default function OrderDetailPage() { )} - {order.mealType === 'lunch' && ( + {order.mealType === "lunch" && ( <>

Portion Size

- setLunch({ ...lunch, portionSize: v as 'small' | 'large' | 'vegetarian' }) + setLunch({ + ...lunch, + portionSize: v as "small" | "large" | "vegetarian", + }) } className="grid gap-2 grid-cols-1 sm:grid-cols-3" > @@ -969,15 +1066,20 @@ export default function OrderDetailPage() {
setLunch({ ...lunch, portionSize: value })} + onClick={() => + setLunch({ ...lunch, portionSize: value }) + } > -
@@ -987,35 +1089,27 @@ export default function OrderDetailPage() {
-

{LUNCH_CONFIG.mealOptions.title}

-
+

+ {LUNCH_CONFIG.mealOptions.title} +

+
{LUNCH_CONFIG.mealOptions.options.map(({ key, label }) => ( ]} - onCheckedChange={(v) => setLunch({ ...lunch, [key]: v })} - /> - ))} -
-
- - -
-

{LUNCH_CONFIG.specialPreparations.title}

-
- {LUNCH_CONFIG.specialPreparations.options.map(({ key, label }) => ( - + ] + } onCheckedChange={(v) => - setLunch({ - ...lunch, - specialPreparations: { ...lunch.specialPreparations, [key]: v }, - }) + setLunch({ ...lunch, [key]: v }) } /> ))} @@ -1024,16 +1118,69 @@ export default function OrderDetailPage() {
-

{LUNCH_CONFIG.restrictions.title}

-
+

+ {LUNCH_CONFIG.specialPreparations.title} +

+
+ {LUNCH_CONFIG.specialPreparations.options.map( + ({ key, label }) => ( + + setLunch({ + ...lunch, + specialPreparations: { + ...lunch.specialPreparations, + [key]: v, + }, + }) + } + /> + ), + )} +
+
+ + +
+

+ {LUNCH_CONFIG.restrictions.title} +

+
{LUNCH_CONFIG.restrictions.options.map(({ key, label }) => ( - setLunch({ ...lunch, restrictions: { ...lunch.restrictions, [key]: v } }) + setLunch({ + ...lunch, + restrictions: { ...lunch.restrictions, [key]: v }, + }) } /> ))} @@ -1042,7 +1189,7 @@ export default function OrderDetailPage() { )} - {order.mealType === 'dinner' && ( + {order.mealType === "dinner" && ( <>

General

@@ -1050,7 +1197,9 @@ export default function OrderDetailPage() { id="dinnerAccordingToPlan" label="According to Plan (lt. Plan)" checked={dinner.accordingToPlan} - onCheckedChange={(v) => setDinner({ ...dinner, accordingToPlan: v })} + onCheckedChange={(v) => + setDinner({ ...dinner, accordingToPlan: v }) + } />
@@ -1059,12 +1208,31 @@ export default function OrderDetailPage() {

{section.title}

-
+
{section.options.map(({ key: optKey, label }) => { - const isTopLevel = ['soup', 'porridge', 'noFish'].includes(optKey) + const isTopLevel = [ + "soup", + "porridge", + "noFish", + ].includes(optKey); const checked = isTopLevel - ? dinner[optKey as keyof Pick] - : (dinner[key as keyof DinnerOptions] as Record)?.[optKey] + ? dinner[ + optKey as keyof Pick< + DinnerOptions, + "soup" | "porridge" | "noFish" + > + ] + : ( + dinner[key as keyof DinnerOptions] as Record< + string, + boolean + > + )?.[optKey]; return ( { if (isTopLevel) { - setDinner({ ...dinner, [optKey]: v }) + setDinner({ ...dinner, [optKey]: v }); } else { setDinner({ ...dinner, [key]: { - ...(dinner[key as keyof DinnerOptions] as Record), + ...(dinner[ + key as keyof DinnerOptions + ] as Record), [optKey]: v, }, - }) + }); } }} /> - ) + ); })}
@@ -1097,10 +1267,18 @@ export default function OrderDetailPage() { {isDraft && (
- - @@ -1136,32 +1314,38 @@ export default function OrderDetailPage() { Missing Meals - {uncoveredResidents.length} residents don't have meals:{' '} - {uncoveredResidents.map((r) => r.name).join(', ')} + {uncoveredResidents.length} residents don't have meals:{" "} + {uncoveredResidents.map((r) => r.name).join(", ")} )}
{meals.map((meal) => { - const resident = typeof meal.resident === 'object' ? meal.resident : null + const resident = + typeof meal.resident === "object" ? meal.resident : null; return (
-
{resident?.name || 'Unknown'}
+
+ {resident?.name || "Unknown"} +
Room {resident?.room} - {getMealSummary(meal)}
- + Ready
- ) + ); })}
@@ -1196,11 +1380,14 @@ export default function OrderDetailPage() { Remove Meal - Are you sure you want to remove this meal? This action cannot be undone. + Are you sure you want to remove this meal? This action cannot be + undone. - Cancel + + Cancel + ) : ( - 'Remove' + "Remove" )}
- ) + ); } diff --git a/src/app/(app)/caregiver/orders/new/page.tsx b/src/app/(app)/caregiver/orders/new/page.tsx index add2ef4..df4ca21 100644 --- a/src/app/(app)/caregiver/orders/new/page.tsx +++ b/src/app/(app)/caregiver/orders/new/page.tsx @@ -1,9 +1,9 @@ -'use client' +"use client"; -import React, { useState, useEffect, Suspense } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import Link from 'next/link' -import { format } from 'date-fns' +import React, { useState, useEffect, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { format } from "date-fns"; import { ArrowLeft, Loader2, @@ -13,81 +13,96 @@ import { Moon, AlertTriangle, Check, -} from 'lucide-react' +} from "lucide-react"; -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Checkbox } from '@/components/ui/checkbox' -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Separator } from '@/components/ui/separator' -import { Badge } from '@/components/ui/badge' -import { cn } from '@/lib/utils' +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; interface Resident { - id: number - name: string - room: string - table?: string - station?: string - highCaloric?: boolean - aversions?: string - notes?: string + id: number; + name: string; + room: string; + table?: string; + station?: string; + highCaloric?: boolean; + aversions?: string; + notes?: string; } -type MealType = 'breakfast' | 'lunch' | 'dinner' +type MealType = "breakfast" | "lunch" | "dinner"; interface BreakfastOptions { - accordingToPlan: boolean + accordingToPlan: boolean; bread: { - breadRoll: boolean - wholeGrainRoll: boolean - greyBread: boolean - wholeGrainBread: boolean - whiteBread: boolean - crispbread: boolean - } - porridge: boolean - preparation: { sliced: boolean; spread: boolean } + breadRoll: boolean; + wholeGrainRoll: boolean; + greyBread: boolean; + wholeGrainBread: boolean; + whiteBread: boolean; + crispbread: boolean; + }; + porridge: boolean; + preparation: { sliced: boolean; spread: boolean }; spreads: { - butter: boolean - margarine: boolean - jam: boolean - diabeticJam: boolean - honey: boolean - cheese: boolean - quark: boolean - sausage: boolean - } - beverages: { coffee: boolean; tea: boolean; hotMilk: boolean; coldMilk: boolean } - additions: { sugar: boolean; sweetener: boolean; coffeeCreamer: boolean } + butter: boolean; + margarine: boolean; + jam: boolean; + diabeticJam: boolean; + honey: boolean; + cheese: boolean; + quark: boolean; + sausage: boolean; + }; + beverages: { + coffee: boolean; + tea: boolean; + hotMilk: boolean; + coldMilk: boolean; + }; + additions: { sugar: boolean; sweetener: boolean; coffeeCreamer: boolean }; } interface LunchOptions { - portionSize: 'small' | 'large' | 'vegetarian' - soup: boolean - dessert: boolean + portionSize: "small" | "large" | "vegetarian"; + soup: boolean; + dessert: boolean; specialPreparations: { - pureedFood: boolean - pureedMeat: boolean - slicedMeat: boolean - mashedPotatoes: boolean - } - restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean } + pureedFood: boolean; + pureedMeat: boolean; + slicedMeat: boolean; + mashedPotatoes: boolean; + }; + restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean }; } interface DinnerOptions { - accordingToPlan: boolean - bread: { greyBread: boolean; wholeGrainBread: boolean; whiteBread: boolean; crispbread: boolean } - preparation: { spread: boolean; sliced: boolean } - spreads: { butter: boolean; margarine: boolean } - soup: boolean - porridge: boolean - noFish: boolean - beverages: { tea: boolean; cocoa: boolean; hotMilk: boolean; coldMilk: boolean } - additions: { sugar: boolean; sweetener: boolean } + accordingToPlan: boolean; + bread: { + greyBread: boolean; + wholeGrainBread: boolean; + whiteBread: boolean; + crispbread: boolean; + }; + preparation: { spread: boolean; sliced: boolean }; + spreads: { butter: boolean; margarine: boolean }; + soup: boolean; + porridge: boolean; + noFish: boolean; + beverages: { + tea: boolean; + cocoa: boolean; + hotMilk: boolean; + coldMilk: boolean; + }; + additions: { sugar: boolean; sweetener: boolean }; } const defaultBreakfast: BreakfastOptions = { @@ -114,10 +129,10 @@ const defaultBreakfast: BreakfastOptions = { }, beverages: { coffee: false, tea: false, hotMilk: false, coldMilk: false }, additions: { sugar: false, sweetener: false, coffeeCreamer: false }, -} +}; const defaultLunch: LunchOptions = { - portionSize: 'large', + portionSize: "large", soup: false, dessert: true, specialPreparations: { @@ -127,11 +142,16 @@ const defaultLunch: LunchOptions = { mashedPotatoes: false, }, restrictions: { noFish: false, fingerFood: false, onlySweet: false }, -} +}; const defaultDinner: DinnerOptions = { accordingToPlan: false, - bread: { greyBread: false, wholeGrainBread: false, whiteBread: false, crispbread: false }, + bread: { + greyBread: false, + wholeGrainBread: false, + whiteBread: false, + crispbread: false, + }, preparation: { spread: false, sliced: false }, spreads: { butter: false, margarine: false }, soup: false, @@ -139,7 +159,7 @@ const defaultDinner: DinnerOptions = { noFish: false, beverages: { tea: false, cocoa: false, hotMilk: false, coldMilk: false }, additions: { sugar: false, sweetener: false }, -} +}; function CheckboxOption({ id, @@ -147,16 +167,18 @@ function CheckboxOption({ checked, onCheckedChange, }: { - id: string - label: string - checked: boolean - onCheckedChange: (checked: boolean) => void + id: string; + label: string; + checked: boolean; + onCheckedChange: (checked: boolean) => void; }) { return (
onCheckedChange(!checked)} > @@ -165,113 +187,119 @@ function CheckboxOption({ {label}
- ) + ); } function NewOrderContent() { - const router = useRouter() - const searchParams = useSearchParams() - const initialMealType = (searchParams.get('mealType') as MealType) || null + const router = useRouter(); + const searchParams = useSearchParams(); + const initialMealType = (searchParams.get("mealType") as MealType) || null; - const [step, setStep] = useState(initialMealType ? 2 : 1) - const [residents, setResidents] = useState([]) - const [selectedResident, setSelectedResident] = useState(null) - const [mealType, setMealType] = useState(initialMealType) - const [date, setDate] = useState(() => format(new Date(), 'yyyy-MM-dd')) - const [breakfast, setBreakfast] = useState(defaultBreakfast) - const [lunch, setLunch] = useState(defaultLunch) - const [dinner, setDinner] = useState(defaultDinner) - const [loading, setLoading] = useState(true) - const [submitting, setSubmitting] = useState(false) - const [error, setError] = useState(null) - const [searchQuery, setSearchQuery] = useState('') + const [step, setStep] = useState(initialMealType ? 2 : 1); + const [residents, setResidents] = useState([]); + const [selectedResident, setSelectedResident] = useState( + null, + ); + const [mealType, setMealType] = useState(initialMealType); + const [date, setDate] = useState(() => format(new Date(), "yyyy-MM-dd")); + const [breakfast, setBreakfast] = + useState(defaultBreakfast); + const [lunch, setLunch] = useState(defaultLunch); + const [dinner, setDinner] = useState(defaultDinner); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchResidents = async () => { try { - const res = await fetch('/api/residents?where[active][equals]=true&limit=100&sort=name', { - credentials: 'include', - }) + const res = await fetch( + "/api/residents?where[active][equals]=true&limit=100&sort=name", + { + credentials: "include", + }, + ); if (res.ok) { - const data = await res.json() - setResidents(data.docs || []) + const data = await res.json(); + setResidents(data.docs || []); } else if (res.status === 401) { - router.push('/caregiver/login') + router.push("/login"); } } catch (err) { - console.error('Error fetching residents:', err) + console.error("Error fetching residents:", err); } finally { - setLoading(false) + setLoading(false); } - } - fetchResidents() - }, [router]) + }; + fetchResidents(); + }, [router]); const filteredResidents = residents.filter( (r) => r.name.toLowerCase().includes(searchQuery.toLowerCase()) || r.room.toLowerCase().includes(searchQuery.toLowerCase()), - ) + ); const handleSubmit = async () => { - if (!selectedResident || !mealType || !date) return + if (!selectedResident || !mealType || !date) return; - setSubmitting(true) - setError(null) + setSubmitting(true); + setError(null); try { const orderData: Record = { resident: selectedResident.id, date, mealType, - status: 'pending', + status: "pending", + }; + + if (mealType === "breakfast") { + orderData.breakfast = breakfast; + } else if (mealType === "lunch") { + orderData.lunch = lunch; + } else if (mealType === "dinner") { + orderData.dinner = dinner; } - if (mealType === 'breakfast') { - orderData.breakfast = breakfast - } else if (mealType === 'lunch') { - orderData.lunch = lunch - } else if (mealType === 'dinner') { - orderData.dinner = dinner - } - - const res = await fetch('/api/meal-orders', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch("/api/meal-orders", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(orderData), - credentials: 'include', - }) + credentials: "include", + }); if (!res.ok) { - const data = await res.json() - throw new Error(data.errors?.[0]?.message || 'Failed to create order') + const data = await res.json(); + throw new Error(data.errors?.[0]?.message || "Failed to create order"); } - router.push('/caregiver/dashboard') + router.push("/caregiver/dashboard"); } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred') + setError(err instanceof Error ? err.message : "An error occurred"); } finally { - setSubmitting(false) + setSubmitting(false); } - } + }; const getMealTypeLabel = (type: MealType) => { switch (type) { - case 'breakfast': - return 'Breakfast (Frühstück)' - case 'lunch': - return 'Lunch (Mittagessen)' - case 'dinner': - return 'Dinner (Abendessen)' + case "breakfast": + return "Breakfast (Frühstück)"; + case "lunch": + return "Lunch (Mittagessen)"; + case "dinner": + return "Dinner (Abendessen)"; } - } + }; if (loading) { return (
- ) + ); } return ( @@ -295,8 +323,12 @@ function NewOrderContent() {
= s ? (step > s ? 'bg-green-500' : 'bg-primary') : 'bg-muted' + "h-2 flex-1 rounded-full transition-colors", + step >= s + ? step > s + ? "bg-green-500" + : "bg-primary" + : "bg-muted", )} /> ))} @@ -328,22 +360,44 @@ function NewOrderContent() {
{[ - { type: 'breakfast' as MealType, icon: Sunrise, label: 'Breakfast', sublabel: 'Frühstück', color: 'text-orange-500' }, - { type: 'lunch' as MealType, icon: Sun, label: 'Lunch', sublabel: 'Mittagessen', color: 'text-yellow-500' }, - { type: 'dinner' as MealType, icon: Moon, label: 'Dinner', sublabel: 'Abendessen', color: 'text-indigo-500' }, + { + type: "breakfast" as MealType, + icon: Sunrise, + label: "Breakfast", + sublabel: "Frühstück", + color: "text-orange-500", + }, + { + type: "lunch" as MealType, + icon: Sun, + label: "Lunch", + sublabel: "Mittagessen", + color: "text-yellow-500", + }, + { + type: "dinner" as MealType, + icon: Moon, + label: "Dinner", + sublabel: "Abendessen", + color: "text-indigo-500", + }, ].map(({ type, icon: Icon, label, sublabel, color }) => ( setMealType(type)} > - + {label} - {sublabel} + + {sublabel} + ))} @@ -384,8 +438,10 @@ function NewOrderContent() { setSelectedResident(resident)} > @@ -396,7 +452,10 @@ function NewOrderContent() { {resident.table && Table {resident.table}}
{resident.highCaloric && ( - + High Caloric )} @@ -426,7 +485,9 @@ function NewOrderContent() { {step === 3 && ( - Step 3: {mealType && getMealTypeLabel(mealType)} Options + + Step 3: {mealType && getMealTypeLabel(mealType)} Options + {(selectedResident?.aversions || selectedResident?.notes) && ( @@ -434,14 +495,18 @@ function NewOrderContent() { Notes for {selectedResident?.name} - {selectedResident?.aversions &&
Aversions: {selectedResident.aversions}
} - {selectedResident?.notes &&
{selectedResident.notes}
} + {selectedResident?.aversions && ( +
Aversions: {selectedResident.aversions}
+ )} + {selectedResident?.notes && ( +
{selectedResident.notes}
+ )}
)} {/* BREAKFAST OPTIONS */} - {mealType === 'breakfast' && ( + {mealType === "breakfast" && ( <>

General

@@ -449,7 +514,9 @@ function NewOrderContent() { id="accordingToPlan" label="According to Plan (lt. Plan)" checked={breakfast.accordingToPlan} - onCheckedChange={(v) => setBreakfast({ ...breakfast, accordingToPlan: v })} + onCheckedChange={(v) => + setBreakfast({ ...breakfast, accordingToPlan: v }) + } />
@@ -458,12 +525,72 @@ function NewOrderContent() {

Bread (Brot)

- setBreakfast({ ...breakfast, bread: { ...breakfast.bread, breadRoll: v } })} /> - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainRoll: v } })} /> - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, greyBread: v } })} /> - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainBread: v } })} /> - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, whiteBread: v } })} /> - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, crispbread: v } })} /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, breadRoll: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, wholeGrainRoll: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, greyBread: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, wholeGrainBread: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, whiteBread: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + bread: { ...breakfast.bread, crispbread: v }, + }) + } + />
@@ -472,9 +599,42 @@ function NewOrderContent() {

Preparation

- setBreakfast({ ...breakfast, porridge: v })} /> - setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, sliced: v } })} /> - setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, spread: v } })} /> + + setBreakfast({ ...breakfast, porridge: v }) + } + /> + + setBreakfast({ + ...breakfast, + preparation: { + ...breakfast.preparation, + sliced: v, + }, + }) + } + /> + + setBreakfast({ + ...breakfast, + preparation: { + ...breakfast.preparation, + spread: v, + }, + }) + } + />
@@ -483,14 +643,94 @@ function NewOrderContent() {

Spreads (Aufstrich)

- setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, butter: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, margarine: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, jam: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, diabeticJam: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, honey: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, cheese: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, quark: v } })} /> - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, sausage: v } })} /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, butter: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, margarine: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, jam: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, diabeticJam: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, honey: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, cheese: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, quark: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + spreads: { ...breakfast.spreads, sausage: v }, + }) + } + />
@@ -499,10 +739,50 @@ function NewOrderContent() {

Beverages (Getränke)

- setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coffee: v } })} /> - setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, tea: v } })} /> - setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, hotMilk: v } })} /> - setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coldMilk: v } })} /> + + setBreakfast({ + ...breakfast, + beverages: { ...breakfast.beverages, coffee: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + beverages: { ...breakfast.beverages, tea: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + beverages: { ...breakfast.beverages, hotMilk: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + beverages: { ...breakfast.beverages, coldMilk: v }, + }) + } + />
@@ -511,39 +791,89 @@ function NewOrderContent() {

Additions (Zusätze)

- setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sugar: v } })} /> - setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sweetener: v } })} /> - setBreakfast({ ...breakfast, additions: { ...breakfast.additions, coffeeCreamer: v } })} /> + + setBreakfast({ + ...breakfast, + additions: { ...breakfast.additions, sugar: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + additions: { ...breakfast.additions, sweetener: v }, + }) + } + /> + + setBreakfast({ + ...breakfast, + additions: { + ...breakfast.additions, + coffeeCreamer: v, + }, + }) + } + />
)} {/* LUNCH OPTIONS */} - {mealType === 'lunch' && ( + {mealType === "lunch" && ( <>

Portion Size

setLunch({ ...lunch, portionSize: v as 'small' | 'large' | 'vegetarian' })} + onValueChange={(v) => + setLunch({ + ...lunch, + portionSize: v as "small" | "large" | "vegetarian", + }) + } className="grid gap-2 sm:grid-cols-3" > {[ - { value: 'small', label: 'Small (Kleine)' }, - { value: 'large', label: 'Large (Große)' }, - { value: 'vegetarian', label: 'Vegetarian' }, + { value: "small", label: "Small (Kleine)" }, + { value: "large", label: "Large (Große)" }, + { value: "vegetarian", label: "Vegetarian" }, ].map(({ value, label }) => (
setLunch({ ...lunch, portionSize: value as 'small' | 'large' | 'vegetarian' })} + onClick={() => + setLunch({ + ...lunch, + portionSize: value as + | "small" + | "large" + | "vegetarian", + }) + } > - +
))}
@@ -554,8 +884,20 @@ function NewOrderContent() {

Meal Options

- setLunch({ ...lunch, soup: v })} /> - setLunch({ ...lunch, dessert: v })} /> + setLunch({ ...lunch, soup: v })} + /> + + setLunch({ ...lunch, dessert: v }) + } + />
@@ -564,10 +906,62 @@ function NewOrderContent() {

Special Preparations

- setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedFood: v } })} /> - setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedMeat: v } })} /> - setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, slicedMeat: v } })} /> - setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, mashedPotatoes: v } })} /> + + setLunch({ + ...lunch, + specialPreparations: { + ...lunch.specialPreparations, + pureedFood: v, + }, + }) + } + /> + + setLunch({ + ...lunch, + specialPreparations: { + ...lunch.specialPreparations, + pureedMeat: v, + }, + }) + } + /> + + setLunch({ + ...lunch, + specialPreparations: { + ...lunch.specialPreparations, + slicedMeat: v, + }, + }) + } + /> + + setLunch({ + ...lunch, + specialPreparations: { + ...lunch.specialPreparations, + mashedPotatoes: v, + }, + }) + } + />
@@ -576,16 +970,52 @@ function NewOrderContent() {

Restrictions

- setLunch({ ...lunch, restrictions: { ...lunch.restrictions, noFish: v } })} /> - setLunch({ ...lunch, restrictions: { ...lunch.restrictions, fingerFood: v } })} /> - setLunch({ ...lunch, restrictions: { ...lunch.restrictions, onlySweet: v } })} /> + + setLunch({ + ...lunch, + restrictions: { ...lunch.restrictions, noFish: v }, + }) + } + /> + + setLunch({ + ...lunch, + restrictions: { + ...lunch.restrictions, + fingerFood: v, + }, + }) + } + /> + + setLunch({ + ...lunch, + restrictions: { + ...lunch.restrictions, + onlySweet: v, + }, + }) + } + />
)} {/* DINNER OPTIONS */} - {mealType === 'dinner' && ( + {mealType === "dinner" && ( <>

General

@@ -593,7 +1023,9 @@ function NewOrderContent() { id="dinnerAccordingToPlan" label="According to Plan (lt. Plan)" checked={dinner.accordingToPlan} - onCheckedChange={(v) => setDinner({ ...dinner, accordingToPlan: v })} + onCheckedChange={(v) => + setDinner({ ...dinner, accordingToPlan: v }) + } />
@@ -602,10 +1034,50 @@ function NewOrderContent() {

Bread (Brot)

- setDinner({ ...dinner, bread: { ...dinner.bread, greyBread: v } })} /> - setDinner({ ...dinner, bread: { ...dinner.bread, wholeGrainBread: v } })} /> - setDinner({ ...dinner, bread: { ...dinner.bread, whiteBread: v } })} /> - setDinner({ ...dinner, bread: { ...dinner.bread, crispbread: v } })} /> + + setDinner({ + ...dinner, + bread: { ...dinner.bread, greyBread: v }, + }) + } + /> + + setDinner({ + ...dinner, + bread: { ...dinner.bread, wholeGrainBread: v }, + }) + } + /> + + setDinner({ + ...dinner, + bread: { ...dinner.bread, whiteBread: v }, + }) + } + /> + + setDinner({ + ...dinner, + bread: { ...dinner.bread, crispbread: v }, + }) + } + />
@@ -614,8 +1086,28 @@ function NewOrderContent() {

Preparation

- setDinner({ ...dinner, preparation: { ...dinner.preparation, spread: v } })} /> - setDinner({ ...dinner, preparation: { ...dinner.preparation, sliced: v } })} /> + + setDinner({ + ...dinner, + preparation: { ...dinner.preparation, spread: v }, + }) + } + /> + + setDinner({ + ...dinner, + preparation: { ...dinner.preparation, sliced: v }, + }) + } + />
@@ -624,8 +1116,28 @@ function NewOrderContent() {

Spreads (Aufstrich)

- setDinner({ ...dinner, spreads: { ...dinner.spreads, butter: v } })} /> - setDinner({ ...dinner, spreads: { ...dinner.spreads, margarine: v } })} /> + + setDinner({ + ...dinner, + spreads: { ...dinner.spreads, butter: v }, + }) + } + /> + + setDinner({ + ...dinner, + spreads: { ...dinner.spreads, margarine: v }, + }) + } + />
@@ -634,9 +1146,30 @@ function NewOrderContent() {

Additional Items

- setDinner({ ...dinner, soup: v })} /> - setDinner({ ...dinner, porridge: v })} /> - setDinner({ ...dinner, noFish: v })} /> + + setDinner({ ...dinner, soup: v }) + } + /> + + setDinner({ ...dinner, porridge: v }) + } + /> + + setDinner({ ...dinner, noFish: v }) + } + />
@@ -645,10 +1178,50 @@ function NewOrderContent() {

Beverages (Getränke)

- setDinner({ ...dinner, beverages: { ...dinner.beverages, tea: v } })} /> - setDinner({ ...dinner, beverages: { ...dinner.beverages, cocoa: v } })} /> - setDinner({ ...dinner, beverages: { ...dinner.beverages, hotMilk: v } })} /> - setDinner({ ...dinner, beverages: { ...dinner.beverages, coldMilk: v } })} /> + + setDinner({ + ...dinner, + beverages: { ...dinner.beverages, tea: v }, + }) + } + /> + + setDinner({ + ...dinner, + beverages: { ...dinner.beverages, cocoa: v }, + }) + } + /> + + setDinner({ + ...dinner, + beverages: { ...dinner.beverages, hotMilk: v }, + }) + } + /> + + setDinner({ + ...dinner, + beverages: { ...dinner.beverages, coldMilk: v }, + }) + } + />
@@ -657,8 +1230,28 @@ function NewOrderContent() {

Additions (Zusätze)

- setDinner({ ...dinner, additions: { ...dinner.additions, sugar: v } })} /> - setDinner({ ...dinner, additions: { ...dinner.additions, sweetener: v } })} /> + + setDinner({ + ...dinner, + additions: { ...dinner.additions, sugar: v }, + }) + } + /> + + setDinner({ + ...dinner, + additions: { ...dinner.additions, sweetener: v }, + }) + } + />
@@ -701,7 +1294,9 @@ function NewOrderContent() {
Meal Type - {mealType && getMealTypeLabel(mealType)} + + {mealType && getMealTypeLabel(mealType)} +
@@ -709,7 +1304,8 @@ function NewOrderContent() { - Note: This resident requires high caloric meals. + Note: This resident requires high caloric + meals. )} @@ -742,7 +1338,7 @@ function NewOrderContent() { )}
- ) + ); } export default function NewOrderPage() { @@ -756,5 +1352,5 @@ export default function NewOrderPage() { > - ) + ); } diff --git a/src/app/(app)/caregiver/orders/page.tsx b/src/app/(app)/caregiver/orders/page.tsx index e1f1974..467359f 100644 --- a/src/app/(app)/caregiver/orders/page.tsx +++ b/src/app/(app)/caregiver/orders/page.tsx @@ -1,22 +1,22 @@ -'use client' +"use client"; -import React, { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { format, parseISO } from 'date-fns' -import { Plus, Pencil, Eye } from 'lucide-react' +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { format, parseISO } from "date-fns"; +import { Plus, Pencil, Eye } from "lucide-react"; -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select' +} from "@/components/ui/select"; import { Table, TableBody, @@ -24,7 +24,7 @@ import { TableHead, TableHeader, TableRow, -} from '@/components/ui/table' +} from "@/components/ui/table"; import { PageHeader, @@ -32,80 +32,82 @@ import { StatusBadge, MealTypeIcon, EmptyState, -} from '@/components/caregiver' +} from "@/components/caregiver"; import { ORDER_STATUSES, getMealTypeLabel, type MealType, type OrderStatus, -} from '@/lib/constants/meal' +} from "@/lib/constants/meal"; interface MealOrder { - id: number - title: string - date: string - mealType: MealType - status: OrderStatus - mealCount: number - createdAt: string + id: number; + title: string; + date: string; + mealType: MealType; + status: OrderStatus; + mealCount: number; + createdAt: string; } export default function OrdersListPage() { - const router = useRouter() - const [orders, setOrders] = useState([]) - const [loading, setLoading] = useState(true) - const [dateFilter, setDateFilter] = useState(() => format(new Date(), 'yyyy-MM-dd')) - const [statusFilter, setStatusFilter] = useState('all') + const router = useRouter(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [dateFilter, setDateFilter] = useState(() => + format(new Date(), "yyyy-MM-dd"), + ); + const [statusFilter, setStatusFilter] = useState("all"); useEffect(() => { const fetchOrders = async () => { - setLoading(true) + setLoading(true); try { - let url = `/api/meal-orders?sort=-date&limit=100&depth=0` + let url = `/api/meal-orders?sort=-date&limit=100&depth=0`; if (dateFilter) { - url += `&where[date][equals]=${dateFilter}` + url += `&where[date][equals]=${dateFilter}`; } - if (statusFilter !== 'all') { - url += `&where[status][equals]=${statusFilter}` + if (statusFilter !== "all") { + url += `&where[status][equals]=${statusFilter}`; } - const res = await fetch(url, { credentials: 'include' }) + const res = await fetch(url, { credentials: "include" }); if (res.ok) { - const data = await res.json() - setOrders(data.docs || []) + const data = await res.json(); + setOrders(data.docs || []); } else if (res.status === 401) { - router.push('/caregiver/login') + router.push("/login"); } } catch (err) { - console.error('Error fetching orders:', err) + console.error("Error fetching orders:", err); } finally { - setLoading(false) + setLoading(false); } - } - fetchOrders() - }, [router, dateFilter, statusFilter]) + }; + fetchOrders(); + }, [router, dateFilter, statusFilter]); const handleCreateOrder = async (mealType: MealType) => { try { - const res = await fetch('/api/meal-orders', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch("/api/meal-orders", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: dateFilter, mealType, - status: 'draft', + status: "draft", }), - credentials: 'include', - }) + credentials: "include", + }); if (res.ok) { - const data = await res.json() - router.push(`/caregiver/orders/${data.doc.id}`) + const data = await res.json(); + router.push(`/caregiver/orders/${data.doc.id}`); } } catch (err) { - console.error('Error creating order:', err) + console.error("Error creating order:", err); } - } + }; return (
@@ -146,7 +148,7 @@ export default function OrdersListPage() {
- {(['breakfast', 'lunch', 'dinner'] as const).map((type) => ( + {(["breakfast", "lunch", "dinner"] as const).map((type) => (
- ) + ); } diff --git a/src/app/(app)/caregiver/residents/page.tsx b/src/app/(app)/caregiver/residents/page.tsx index 9bcf469..fce6782 100644 --- a/src/app/(app)/caregiver/residents/page.tsx +++ b/src/app/(app)/caregiver/residents/page.tsx @@ -1,60 +1,64 @@ -'use client' +"use client"; -import React, { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { ArrowLeft, Search, Loader2 } from 'lucide-react' +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { ArrowLeft, Search, Loader2 } from "lucide-react"; -import { Button } from '@/components/ui/button' -import { Card, CardContent } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Badge } from '@/components/ui/badge' +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; interface Resident { - id: number - name: string - room: string - table?: string - station?: string - highCaloric?: boolean - aversions?: string - notes?: string - active: boolean + id: number; + name: string; + room: string; + table?: string; + station?: string; + highCaloric?: boolean; + aversions?: string; + notes?: string; + active: boolean; } export default function ResidentsListPage() { - const router = useRouter() - const [residents, setResidents] = useState([]) - const [loading, setLoading] = useState(true) - const [searchQuery, setSearchQuery] = useState('') + const router = useRouter(); + const [residents, setResidents] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchResidents = async () => { try { - const res = await fetch('/api/residents?where[active][equals]=true&limit=100&sort=name', { - credentials: 'include', - }) + const res = await fetch( + "/api/residents?where[active][equals]=true&limit=100&sort=name", + { + credentials: "include", + }, + ); if (res.ok) { - const data = await res.json() - setResidents(data.docs || []) + const data = await res.json(); + setResidents(data.docs || []); } else if (res.status === 401) { - router.push('/caregiver/login') + router.push("/login"); } } catch (err) { - console.error('Error fetching residents:', err) + console.error("Error fetching residents:", err); } finally { - setLoading(false) + setLoading(false); } - } - fetchResidents() - }, [router]) + }; + fetchResidents(); + }, [router]); const filteredResidents = residents.filter( (r) => r.name.toLowerCase().includes(searchQuery.toLowerCase()) || r.room.toLowerCase().includes(searchQuery.toLowerCase()) || - (r.station && r.station.toLowerCase().includes(searchQuery.toLowerCase())), - ) + (r.station && + r.station.toLowerCase().includes(searchQuery.toLowerCase())), + ); return (
@@ -73,7 +77,9 @@ export default function ResidentsListPage() {

Residents

-

View resident information and dietary requirements

+

+ View resident information and dietary requirements +

@@ -107,7 +113,10 @@ export default function ResidentsListPage() {
{resident.name}
{resident.highCaloric && ( - + High Cal )} @@ -122,12 +131,14 @@ export default function ResidentsListPage() {
{resident.aversions && (
- Aversions: {resident.aversions} + Aversions:{" "} + {resident.aversions}
)} {resident.notes && (
- Notes: {resident.notes} + Notes:{" "} + {resident.notes}
)}
@@ -139,5 +150,5 @@ export default function ResidentsListPage() { )}
- ) + ); } diff --git a/src/app/(app)/kitchen/dashboard/page.tsx b/src/app/(app)/kitchen/dashboard/page.tsx new file mode 100644 index 0000000..47a121b --- /dev/null +++ b/src/app/(app)/kitchen/dashboard/page.tsx @@ -0,0 +1,730 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { format } from "date-fns"; +import { + LogOut, + ChefHat, + Users, + Clock, + CheckCircle2, + Loader2, + RefreshCw, + ChevronDown, + ChevronUp, + AlertTriangle, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; + +import { useAuth } from "@/hooks/useAuth"; +import { + LoadingSpinner, + MealTypeSelector, + StatCard, +} from "@/components/caregiver"; +import { MEAL_TYPES, type MealType } from "@/lib/constants/meal"; + +interface Resident { + id: number; + name: string; + room: string; + table?: string; + station?: string; +} + +interface MealOrder { + id: number; + title: string; + date: string; + mealType: MealType; + status: string; + mealCount: number; +} + +interface Meal { + id: number; + title: string; + resident: Resident | number; + mealType: MealType; + status: "pending" | "preparing" | "prepared"; + order?: MealOrder | number; + highCaloric?: boolean; + notes?: string; +} + +interface KitchenReport { + date: string; + mealType: string; + totalMeals: number; + ingredients: Record; + labels: Record; + portionSizes?: Record; +} + +interface IngredientCategory { + name: string; + items: { key: string; label: string; count: number }[]; +} + +const MEAL_STATUS_CONFIG = { + pending: { + label: "Pending", + bgColor: "bg-gray-100", + textColor: "text-gray-700", + icon: Clock, + }, + preparing: { + label: "Preparing", + bgColor: "bg-yellow-100", + textColor: "text-yellow-700", + icon: ChefHat, + }, + prepared: { + label: "Prepared", + bgColor: "bg-green-100", + textColor: "text-green-700", + icon: CheckCircle2, + }, +}; + +function categorizeIngredients( + ingredients: Record, + labels: Record, +): IngredientCategory[] { + const categories: Record = { + bread: { name: "Bread", items: [] }, + preparation: { name: "Preparation", items: [] }, + spreads: { name: "Spreads", items: [] }, + beverages: { name: "Beverages", items: [] }, + additions: { name: "Additions", items: [] }, + mealOptions: { name: "Meal Options", items: [] }, + specialPreparations: { name: "Special Preparations", items: [] }, + restrictions: { name: "Restrictions", items: [] }, + other: { name: "Other", items: [] }, + }; + + for (const [key, count] of Object.entries(ingredients)) { + const label = labels[key] || key; + const item = { key, label, count }; + + if ( + key.includes("Bread") || + key.includes("Roll") || + key.includes("bread") || + key === "crispbread" + ) { + categories.bread.items.push(item); + } else if (key === "sliced" || key === "spread" || key === "porridge") { + categories.preparation.items.push(item); + } else if ( + [ + "butter", + "margarine", + "jam", + "diabeticJam", + "honey", + "cheese", + "quark", + "sausage", + ].includes(key) + ) { + categories.spreads.items.push(item); + } else if ( + ["coffee", "tea", "hotMilk", "coldMilk", "cocoa"].includes(key) + ) { + categories.beverages.items.push(item); + } else if (["sugar", "sweetener", "coffeeCreamer"].includes(key)) { + categories.additions.items.push(item); + } else if (["soup", "dessert"].includes(key)) { + categories.mealOptions.items.push(item); + } else if ( + ["pureedFood", "pureedMeat", "slicedMeat", "mashedPotatoes"].includes(key) + ) { + categories.specialPreparations.items.push(item); + } else if (["noFish", "fingerFood", "onlySweet"].includes(key)) { + categories.restrictions.items.push(item); + } else { + categories.other.items.push(item); + } + } + + return Object.values(categories).filter((cat) => cat.items.length > 0); +} + +export default function KitchenDashboardPage() { + const { + user, + loading: authLoading, + tenantName, + logout, + } = useAuth({ requiredRole: "kitchen" }); + + const [date, setDate] = useState(() => format(new Date(), "yyyy-MM-dd")); + const [mealType, setMealType] = useState("breakfast"); + const [report, setReport] = useState(null); + const [meals, setMeals] = useState([]); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(false); + const [updatingMealId, setUpdatingMealId] = useState(null); + const [expandedCategories, setExpandedCategories] = useState>( + new Set(["bread", "mealOptions"]), + ); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [reportRes, mealsRes, ordersRes] = await Promise.all([ + fetch(`/api/meals/kitchen-report?date=${date}&mealType=${mealType}`, { + credentials: "include", + }), + fetch( + `/api/meals?where[date][equals]=${date}&where[mealType][equals]=${mealType}&depth=2&limit=200&sort=resident`, + { credentials: "include" }, + ), + fetch( + `/api/meal-orders?where[date][equals]=${date}&where[mealType][equals]=${mealType}&depth=0`, + { credentials: "include" }, + ), + ]); + + if (reportRes.ok) { + const reportData = await reportRes.json(); + setReport(reportData); + } + + if (mealsRes.ok) { + const mealsData = await mealsRes.json(); + setMeals(mealsData.docs || []); + } + + if (ordersRes.ok) { + const ordersData = await ordersRes.json(); + setOrders(ordersData.docs || []); + } + } catch (err) { + console.error("Error fetching data:", err); + } finally { + setLoading(false); + } + }, [date, mealType]); + + useEffect(() => { + if (!authLoading && user) { + fetchData(); + } + }, [authLoading, user, fetchData]); + + const handleStatusChange = async ( + mealId: number, + newStatus: Meal["status"], + ) => { + setUpdatingMealId(mealId); + try { + const res = await fetch(`/api/meals/${mealId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: newStatus }), + credentials: "include", + }); + + if (res.ok) { + setMeals((prev) => + prev.map((meal) => + meal.id === mealId ? { ...meal, status: newStatus } : meal, + ), + ); + } + } catch (err) { + console.error("Error updating meal status:", err); + } finally { + setUpdatingMealId(null); + } + }; + + const handleBulkStatusChange = async (newStatus: Meal["status"]) => { + const mealsToUpdate = meals.filter((m) => m.status !== newStatus); + if (mealsToUpdate.length === 0) return; + + setLoading(true); + try { + await Promise.all( + mealsToUpdate.map((meal) => + fetch(`/api/meals/${meal.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: newStatus }), + credentials: "include", + }), + ), + ); + fetchData(); + } catch (err) { + console.error("Error bulk updating meals:", err); + } finally { + setLoading(false); + } + }; + + const toggleCategory = (categoryName: string) => { + setExpandedCategories((prev) => { + const newSet = new Set(prev); + if (newSet.has(categoryName)) { + newSet.delete(categoryName); + } else { + newSet.add(categoryName); + } + return newSet; + }); + }; + + if (authLoading) { + return ; + } + + const mealStats = { + total: meals.length, + pending: meals.filter((m) => m.status === "pending").length, + preparing: meals.filter((m) => m.status === "preparing").length, + prepared: meals.filter((m) => m.status === "prepared").length, + }; + + const progressPercent = + mealStats.total > 0 ? (mealStats.prepared / mealStats.total) * 100 : 0; + + const ingredientCategories = report + ? categorizeIngredients(report.ingredients, report.labels) + : []; + + const submittedOrders = orders.filter((o) => o.status !== "draft"); + + return ( +
+
+
+
+ +
+

Kitchen Dashboard

+

{tenantName}

+
+
+
+ + {user?.name || user?.email} + + +
+
+
+ +
+ + + Select Date & Meal + + +
+
+ + setDate(e.target.value)} + /> +
+
+ + +
+
+ +
+
+ + {submittedOrders.length === 0 && !loading ? ( + + + +

+ No Orders Submitted +

+

+ No meal orders have been submitted for{" "} + {MEAL_TYPES.find((m) => m.value === mealType)?.label} on{" "} + {format(new Date(date), "MMMM d, yyyy")}. +

+
+
+ ) : ( + <> +
+ + + + +
+ + + +
+ + Preparation Progress + + + {mealStats.prepared} of {mealStats.total} complete + +
+ +
+
+ +
+ + + Ingredient Summary + + Total quantities needed for {mealStats.total} meals + + + + {loading ? ( + + ) : ingredientCategories.length === 0 ? ( +

+ No ingredients to display +

+ ) : ( + <> + {report?.portionSizes && ( +
+

Portion Sizes

+
+ {Object.entries(report.portionSizes).map( + ([size, count]) => ( +
+ + {size} + + {count} +
+ ), + )} +
+
+ )} + + {ingredientCategories.map((category) => ( + + toggleCategory(category.name.toLowerCase()) + } + > + + + + +
+ {category.items.map((item) => ( +
+ {item.label} + {item.count} +
+ ))} +
+
+
+ ))} + + )} +
+
+ + + +
+
+ Meal Status + + Update status as meals are prepared + +
+
+ + +
+
+
+ + {loading ? ( + + ) : meals.length === 0 ? ( +

+ No meals found +

+ ) : ( +
+ + + + Resident + Room + Status + + Actions + + + + + {meals.map((meal) => { + const resident = + typeof meal.resident === "object" + ? meal.resident + : null; + const statusConfig = + MEAL_STATUS_CONFIG[meal.status]; + const StatusIcon = statusConfig.icon; + + return ( + + +
+ {resident?.name || "Unknown"} + Room {resident?.room || "-"} + {meal.highCaloric && ( + + HC + + )} +
+
+ {resident?.room || "-"} + + + + {statusConfig.label} + + + + {updatingMealId === meal.id ? ( + + ) : ( + + )} + +
+ ); + })} +
+
+
+ )} +
+
+
+ + {meals.some((m) => m.notes) && ( + + + Special Notes + + Meals with special requirements or notes + + + +
+ {meals + .filter((m) => m.notes || m.highCaloric) + .map((meal) => { + const resident = + typeof meal.resident === "object" + ? meal.resident + : null; + return ( +
+
+
+ {resident?.name} - Room {resident?.room} +
+ {meal.highCaloric && ( + + High Caloric Requirement + + )} + {meal.notes && ( +

+ {meal.notes} +

+ )} +
+
+ ); + })} +
+
+
+ )} + + )} +
+
+ ); +} diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 7dba5e7..448da0b 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,24 +1,30 @@ -import React from 'react' -import type { Viewport } from 'next' +import React from "react"; +import type { Viewport } from "next"; -import '@/app/globals.css' +import "@/app/globals.css"; export const metadata = { - description: 'Meal ordering for caregivers', - title: 'Meal Planner - Caregiver', -} + description: "Meal ordering for caregivers", + title: "Meal Planner - Caregiver", +}; export const viewport: Viewport = { - width: 'device-width', + width: "device-width", initialScale: 1, - viewportFit: 'cover', -} + viewportFit: "cover", +}; // eslint-disable-next-line no-restricted-exports -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { return ( - {children} + + {children} + - ) + ); } diff --git a/src/app/(app)/login/page.tsx b/src/app/(app)/login/page.tsx new file mode 100644 index 0000000..67aeb01 --- /dev/null +++ b/src/app/(app)/login/page.tsx @@ -0,0 +1,179 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2, ChefHat, Heart } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +import { type User, getPrimaryRole, getRedirectPath } from "@/lib/auth"; + +export default function UnifiedLoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [checking, setChecking] = useState(true); + + useEffect(() => { + const checkAuth = async () => { + try { + const res = await fetch("/api/users/me", { credentials: "include" }); + if (res.ok) { + const data = await res.json(); + if (data.user) { + const role = getPrimaryRole(data.user as User); + router.push(getRedirectPath(role)); + return; + } + } + } catch { + // Not logged in + } + setChecking(false); + }; + checkAuth(); + }, [router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setLoading(true); + + try { + const res = await fetch("/api/users/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + credentials: "include", + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.errors?.[0]?.message || "Login failed"); + } + + const user = data.user as User; + const role = getPrimaryRole(user); + + if (!role) { + await fetch("/api/users/logout", { + method: "POST", + credentials: "include", + }); + throw new Error("You do not have access to this application"); + } + + router.push(getRedirectPath(role)); + } catch (err) { + setError(err instanceof Error ? err.message : "Login failed"); + } finally { + setLoading(false); + } + }; + + if (checking) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

Meal Planner

+

Care Home Management

+
+ + + + Welcome Back + Sign in to access your portal + + + {error && ( + + {error} + + )} + +
+
+ + setEmail(e.target.value)} + placeholder="Enter your email" + required + autoComplete="email" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter your password" + required + autoComplete="current-password" + /> +
+ + +
+ +
+

+ You will be redirected based on your role +

+
+
+ + Caregivers +
+
+ + Kitchen Staff +
+
+
+
+
+
+
+ ); +} diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx index 09eceea..6fb2458 100644 --- a/src/app/(app)/page.tsx +++ b/src/app/(app)/page.tsx @@ -1,6 +1,5 @@ -import { redirect } from 'next/navigation' +import { redirect } from "next/navigation"; export default function HomePage() { - // Redirect to caregiver login by default - redirect('/caregiver/login') + redirect("/login"); } diff --git a/src/app/(payload)/admin/[[...segments]]/not-found.tsx b/src/app/(payload)/admin/[[...segments]]/not-found.tsx index 180e6f8..5868415 100644 --- a/src/app/(payload)/admin/[[...segments]]/not-found.tsx +++ b/src/app/(payload)/admin/[[...segments]]/not-found.tsx @@ -1,25 +1,28 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import type { Metadata } from 'next' +import type { Metadata } from "next"; -import config from '@payload-config' -import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' +import config from "@payload-config"; +import { generatePageMetadata, NotFoundPage } from "@payloadcms/next/views"; -import { importMap } from '../importMap.js' +import { importMap } from "../importMap.js"; type Args = { params: Promise<{ - segments: string[] - }> + segments: string[]; + }>; searchParams: Promise<{ - [key: string]: string | string[] - }> -} + [key: string]: string | string[]; + }>; +}; -export const generateMetadata = ({ params, searchParams }: Args): Promise => - generatePageMetadata({ config, params, searchParams }) +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise => + generatePageMetadata({ config, params, searchParams }); const NotFound = ({ params, searchParams }: Args) => - NotFoundPage({ config, importMap, params, searchParams }) + NotFoundPage({ config, importMap, params, searchParams }); -export default NotFound +export default NotFound; diff --git a/src/app/(payload)/admin/[[...segments]]/page.tsx b/src/app/(payload)/admin/[[...segments]]/page.tsx index e59b2d3..bc8ef96 100644 --- a/src/app/(payload)/admin/[[...segments]]/page.tsx +++ b/src/app/(payload)/admin/[[...segments]]/page.tsx @@ -1,25 +1,28 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import type { Metadata } from 'next' +import type { Metadata } from "next"; -import config from '@payload-config' -import { generatePageMetadata, RootPage } from '@payloadcms/next/views' +import config from "@payload-config"; +import { generatePageMetadata, RootPage } from "@payloadcms/next/views"; -import { importMap } from '../importMap.js' +import { importMap } from "../importMap.js"; type Args = { params: Promise<{ - segments: string[] - }> + segments: string[]; + }>; searchParams: Promise<{ - [key: string]: string | string[] - }> -} + [key: string]: string | string[]; + }>; +}; -export const generateMetadata = ({ params, searchParams }: Args): Promise => - generatePageMetadata({ config, params, searchParams }) +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise => + generatePageMetadata({ config, params, searchParams }); const Page = ({ params, searchParams }: Args) => - RootPage({ config, importMap, params, searchParams }) + RootPage({ config, importMap, params, searchParams }); -export default Page +export default Page; diff --git a/src/app/(payload)/admin/views/KitchenDashboard/index.tsx b/src/app/(payload)/admin/views/KitchenDashboard/index.tsx index 118b64c..aae7e7a 100644 --- a/src/app/(payload)/admin/views/KitchenDashboard/index.tsx +++ b/src/app/(payload)/admin/views/KitchenDashboard/index.tsx @@ -1,80 +1,82 @@ -'use client' +"use client"; -import React, { useState } from 'react' -import { format, parseISO } from 'date-fns' -import { Gutter } from '@payloadcms/ui' -import './styles.scss' +import React, { useState } from "react"; +import { format, parseISO } from "date-fns"; +import { Gutter } from "@payloadcms/ui"; +import "./styles.scss"; interface MealWithImage { - id: number - residentName: string - residentRoom: string - status: string - formImageUrl?: string - formImageThumbnail?: string + id: number; + residentName: string; + residentRoom: string; + status: string; + formImageUrl?: string; + formImageThumbnail?: string; } interface KitchenReportResponse { - date: string - mealType: string - totalMeals: number - ingredients: Record - labels: Record - portionSizes?: Record - mealsWithImages?: MealWithImage[] - error?: string + date: string; + mealType: string; + totalMeals: number; + ingredients: Record; + labels: Record; + portionSizes?: Record; + mealsWithImages?: MealWithImage[]; + error?: string; } export const KitchenDashboard: React.FC = () => { - const [date, setDate] = useState(() => format(new Date(), 'yyyy-MM-dd')) - const [mealType, setMealType] = useState<'breakfast' | 'lunch' | 'dinner'>('breakfast') - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [report, setReport] = useState(null) + const [date, setDate] = useState(() => format(new Date(), "yyyy-MM-dd")); + const [mealType, setMealType] = useState<"breakfast" | "lunch" | "dinner">( + "breakfast", + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [report, setReport] = useState(null); const generateReport = async () => { - setLoading(true) - setError(null) - setReport(null) + setLoading(true); + setError(null); + setReport(null); try { const response = await fetch( `/api/meals/kitchen-report?date=${date}&mealType=${mealType}`, { - credentials: 'include', + credentials: "include", }, - ) + ); - const data = await response.json() + const data = await response.json(); if (!response.ok) { - throw new Error(data.error || 'Failed to generate report') + throw new Error(data.error || "Failed to generate report"); } - setReport(data) + setReport(data); } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred') + setError(err instanceof Error ? err.message : "An error occurred"); } finally { - setLoading(false) + setLoading(false); } - } + }; const getMealTypeLabel = (type: string) => { switch (type) { - case 'breakfast': - return 'Breakfast (Frühstück)' - case 'lunch': - return 'Lunch (Mittagessen)' - case 'dinner': - return 'Dinner (Abendessen)' + case "breakfast": + return "Breakfast (Frühstück)"; + case "lunch": + return "Lunch (Mittagessen)"; + case "dinner": + return "Dinner (Abendessen)"; default: - return type + return type; } - } + }; const formatDate = (dateStr: string) => { - return format(parseISO(dateStr), 'EEEE, MMMM d, yyyy') - } + return format(parseISO(dateStr), "EEEE, MMMM d, yyyy"); + }; return ( @@ -102,7 +104,11 @@ export const KitchenDashboard: React.FC = () => {