From 13b56d4960fcdb1f3f8f9b4319a6b17c2e9eaa94 Mon Sep 17 00:00:00 2001 From: Mohammad Khyata Date: Fri, 27 Mar 2026 16:20:46 +0300 Subject: [PATCH] init --- .github/skills/crud-page/SKILL.md | 182 + .../skills/crud-page/references/api-client.md | 140 + .github/skills/crud-page/references/form.md | 234 + .github/skills/crud-page/references/page.md | 225 + .github/skills/crud-page/references/schema.md | 143 + .gitignore | 38 + .npmrc | 0 .vscode/mcp.json | 11 + Garage Management System.pdf | Bin 0 -> 182136 bytes README.md | 159 + apps/dashboard/.gitignore | 39 + apps/dashboard/.prettierignore | 7 + apps/dashboard/.prettierrc | 11 + apps/dashboard/.vscode/mcp.json | 11 + apps/dashboard/README.md | 21 + apps/dashboard/app/(auth)/login/page.tsx | 12 + .../app/(authenticated)/items/parts/page.tsx | 90 + .../items/service-group/page.tsx | 72 + .../(authenticated)/items/services/page.tsx | 69 + apps/dashboard/app/(authenticated)/layout.tsx | 219 + apps/dashboard/app/(authenticated)/page.tsx | 14 + .../productivity/employees/page.tsx | 65 + .../productivity/shop-calendars/page.tsx | 50 + .../productivity/shop-timings/page.tsx | 57 + .../(authenticated)/sales/customers/page.tsx | 54 + .../sales/inspections/page.tsx | 65 + .../(authenticated)/sales/vehicles/page.tsx | 98 + .../settings/shop-type/page.tsx | 54 + apps/dashboard/app/favicon.ico | Bin 0 -> 25931 bytes apps/dashboard/app/globals.css | 179 + apps/dashboard/app/layout.tsx | 40 + .../components/auth-store-initializer.tsx | 19 + .../layout/dashboard/app-sidebar.tsx | 240 + .../layout/dashboard/dashboard-header.tsx | 210 + .../layout/dashboard/dashboard-layout.tsx | 41 + .../layout/dashboard/dashboard-page.tsx | 20 + .../base/components/layout/dashboard/index.ts | 3 + apps/dashboard/base/types/navigation.ts | 31 + apps/dashboard/components.json | 25 + apps/dashboard/cypress.config.ts | 16 + .../customers/customer-form-integration.cy.ts | 293 + .../cypress/e2e/customers/customer-form.cy.ts | 347 + .../dashboard/cypress/fixtures/customers.json | 47 + apps/dashboard/cypress/support/commands.ts | 33 + apps/dashboard/cypress/support/e2e.ts | 1 + apps/dashboard/cypress/tsconfig.json | 15 + apps/dashboard/eslint.config.mjs | 18 + apps/dashboard/modules/auth/auth.actions.ts | 55 + .../modules/auth/login-form.schema.ts | 11 + apps/dashboard/modules/auth/login-form.tsx | 148 + .../modules/customers/customer-form.tsx | 264 + .../modules/customers/customer.schema.ts | 44 + .../modules/employees/employee-form.tsx | 236 + .../modules/employees/employee.schema.ts | 32 + .../inspection-category-inline-form.tsx | 57 + .../modules/inspections/inspection-form.tsx | 231 + .../modules/inspections/inspection.schema.ts | 22 + apps/dashboard/modules/parts/part-form.tsx | 242 + apps/dashboard/modules/parts/part.schema.ts | 19 + .../service-groups/service-group-form.tsx | 274 + .../service-groups/service-group.schema.ts | 23 + .../services/department-assignment-types.ts | 7 + .../inline-forms/category-inline-form.tsx | 2 + .../inline-forms/department-inline-form.tsx | 66 + .../inventory-category-inline-form.tsx | 78 + .../inline-forms/unit-type-inline-form.tsx | 55 + .../modules/services/service-form.tsx | 238 + .../modules/services/service.schema.ts | 19 + .../settings/shop-type/shop-type-form.tsx | 157 + .../settings/shop-type/shop-type.schema.ts | 19 + .../shop-calendars/shop-calendar-form.tsx | 99 + .../shop-calendars/shop-calendar.schema.ts | 11 + .../modules/shop-timings/shop-timing-form.tsx | 161 + .../shop-timings/shop-timing.schema.ts | 19 + .../inline-forms/body-type-inline-form.tsx | 55 + .../inline-forms/color-inline-form.tsx | 55 + .../inline-forms/fuel-type-inline-form.tsx | 55 + .../inline-forms/shop-type-inline-form.tsx | 114 + .../inline-forms/transmission-inline-form.tsx | 55 + .../modules/vehicles/vehicle-form.tsx | 267 + .../modules/vehicles/vehicle.schema.ts | 35 + apps/dashboard/next.config.mjs | 4 + apps/dashboard/package.json | 65 + apps/dashboard/postcss.config.mjs | 8 + apps/dashboard/public/.gitkeep | 0 apps/dashboard/public/assets/logo.png | Bin 0 -> 59735 bytes apps/dashboard/shared/api.ts | 12 + apps/dashboard/shared/components/.gitkeep | 0 .../shared/components/confirm-dialog.tsx | 120 + .../shared/components/form-dialog.tsx | 87 + .../form/controls/async-select-field.tsx | 160 + .../form/controls/checkbox-field.tsx | 28 + .../form/controls/file-input-field.tsx | 28 + .../components/form/controls/select-field.tsx | 45 + .../form/controls/text-input-field.tsx | 31 + .../form/controls/textarea-field.tsx | 31 + .../shared/components/form/field-shell.tsx | 29 + .../form/fields/rhf-async-select-field.tsx | 278 + .../form/fields/rhf-checkbox-field.tsx | 62 + .../components/form/fields/rhf-file-field.tsx | 24 + .../form/fields/rhf-select-field.tsx | 24 + .../components/form/fields/rhf-text-field.tsx | 24 + .../form/fields/rhf-textarea-field.tsx | 24 + .../form/fields/simple-title-form.tsx | 65 + .../dashboard/shared/components/form/index.ts | 33 + .../shared/components/form/rhf-field.tsx | 62 + .../shared/components/form/rhform.tsx | 24 + .../dashboard/shared/components/form/types.ts | 63 + .../shared/components/query-provider.tsx | 42 + .../shared/components/theme-provider.tsx | 71 + .../shared/components/ui/accordion.tsx | 81 + .../shared/components/ui/alert-dialog.tsx | 199 + apps/dashboard/shared/components/ui/alert.tsx | 76 + .../shared/components/ui/aspect-ratio.tsx | 11 + .../dashboard/shared/components/ui/avatar.tsx | 112 + apps/dashboard/shared/components/ui/badge.tsx | 49 + .../shared/components/ui/breadcrumb.tsx | 122 + .../shared/components/ui/button-group.tsx | 83 + .../dashboard/shared/components/ui/button.tsx | 67 + .../shared/components/ui/calendar.tsx | 222 + apps/dashboard/shared/components/ui/card.tsx | 103 + .../shared/components/ui/carousel.tsx | 242 + apps/dashboard/shared/components/ui/chart.tsx | 372 + .../shared/components/ui/checkbox.tsx | 33 + .../shared/components/ui/collapsible.tsx | 33 + .../shared/components/ui/combobox.tsx | 299 + .../shared/components/ui/command.tsx | 195 + .../shared/components/ui/context-menu.tsx | 263 + .../dashboard/shared/components/ui/dialog.tsx | 165 + .../shared/components/ui/direction.tsx | 22 + .../dashboard/shared/components/ui/drawer.tsx | 131 + .../shared/components/ui/dropdown-menu.tsx | 269 + apps/dashboard/shared/components/ui/empty.tsx | 104 + apps/dashboard/shared/components/ui/field.tsx | 238 + .../shared/components/ui/hover-card.tsx | 44 + .../shared/components/ui/input-group.tsx | 156 + .../shared/components/ui/input-otp.tsx | 87 + apps/dashboard/shared/components/ui/input.tsx | 19 + apps/dashboard/shared/components/ui/item.tsx | 196 + apps/dashboard/shared/components/ui/kbd.tsx | 26 + apps/dashboard/shared/components/ui/label.tsx | 24 + .../shared/components/ui/menubar.tsx | 280 + .../shared/components/ui/native-select.tsx | 52 + .../shared/components/ui/navigation-menu.tsx | 164 + .../shared/components/ui/pagination.tsx | 129 + .../shared/components/ui/popover.tsx | 89 + .../shared/components/ui/progress.tsx | 31 + .../shared/components/ui/radio-group.tsx | 44 + .../shared/components/ui/resizable.tsx | 50 + .../shared/components/ui/scroll-area.tsx | 55 + .../dashboard/shared/components/ui/select.tsx | 192 + .../shared/components/ui/separator.tsx | 28 + apps/dashboard/shared/components/ui/sheet.tsx | 144 + .../shared/components/ui/sidebar.tsx | 702 + .../shared/components/ui/skeleton.tsx | 13 + .../dashboard/shared/components/ui/slider.tsx | 59 + .../dashboard/shared/components/ui/sonner.tsx | 49 + .../shared/components/ui/spinner.tsx | 10 + .../dashboard/shared/components/ui/switch.tsx | 33 + apps/dashboard/shared/components/ui/table.tsx | 116 + apps/dashboard/shared/components/ui/tabs.tsx | 90 + .../shared/components/ui/textarea.tsx | 18 + .../shared/components/ui/toggle-group.tsx | 89 + .../dashboard/shared/components/ui/toggle.tsx | 46 + .../shared/components/ui/tooltip.tsx | 57 + .../shared/data-view/resource-page/index.ts | 14 + .../data-view/resource-page/resource-page.tsx | 89 + .../resource-page/use-resource-page.ts | 102 + .../data-view/table-view/actions-column.tsx | 64 + .../data-view/table-view/column-header.tsx | 82 + .../data-view/table-view/data-table.tsx | 127 + .../table-view/data-view-context.tsx | 30 + .../table-view/data-view-pagination.tsx | 91 + .../shared/data-view/table-view/index.ts | 16 + .../data-view/table-view/search-params.ts | 20 + .../shared/data-view/table-view/types.ts | 28 + .../table-view/use-data-table-query.ts | 86 + apps/dashboard/shared/hooks/.gitkeep | 0 apps/dashboard/shared/hooks/use-auth.ts | 24 + .../shared/hooks/use-form-mutation.ts | 22 + apps/dashboard/shared/hooks/use-mobile.ts | 19 + .../shared/hooks/use-resource-form.ts | 61 + apps/dashboard/shared/lib/.gitkeep | 0 apps/dashboard/shared/lib/utils.ts | 26 + apps/dashboard/shared/stores/app-store.ts | 23 + apps/dashboard/shared/stores/auth-store.ts | 90 + apps/dashboard/shared/useApi.ts | 11 + apps/dashboard/tsconfig.json | 34 + docs/dashboard/crud/data-fetching.md | 278 + docs/dashboard/crud/enhancement-plan.md | 225 + docs/dashboard/crud/form-system.md | 308 + docs/dashboard/crud/overview.md | 128 + docs/dashboard/crud/resource-page.md | 179 + docs/dashboard/feature-checklist.md | 261 + package.json | 22 + packages/api/open-api/schema.json | 28678 ++++++++++++++++ packages/api/open-api/schema.yaml | 9625 ++++++ packages/api/package.json | 39 + packages/api/postman/collection.json | 20462 +++++++++++ packages/api/scripts/generate-openapi.cjs | 223 + packages/api/scripts/generate-types.cjs | 13 + packages/api/src/api.ts | 65 + packages/api/src/clients/appointments.ts | 35 + packages/api/src/clients/auth.ts | 26 + packages/api/src/clients/customers.ts | 32 + packages/api/src/clients/departments.ts | 40 + packages/api/src/clients/employees.ts | 17 + packages/api/src/clients/estimates.ts | 69 + packages/api/src/clients/expenses.ts | 74 + packages/api/src/clients/geo.ts | 21 + packages/api/src/clients/index.ts | 28 + packages/api/src/clients/inspections.ts | 118 + packages/api/src/clients/insurance-types.ts | 30 + packages/api/src/clients/inventory.ts | 99 + packages/api/src/clients/job-cards.ts | 90 + packages/api/src/clients/labels.ts | 30 + packages/api/src/clients/parts.ts | 45 + packages/api/src/clients/payment-terms.ts | 35 + packages/api/src/clients/payments.ts | 50 + packages/api/src/clients/purchase-orders.ts | 30 + packages/api/src/clients/referral-sources.ts | 35 + packages/api/src/clients/service-groups.ts | 17 + packages/api/src/clients/services.ts | 40 + packages/api/src/clients/shop-calendars.ts | 41 + packages/api/src/clients/shop-timings.ts | 27 + packages/api/src/clients/shop-types.ts | 60 + packages/api/src/clients/tasks.ts | 99 + .../api/src/clients/vehicle-attributes.ts | 88 + packages/api/src/clients/vehicle-documents.ts | 69 + packages/api/src/clients/vehicles.ts | 55 + packages/api/src/clients/vendors.ts | 45 + packages/api/src/contracts/crud.ts | 8 + packages/api/src/contracts/types.ts | 19 + packages/api/src/index.ts | 13 + packages/api/src/infra/client.ts | 269 + packages/api/src/infra/crud-client.ts | 58 + packages/api/src/infra/index.ts | 22 + packages/api/src/infra/token.ts | 6 + packages/api/src/infra/types.ts | 112 + packages/api/src/server.ts | 24 + packages/api/tsconfig.json | 11 + packages/api/types/index.ts | 22335 ++++++++++++ packages/eslint-config/README.md | 3 + packages/eslint-config/base.js | 32 + packages/eslint-config/next.js | 57 + packages/eslint-config/package.json | 24 + packages/eslint-config/react-internal.js | 39 + packages/typescript-config/base.json | 19 + packages/typescript-config/nextjs.json | 12 + packages/typescript-config/package.json | 9 + packages/typescript-config/react-library.json | 7 + packages/ui/eslint.config.mjs | 4 + packages/ui/package.json | 26 + packages/ui/src/button.tsx | 20 + packages/ui/src/card.tsx | 27 + packages/ui/src/code.tsx | 11 + packages/ui/tsconfig.json | 9 + pnpm-lock.yaml | 9926 ++++++ pnpm-workspace.yaml | 3 + turbo.json | 46 + 260 files changed, 111131 insertions(+) create mode 100644 .github/skills/crud-page/SKILL.md create mode 100644 .github/skills/crud-page/references/api-client.md create mode 100644 .github/skills/crud-page/references/form.md create mode 100644 .github/skills/crud-page/references/page.md create mode 100644 .github/skills/crud-page/references/schema.md create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .vscode/mcp.json create mode 100644 Garage Management System.pdf create mode 100644 README.md create mode 100644 apps/dashboard/.gitignore create mode 100644 apps/dashboard/.prettierignore create mode 100644 apps/dashboard/.prettierrc create mode 100644 apps/dashboard/.vscode/mcp.json create mode 100644 apps/dashboard/README.md create mode 100644 apps/dashboard/app/(auth)/login/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/items/parts/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/items/service-group/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/items/services/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/layout.tsx create mode 100644 apps/dashboard/app/(authenticated)/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/productivity/employees/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/sales/customers/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/sales/inspections/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx create mode 100644 apps/dashboard/app/(authenticated)/settings/shop-type/page.tsx create mode 100644 apps/dashboard/app/favicon.ico create mode 100644 apps/dashboard/app/globals.css create mode 100644 apps/dashboard/app/layout.tsx create mode 100644 apps/dashboard/base/components/auth-store-initializer.tsx create mode 100644 apps/dashboard/base/components/layout/dashboard/app-sidebar.tsx create mode 100644 apps/dashboard/base/components/layout/dashboard/dashboard-header.tsx create mode 100644 apps/dashboard/base/components/layout/dashboard/dashboard-layout.tsx create mode 100644 apps/dashboard/base/components/layout/dashboard/dashboard-page.tsx create mode 100644 apps/dashboard/base/components/layout/dashboard/index.ts create mode 100644 apps/dashboard/base/types/navigation.ts create mode 100644 apps/dashboard/components.json create mode 100644 apps/dashboard/cypress.config.ts create mode 100644 apps/dashboard/cypress/e2e/customers/customer-form-integration.cy.ts create mode 100644 apps/dashboard/cypress/e2e/customers/customer-form.cy.ts create mode 100644 apps/dashboard/cypress/fixtures/customers.json create mode 100644 apps/dashboard/cypress/support/commands.ts create mode 100644 apps/dashboard/cypress/support/e2e.ts create mode 100644 apps/dashboard/cypress/tsconfig.json create mode 100644 apps/dashboard/eslint.config.mjs create mode 100644 apps/dashboard/modules/auth/auth.actions.ts create mode 100644 apps/dashboard/modules/auth/login-form.schema.ts create mode 100644 apps/dashboard/modules/auth/login-form.tsx create mode 100644 apps/dashboard/modules/customers/customer-form.tsx create mode 100644 apps/dashboard/modules/customers/customer.schema.ts create mode 100644 apps/dashboard/modules/employees/employee-form.tsx create mode 100644 apps/dashboard/modules/employees/employee.schema.ts create mode 100644 apps/dashboard/modules/inspections/inline-forms/inspection-category-inline-form.tsx create mode 100644 apps/dashboard/modules/inspections/inspection-form.tsx create mode 100644 apps/dashboard/modules/inspections/inspection.schema.ts create mode 100644 apps/dashboard/modules/parts/part-form.tsx create mode 100644 apps/dashboard/modules/parts/part.schema.ts create mode 100644 apps/dashboard/modules/service-groups/service-group-form.tsx create mode 100644 apps/dashboard/modules/service-groups/service-group.schema.ts create mode 100644 apps/dashboard/modules/services/department-assignment-types.ts create mode 100644 apps/dashboard/modules/services/inline-forms/category-inline-form.tsx create mode 100644 apps/dashboard/modules/services/inline-forms/department-inline-form.tsx create mode 100644 apps/dashboard/modules/services/inline-forms/inventory-category-inline-form.tsx create mode 100644 apps/dashboard/modules/services/inline-forms/unit-type-inline-form.tsx create mode 100644 apps/dashboard/modules/services/service-form.tsx create mode 100644 apps/dashboard/modules/services/service.schema.ts create mode 100644 apps/dashboard/modules/settings/shop-type/shop-type-form.tsx create mode 100644 apps/dashboard/modules/settings/shop-type/shop-type.schema.ts create mode 100644 apps/dashboard/modules/shop-calendars/shop-calendar-form.tsx create mode 100644 apps/dashboard/modules/shop-calendars/shop-calendar.schema.ts create mode 100644 apps/dashboard/modules/shop-timings/shop-timing-form.tsx create mode 100644 apps/dashboard/modules/shop-timings/shop-timing.schema.ts create mode 100644 apps/dashboard/modules/vehicles/inline-forms/body-type-inline-form.tsx create mode 100644 apps/dashboard/modules/vehicles/inline-forms/color-inline-form.tsx create mode 100644 apps/dashboard/modules/vehicles/inline-forms/fuel-type-inline-form.tsx create mode 100644 apps/dashboard/modules/vehicles/inline-forms/shop-type-inline-form.tsx create mode 100644 apps/dashboard/modules/vehicles/inline-forms/transmission-inline-form.tsx create mode 100644 apps/dashboard/modules/vehicles/vehicle-form.tsx create mode 100644 apps/dashboard/modules/vehicles/vehicle.schema.ts create mode 100644 apps/dashboard/next.config.mjs create mode 100644 apps/dashboard/package.json create mode 100644 apps/dashboard/postcss.config.mjs create mode 100644 apps/dashboard/public/.gitkeep create mode 100644 apps/dashboard/public/assets/logo.png create mode 100644 apps/dashboard/shared/api.ts create mode 100644 apps/dashboard/shared/components/.gitkeep create mode 100644 apps/dashboard/shared/components/confirm-dialog.tsx create mode 100644 apps/dashboard/shared/components/form-dialog.tsx create mode 100644 apps/dashboard/shared/components/form/controls/async-select-field.tsx create mode 100644 apps/dashboard/shared/components/form/controls/checkbox-field.tsx create mode 100644 apps/dashboard/shared/components/form/controls/file-input-field.tsx create mode 100644 apps/dashboard/shared/components/form/controls/select-field.tsx create mode 100644 apps/dashboard/shared/components/form/controls/text-input-field.tsx create mode 100644 apps/dashboard/shared/components/form/controls/textarea-field.tsx create mode 100644 apps/dashboard/shared/components/form/field-shell.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-async-select-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-checkbox-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-file-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-select-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-text-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/rhf-textarea-field.tsx create mode 100644 apps/dashboard/shared/components/form/fields/simple-title-form.tsx create mode 100644 apps/dashboard/shared/components/form/index.ts create mode 100644 apps/dashboard/shared/components/form/rhf-field.tsx create mode 100644 apps/dashboard/shared/components/form/rhform.tsx create mode 100644 apps/dashboard/shared/components/form/types.ts create mode 100644 apps/dashboard/shared/components/query-provider.tsx create mode 100644 apps/dashboard/shared/components/theme-provider.tsx create mode 100644 apps/dashboard/shared/components/ui/accordion.tsx create mode 100644 apps/dashboard/shared/components/ui/alert-dialog.tsx create mode 100644 apps/dashboard/shared/components/ui/alert.tsx create mode 100644 apps/dashboard/shared/components/ui/aspect-ratio.tsx create mode 100644 apps/dashboard/shared/components/ui/avatar.tsx create mode 100644 apps/dashboard/shared/components/ui/badge.tsx create mode 100644 apps/dashboard/shared/components/ui/breadcrumb.tsx create mode 100644 apps/dashboard/shared/components/ui/button-group.tsx create mode 100644 apps/dashboard/shared/components/ui/button.tsx create mode 100644 apps/dashboard/shared/components/ui/calendar.tsx create mode 100644 apps/dashboard/shared/components/ui/card.tsx create mode 100644 apps/dashboard/shared/components/ui/carousel.tsx create mode 100644 apps/dashboard/shared/components/ui/chart.tsx create mode 100644 apps/dashboard/shared/components/ui/checkbox.tsx create mode 100644 apps/dashboard/shared/components/ui/collapsible.tsx create mode 100644 apps/dashboard/shared/components/ui/combobox.tsx create mode 100644 apps/dashboard/shared/components/ui/command.tsx create mode 100644 apps/dashboard/shared/components/ui/context-menu.tsx create mode 100644 apps/dashboard/shared/components/ui/dialog.tsx create mode 100644 apps/dashboard/shared/components/ui/direction.tsx create mode 100644 apps/dashboard/shared/components/ui/drawer.tsx create mode 100644 apps/dashboard/shared/components/ui/dropdown-menu.tsx create mode 100644 apps/dashboard/shared/components/ui/empty.tsx create mode 100644 apps/dashboard/shared/components/ui/field.tsx create mode 100644 apps/dashboard/shared/components/ui/hover-card.tsx create mode 100644 apps/dashboard/shared/components/ui/input-group.tsx create mode 100644 apps/dashboard/shared/components/ui/input-otp.tsx create mode 100644 apps/dashboard/shared/components/ui/input.tsx create mode 100644 apps/dashboard/shared/components/ui/item.tsx create mode 100644 apps/dashboard/shared/components/ui/kbd.tsx create mode 100644 apps/dashboard/shared/components/ui/label.tsx create mode 100644 apps/dashboard/shared/components/ui/menubar.tsx create mode 100644 apps/dashboard/shared/components/ui/native-select.tsx create mode 100644 apps/dashboard/shared/components/ui/navigation-menu.tsx create mode 100644 apps/dashboard/shared/components/ui/pagination.tsx create mode 100644 apps/dashboard/shared/components/ui/popover.tsx create mode 100644 apps/dashboard/shared/components/ui/progress.tsx create mode 100644 apps/dashboard/shared/components/ui/radio-group.tsx create mode 100644 apps/dashboard/shared/components/ui/resizable.tsx create mode 100644 apps/dashboard/shared/components/ui/scroll-area.tsx create mode 100644 apps/dashboard/shared/components/ui/select.tsx create mode 100644 apps/dashboard/shared/components/ui/separator.tsx create mode 100644 apps/dashboard/shared/components/ui/sheet.tsx create mode 100644 apps/dashboard/shared/components/ui/sidebar.tsx create mode 100644 apps/dashboard/shared/components/ui/skeleton.tsx create mode 100644 apps/dashboard/shared/components/ui/slider.tsx create mode 100644 apps/dashboard/shared/components/ui/sonner.tsx create mode 100644 apps/dashboard/shared/components/ui/spinner.tsx create mode 100644 apps/dashboard/shared/components/ui/switch.tsx create mode 100644 apps/dashboard/shared/components/ui/table.tsx create mode 100644 apps/dashboard/shared/components/ui/tabs.tsx create mode 100644 apps/dashboard/shared/components/ui/textarea.tsx create mode 100644 apps/dashboard/shared/components/ui/toggle-group.tsx create mode 100644 apps/dashboard/shared/components/ui/toggle.tsx create mode 100644 apps/dashboard/shared/components/ui/tooltip.tsx create mode 100644 apps/dashboard/shared/data-view/resource-page/index.ts create mode 100644 apps/dashboard/shared/data-view/resource-page/resource-page.tsx create mode 100644 apps/dashboard/shared/data-view/resource-page/use-resource-page.ts create mode 100644 apps/dashboard/shared/data-view/table-view/actions-column.tsx create mode 100644 apps/dashboard/shared/data-view/table-view/column-header.tsx create mode 100644 apps/dashboard/shared/data-view/table-view/data-table.tsx create mode 100644 apps/dashboard/shared/data-view/table-view/data-view-context.tsx create mode 100644 apps/dashboard/shared/data-view/table-view/data-view-pagination.tsx create mode 100644 apps/dashboard/shared/data-view/table-view/index.ts create mode 100644 apps/dashboard/shared/data-view/table-view/search-params.ts create mode 100644 apps/dashboard/shared/data-view/table-view/types.ts create mode 100644 apps/dashboard/shared/data-view/table-view/use-data-table-query.ts create mode 100644 apps/dashboard/shared/hooks/.gitkeep create mode 100644 apps/dashboard/shared/hooks/use-auth.ts create mode 100644 apps/dashboard/shared/hooks/use-form-mutation.ts create mode 100644 apps/dashboard/shared/hooks/use-mobile.ts create mode 100644 apps/dashboard/shared/hooks/use-resource-form.ts create mode 100644 apps/dashboard/shared/lib/.gitkeep create mode 100644 apps/dashboard/shared/lib/utils.ts create mode 100644 apps/dashboard/shared/stores/app-store.ts create mode 100644 apps/dashboard/shared/stores/auth-store.ts create mode 100644 apps/dashboard/shared/useApi.ts create mode 100644 apps/dashboard/tsconfig.json create mode 100644 docs/dashboard/crud/data-fetching.md create mode 100644 docs/dashboard/crud/enhancement-plan.md create mode 100644 docs/dashboard/crud/form-system.md create mode 100644 docs/dashboard/crud/overview.md create mode 100644 docs/dashboard/crud/resource-page.md create mode 100644 docs/dashboard/feature-checklist.md create mode 100644 package.json create mode 100644 packages/api/open-api/schema.json create mode 100644 packages/api/open-api/schema.yaml create mode 100644 packages/api/package.json create mode 100644 packages/api/postman/collection.json create mode 100644 packages/api/scripts/generate-openapi.cjs create mode 100644 packages/api/scripts/generate-types.cjs create mode 100644 packages/api/src/api.ts create mode 100644 packages/api/src/clients/appointments.ts create mode 100644 packages/api/src/clients/auth.ts create mode 100644 packages/api/src/clients/customers.ts create mode 100644 packages/api/src/clients/departments.ts create mode 100644 packages/api/src/clients/employees.ts create mode 100644 packages/api/src/clients/estimates.ts create mode 100644 packages/api/src/clients/expenses.ts create mode 100644 packages/api/src/clients/geo.ts create mode 100644 packages/api/src/clients/index.ts create mode 100644 packages/api/src/clients/inspections.ts create mode 100644 packages/api/src/clients/insurance-types.ts create mode 100644 packages/api/src/clients/inventory.ts create mode 100644 packages/api/src/clients/job-cards.ts create mode 100644 packages/api/src/clients/labels.ts create mode 100644 packages/api/src/clients/parts.ts create mode 100644 packages/api/src/clients/payment-terms.ts create mode 100644 packages/api/src/clients/payments.ts create mode 100644 packages/api/src/clients/purchase-orders.ts create mode 100644 packages/api/src/clients/referral-sources.ts create mode 100644 packages/api/src/clients/service-groups.ts create mode 100644 packages/api/src/clients/services.ts create mode 100644 packages/api/src/clients/shop-calendars.ts create mode 100644 packages/api/src/clients/shop-timings.ts create mode 100644 packages/api/src/clients/shop-types.ts create mode 100644 packages/api/src/clients/tasks.ts create mode 100644 packages/api/src/clients/vehicle-attributes.ts create mode 100644 packages/api/src/clients/vehicle-documents.ts create mode 100644 packages/api/src/clients/vehicles.ts create mode 100644 packages/api/src/clients/vendors.ts create mode 100644 packages/api/src/contracts/crud.ts create mode 100644 packages/api/src/contracts/types.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/infra/client.ts create mode 100644 packages/api/src/infra/crud-client.ts create mode 100644 packages/api/src/infra/index.ts create mode 100644 packages/api/src/infra/token.ts create mode 100644 packages/api/src/infra/types.ts create mode 100644 packages/api/src/server.ts create mode 100644 packages/api/tsconfig.json create mode 100644 packages/api/types/index.ts create mode 100644 packages/eslint-config/README.md create mode 100644 packages/eslint-config/base.js create mode 100644 packages/eslint-config/next.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/eslint-config/react-internal.js create mode 100644 packages/typescript-config/base.json create mode 100644 packages/typescript-config/nextjs.json create mode 100644 packages/typescript-config/package.json create mode 100644 packages/typescript-config/react-library.json create mode 100644 packages/ui/eslint.config.mjs create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/button.tsx create mode 100644 packages/ui/src/card.tsx create mode 100644 packages/ui/src/code.tsx create mode 100644 packages/ui/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 turbo.json diff --git a/.github/skills/crud-page/SKILL.md b/.github/skills/crud-page/SKILL.md new file mode 100644 index 0000000..db65136 --- /dev/null +++ b/.github/skills/crud-page/SKILL.md @@ -0,0 +1,182 @@ +--- +name: crud-page +description: "Create CRUD resource pages, forms, schemas, and API clients for the carage-erp dashboard. Use when: adding a new resource page, creating a CRUD feature, building a list/create/edit/delete page, scaffolding a new module, adding a new entity to the dashboard. Covers API client, Zod schema, form component, and page component creation." +--- + +# CRUD Page Generator + +Create fully functional CRUD resource pages following the established codebase patterns. This skill covers the full stack: API client → Zod schema → form component → page component. + +## When to Use + +- User asks to create a new resource/entity page (e.g. "create a vendors page", "add invoices CRUD") +- User asks to add list/create/edit/delete functionality for a domain entity +- User asks to scaffold a new module or feature page +- User wants to extend the dashboard with a new data management page + +## Decision: ResourcePage vs Manual DataTable + +**Use `ResourcePage` (preferred)** when the resource needs full CRUD (list + create + edit + delete in a dialog). This is the standard pattern. + +**Use manual `DataTable` + `useDataTableQuery`** only when the page is read-only or has highly custom layout needs. + +Always prefer `ResourcePage` unless the user explicitly needs something different. + +## Procedure + +Follow these steps **in order**. Each step produces one file. Check the [reference files](./references/) for complete templates and patterns. + +### Step 1: Check if API Client Exists + +Look in `packages/api/src/clients/` for an existing client. Also check `packages/api/src/clients/index.ts` for all registered clients, and `packages/api/src/api.ts` for the factory. + +- If client exists → skip to Step 3 +- If client doesn't exist → continue to Step 2 + +### Step 2: Create API Client + +Read the [API Client Reference](./references/api-client.md) for patterns and template. + +Create the domain client file at `packages/api/src/clients/.ts`: + +1. Define `RESOURCE_ROUTES` const with `INDEX` and `BY_ID` routes (and any extras) +2. Create a class extending `CrudClient` with the route types +3. Add any domain-specific methods beyond standard CRUD +4. Register in `packages/api/src/clients/index.ts` (export class + routes) +5. Register in `packages/api/src/api.ts` (import + add to `createApi()`) + +**Route pattern**: `"/api/"` for INDEX, `"/api//{id}"` for BY_ID. + +**IMPORTANT**: Routes must exist in the OpenAPI schema (`packages/api/types/index.ts`) for type safety. If the route doesn't exist in the schema yet, inform the user and ask if they want to proceed with `any` types or wait for schema update. + +### Step 3: Create Zod Schema + +Read the [Schema Reference](./references/schema.md) for patterns and template. + +Create `apps/dashboard/modules//.schema.ts`: + +1. Define `relationFieldSchema` (reuse if already exported) for foreign-key fields +2. Build the Zod object schema with all form fields +3. Use `.optional()` for non-required fields, `.min(1, "...")` for required strings +4. Use `z.union([z.string().email(...), z.literal("")]).optional()` for optional emails +5. Export the schema, the inferred type, and `relationFieldSchema` if new + +### Step 4: Create Form Component + +Read the [Form Reference](./references/form.md) for the complete template. + +Create `apps/dashboard/modules//-form.tsx`: + +1. Define default values matching the schema +2. Create `mapToFormValues(data)` — transforms API shape → form shape using `toRelation()` +3. Create `mapFormToPayload(values)` — transforms form shape → API shape using `toId()` +4. Use `useResourceForm()` for form initialization + edit pre-filling +5. Use `useFormMutation()` for submit with automatic validation error mapping +6. Render with `Rhform` + `RhfTextField` / `RhfSelectField` / `RhfAsyncSelectField` etc. +7. Include error alert, submit button with loading/edit states + +### Step 5: Create Page Component + +Read the [Page Reference](./references/page.md) for the complete template. + +Create `apps/dashboard/app/(authenticated)/
//page.tsx`: + +1. Add `"use client"` directive +2. Import `ResourcePage`, `ColumnHeader`, the form, client type, and routes +3. Configure: `pageTitle`, `title`, `routeKey`, `getClient`, `columns`, `renderForm` +4. Use `columns` callback to receive `actionsColumn` helper +5. Add sortable column headers with `` +6. Include `actionsColumn()` as last column + +### Step 6: Verify + +- Ensure all imports resolve +- Check that route constants match OpenAPI paths +- Confirm the client is registered in both `clients/index.ts` and `api.ts` + +## Key Conventions + +### Naming + +| Item | Pattern | Example | +|---|---|---| +| Client file | `packages/api/src/clients/.ts` | `job-cards.ts` | +| Client class | `Client` | `JobCardsClient` | +| Routes const | `_ROUTES` | `JOB_CARD_ROUTES` | +| Schema file | `modules//.schema.ts` | `job-card.schema.ts` | +| Form file | `modules//-form.tsx` | `job-card-form.tsx` | +| Page file | `app/(authenticated)/
//page.tsx` | `sales/job-cards/page.tsx` | +| Zod schema | `FormSchema` | `jobCardFormSchema` | +| Form values type | `FormValues` | `JobCardFormValues` | +| Form component | `Form` | `JobCardForm` | +| Page component | `Page` (default export) | `JobCardsPage` | + +### Relation Fields (Foreign Keys) + +- Stored in form as `{ value: string, label: string } | null` +- Use `toRelation(id, name)` to convert API data → form value +- Use `toId(relation)` to convert form value → API payload +- Schema uses `relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()` +- Rendered with `` (fetches options via React Query) + +### Async Select Pattern + +```tsx +const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name }) +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + + api.resource.listSomething()} + mapOption={mapLookupOption} + {...STORE_OBJECT} +/> +``` + +### Available Form Field Components + +| Component | Use For | +|---|---| +| `RhfTextField` | Text, email, phone, URL inputs | +| `RhfTextareaField` | Multi-line text | +| `RhfCheckboxField` | Boolean toggles | +| `RhfSelectField` | Static option dropdowns | +| `RhfAsyncSelectField` | Server-fetched single-select combobox | +| `RhfAsyncMultiSelectField` | Server-fetched multi-select combobox | + +### Imports Cheat Sheet + +```tsx +// Page +import { ResourcePage } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import type { Client } from '@garage/api' +import { _ROUTES } from '@garage/api' + +// Form +import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField } from "@/shared/components/form" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { useAuthApi } from "@/shared/useApi" +import { toRelation, toId } from "@/shared/lib/utils" +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { toast } from "sonner" + +// Schema +import { z } from "zod" +``` + +## Extending the CRUD Codebase + +If a feature requires functionality not covered by existing utilities (e.g. inline editing, tab-based forms, file uploads, nested resources), you are encouraged to extend the shared infrastructure: + +- Add new form field components in `shared/components/form/controls/` and `shared/components/form/fields/` +- Add new hooks in `shared/hooks/` +- Extend `ResourcePage` props if needed +- Add new column helper factories in `shared/data-view/table-view/` +- Keep extensions generic and reusable — follow the same patterns as existing code diff --git a/.github/skills/crud-page/references/api-client.md b/.github/skills/crud-page/references/api-client.md new file mode 100644 index 0000000..ebf921c --- /dev/null +++ b/.github/skills/crud-page/references/api-client.md @@ -0,0 +1,140 @@ +# API Client Reference + +## File Location + +`packages/api/src/clients/.ts` + +## Standard CrudClient Pattern (Preferred) + +Use this when the resource has standard CRUD endpoints that exist in the OpenAPI schema. + +```ts +import { CrudClient } from "../infra/crud-client" +import type { ApiClientOptions } from "../infra/client" +import type { ApiPath, ApiRequestBody } from "../infra/types" + +export const _ROUTES = { + INDEX: "/api/", + BY_ID: "/api//{id}", + // Add extra routes as needed: + // EXPORT: "/api//export", + // IMPORT: "/api//import", + // RELATED: "/api/", +} as const satisfies Record + +export class Client extends CrudClient< + typeof _ROUTES.INDEX, + typeof _ROUTES.BY_ID +> { + constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { + super(baseUrl, defaultOptions, _ROUTES.INDEX, _ROUTES.BY_ID) + } + + // Add domain-specific methods: + // async listCategories() { + // return this.get(_ROUTES.RELATED) + // } + // + // async export() { + // return this.get(_ROUTES.EXPORT) + // } +} +``` + +### CrudClient Gives You For Free + +| Method | HTTP | Description | +|---|---|---| +| `list(query?)` | `GET /api/` | Paginated list with query params | +| `show(id)` | `GET /api//{id}` | Single item fetch | +| `create(payload)` | `POST /api/` | Create new item | +| `update(id, payload)` | `PUT /api//{id}` | Update existing item | +| `destroy(id)` | `DELETE /api//{id}` | Delete item | + +## Minimal CrudClient (No Custom Methods) + +For simple resources with only standard CRUD: + +```ts +import { CrudClient } from "../infra/crud-client" +import type { ApiClientOptions } from "../infra/client" +import type { ApiPath } from "../infra/types" + +export const _ROUTES = { + INDEX: "/api/", + BY_ID: "/api//{id}", +} as const satisfies Record + +export class Client extends CrudClient< + typeof _ROUTES.INDEX, + typeof _ROUTES.BY_ID +> { + constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { + super(baseUrl, defaultOptions, _ROUTES.INDEX, _ROUTES.BY_ID) + } +} +``` + +## Registration + +After creating the client, register it in two files: + +### 1. `packages/api/src/clients/index.ts` + +```ts +export { Client, _ROUTES } from "./" +``` + +### 2. `packages/api/src/api.ts` + +Add the import at the top: +```ts +import { Client } from "./clients/" +``` + +Add to the `createApi()` return object: +```ts +export function createApi(options?: ApiClientOptions) { + return { + // ...existing clients... + : new Client(undefined, options), + } +} +``` + +## Real Example: CustomersClient + +```ts +import { CrudClient } from "../infra/crud-client" +import { ApiClient, type ApiClientOptions } from "../infra/client" +import type { ApiPath, ApiRequestBody } from "../infra/types" + +export const CUSTOMER_ROUTES = { + INDEX: "/api/customers", + BY_ID: "/api/customers/{id}", + EXPORT: "/api/customers/export", + IMPORT: "/api/customers/import", + CUSTOMER_TYPES: "/api/customer-types", +} as const satisfies Record + +export class CustomersClient extends CrudClient< + typeof CUSTOMER_ROUTES.INDEX, + typeof CUSTOMER_ROUTES.BY_ID +> { + constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { + super(baseUrl, defaultOptions, CUSTOMER_ROUTES.INDEX, CUSTOMER_ROUTES.BY_ID) + } + + async listCustomerTypes() { + return this.get(CUSTOMER_ROUTES.CUSTOMER_TYPES) + } + + async export() { + return this.get(CUSTOMER_ROUTES.EXPORT) + } + + async import(payload: ApiRequestBody) { + return this.post(CUSTOMER_ROUTES.IMPORT, payload) + } +} +``` diff --git a/.github/skills/crud-page/references/form.md b/.github/skills/crud-page/references/form.md new file mode 100644 index 0000000..c6df8ba --- /dev/null +++ b/.github/skills/crud-page/references/form.md @@ -0,0 +1,234 @@ +# Form Reference + +## File Location + +`apps/dashboard/modules//-form.tsx` + +## Complete Template + +```tsx +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfAsyncSelectField, + // RhfTextareaField, + // RhfCheckboxField, + // RhfAsyncMultiSelectField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" + +import { + FormSchema, + type FormValues, +} from "./.schema" +import { _ROUTES } from "@garage/api" + +// ── Constants ── + +// Static select options (if needed): +// const STATUS_OPTIONS = [ +// { value: "active", label: "Active" }, +// { value: "inactive", label: "Inactive" }, +// ] + +// ── Props ── + +export type FormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: FormValues = { + // Match every field in the Zod schema: + // name: "", + // email: "", + // category: null, // relation fields default to null + // is_active: true, // booleans +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): FormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + // String fields: + // name: d.name || "", + // email: d.email || "", + + // Relation fields (API returns id + name separately): + // category: toRelation(d.category_id, d.category_name), + + // Booleans: + // is_active: d.is_active ?? true, + } +} + +function mapFormToPayload(values: FormValues) { + return { + // String fields — use `|| undefined` to send null for empty strings: + // name: values.name, + // email: values.email || undefined, + + // Relation fields — extract the numeric ID: + // category_id: toId(values.category), + + // Booleans: + // is_active: values.is_active, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name, +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function Form({ resourceId, initialData, onSuccess }: FormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm<FormValues, any>({ + schema: FormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api..show(id), + queryKey: [_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: FormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api..update(resourceId, payload) + : api..create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating ..." : "Creating ...", + success: isEditing ? " updated successfully" : " created successfully", + error: isEditing ? "Failed to update " : "Failed to create ", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update " : "Failed to create "} + + {error.message} + + )} + + + {/* Text fields */} + {/* */} + + {/* Grid layout for side-by-side fields */} + {/*
+ + +
*/} + + {/* Static select */} + {/* */} + + {/* Async select (fetches options from API) */} + {/* api.categories.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> */} + + {/* Textarea */} + {/* */} + + {/* Checkbox */} + {/* */} + + +
+
+ ) +} +``` + +## Key Patterns + +### mapToFormValues + +Transforms API response → form values. Always handle: +- Null safety: `d.field || ""` +- Relation fields: `toRelation(d.relation_id, d.relation_name)` +- Nested data: `(data as any)?.data ?? data ?? {}` +- Booleans: `d.field ?? defaultValue` + +### mapFormToPayload + +Transforms form values → API request body. Always handle: +- Empty strings to undefined: `values.field || undefined` +- Relation to ID: `toId(values.relation)` +- Keep required fields as-is: `values.name` + +### useResourceForm + +Initializes react-hook-form with Zod validation. Handles both create and edit modes: +- `resourceId` null → create mode (uses `defaultValues`) +- `resourceId` set → edit mode (fetches via `initialize`, maps with `mapToFormValues`) + +### useFormMutation + +Wraps `useMutation` with automatic Laravel validation error mapping to form fields. No need to manually handle `ApiError.validationErrors`. + +### Toast Pattern + +Always use `toast.promise()` wrapping the API call for consistent loading/success/error feedback. + +## Layout Conventions + +- Use `` to wrap all fields +- Use `
` for side-by-side fields +- Place the submit button at the bottom inside `` +- Show error alert above fields when mutation fails diff --git a/.github/skills/crud-page/references/page.md b/.github/skills/crud-page/references/page.md new file mode 100644 index 0000000..66a95ff --- /dev/null +++ b/.github/skills/crud-page/references/page.md @@ -0,0 +1,225 @@ +# Page Reference + +## File Location + +`apps/dashboard/app/(authenticated)/
//page.tsx` + +Where `
` is the navigation section (e.g. `sales`, `inventory`, `hr`) and `` is the resource in kebab-case plural. + +## Complete Template (ResourcePage Pattern) + +```tsx +"use client" + +import { ResourcePage } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import { Form } from '@/modules//-form' +import { _ROUTES } from '@garage/api' +import type { Client } from '@garage/api' + +export default function Page() { + return ( + Client> + pageTitle="" + title="" + routeKey={_ROUTES.INDEX} + getClient={(api) => api.} + columns={({ actionsColumn }) => [ + { + accessorKey: "", + header: ({ column }) => , + }, + { + accessorKey: "", + header: ({ column }) => , + }, + // Add more columns as needed... + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + <Form + resourceId={resourceId} + initialData={initialData} + onSuccess={onSuccess} + /> + )} + /> + ) +} +``` + +## ResourcePage Props + +| Prop | Required | Description | +|---|---|---| +| `pageTitle` | No | Page heading text (e.g. "Customers") | +| `title` | Yes | Singular noun for button/dialog (e.g. "Customer" → "Add Customer") | +| `routeKey` | Yes | React Query cache key, use `ROUTES.INDEX` | +| `getClient` | Yes | Selects the domain client from the authenticated API | +| `columns` | Yes | Column definitions — use callback form to get `actionsColumn` helper | +| `renderForm` | Yes | Renders the form component inside the dialog | +| `queryOptions` | No | React Query overrides (`staleTime`, etc.) | + +## Column Patterns + +### Simple text column (sortable) +```tsx +{ + accessorKey: "name", + header: ({ column }) => , +}, +``` + +### Custom cell renderer +```tsx +{ + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.status} + + ), +}, +``` + +### Column with icon +```tsx +{ + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => ( +
+ + {row.original.name} +
+ ), +}, +``` + +### Non-sortable column +```tsx +{ + accessorKey: "notes", + header: () => Notes, + enableSorting: false, +}, +``` + +### Actions column (always last) +```tsx +actionsColumn(), +``` + +## Real Example: Customers Page + +```tsx +"use client" + +import { ResourcePage } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import { CustomerForm } from '@/modules/customers/customer-form' +import { CUSTOMER_ROUTES } from '@garage/api' +import type { CustomersClient } from '@garage/api' +import { Building2Icon, UserIcon } from 'lucide-react' + +export default function CustomersPage() { + return ( + + pageTitle='Customers' + title="Customer" + routeKey={CUSTOMER_ROUTES.INDEX} + getClient={(api) => api.customers} + columns={({ actionsColumn }) => [ + { + accessorKey: "first_name", + header: ({ column }) => , + cell: ({ row }) => { + const customerName = row.original.first_name + const isCompany = row.original.customer_type?.name?.toLocaleLowerCase() === "company"; + const companyName = row.original.company_name + const name = isCompany && companyName + ? `${customerName} (${row.original.last_name})` + : customerName + return ( +
+ {isCompany + ? + : } + {name} +
+ ) + }, + }, + { + accessorKey: "email", + header: ({ column }) => , + }, + { + accessorKey: "phone", + header: ({ column }) => , + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} +``` + +## Alternative: Manual DataTable Pattern (Read-Only or Custom Layout) + +Use only when you don't need create/edit/delete in a dialog: + +```tsx +"use client" + +import { DashboardHeader } from '@/base/components/layout/dashboard' +import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' +import { ColumnHeader, DataTable, useDataTableQuery } from '@/shared/data-view/table-view' +import { useAuthApi } from '@/shared/useApi' +import { _ROUTES } from '@garage/api' +import type { ColumnDef } from '@tanstack/react-table' + +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => , + }, + // ... more columns +] + +export default function Page() { + const api = useAuthApi() + + const { data, isLoading, pagination, sorting, handleChange } = useDataTableQuery({ + queryKey: [_ROUTES.INDEX], + client: api., + }) + + const response = data as any + + return ( + }> + + + ) +} +``` diff --git a/.github/skills/crud-page/references/schema.md b/.github/skills/crud-page/references/schema.md new file mode 100644 index 0000000..5830ced --- /dev/null +++ b/.github/skills/crud-page/references/schema.md @@ -0,0 +1,143 @@ +# Schema Reference + +## File Location + +`apps/dashboard/modules//.schema.ts` + +## Template + +```ts +import { z } from "zod" + +// Reusable relation field schema — use for all foreign-key / lookup fields +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +const FormSchema = z.object({ + // ── Relations (stored as { value, label } objects, mapped to IDs on submit) ── + // category: relationFieldSchema, + + // ── Required strings ── + // name: z.string().min(1, "Name is required"), + + // ── Optional strings ── + // description: z.string().optional(), + + // ── Optional email (allows empty string) ── + // email: z.union([ + // z.string().email("Enter a valid email address"), + // z.literal(""), + // ]).optional(), + + // ── Optional phone ── + // phone: z.string().optional(), + + // ── Boolean ── + // is_active: z.boolean().default(true), + + // ── Number ── + // quantity: z.coerce.number().min(0), + + // ── Date ── + // due_date: z.string().optional(), +}) + +type FormValues = z.inferFormSchema> + +export { FormSchema, relationFieldSchema } +export type { FormValues } +``` + +## Field Type Patterns + +### Required string +```ts +name: z.string().min(1, "Name is required"), +``` + +### Optional string +```ts +notes: z.string().optional(), +``` + +### Optional email (allows empty) +```ts +email: z.union([ + z.string().email("Enter a valid email address"), + z.literal(""), +]).optional(), +``` + +### Relation / Foreign key +```ts +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +// In schema: +department: relationFieldSchema, +``` + +### Required relation +```ts +department: z + .object({ value: z.string(), label: z.string() }) + .refine((v) => v !== null, { message: "Department is required" }), +``` + +### Boolean with default +```ts +is_active: z.boolean().default(true), +``` + +### Number (from string input) +```ts +quantity: z.coerce.number().min(0, "Must be non-negative"), +price: z.coerce.number().min(0), +``` + +### Static enum select +```ts +status: z.enum(["active", "inactive", "pending"]).default("active"), +salutation: z.string().optional(), +``` + +## Real Example: CustomerFormSchema + +```ts +import { z } from "zod" + +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +type RelationField = z.infer + +const customerFormSchema = z.object({ + customer_type: relationFieldSchema, + referral_source: relationFieldSchema, + payment_terms: relationFieldSchema, + country: relationFieldSchema, + state: relationFieldSchema, + salutation: z.string().optional(), + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + company_name: z.string().optional(), + email: z.union([ + z.string().email("Enter a valid email address"), + z.literal(""), + ]).optional(), + phone: z.string().optional(), + alternate_phone: z.string().optional(), + address_line_1: z.string().optional(), + address_line_2: z.string().optional(), + city: z.string().optional(), + zip_code: z.string().optional(), +}) + +type CustomerFormValues = z.infer + +export { customerFormSchema, relationFieldSchema } +export type { CustomerFormValues, RelationField } +``` diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96fab4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..6716ff9 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,11 @@ +{ + "servers": { + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + } + } +} diff --git a/Garage Management System.pdf b/Garage Management System.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d91fe0a964ffc145ade969aed1125921924b8346 GIT binary patch literal 182136 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%0A{tpSz8VTAcJicpL0eFpouF0R5{21LD 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%0AXm5o;PTm3eX}P3|H^O(7*Ql*uG6 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 [!TIP] +> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). + +Turborepo can use a technique known as [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. + +By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: + +With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended): + +```sh +cd my-turborepo +turbo login +``` + +Without global `turbo`, use your package manager: + +```sh +cd my-turborepo +npx turbo login +yarn exec turbo login +pnpm exec turbo login +``` + +This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). + +Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: + +With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed: + +```sh +turbo link +``` + +Without global `turbo`: + +```sh +npx turbo link +yarn exec turbo link +pnpm exec turbo link +``` + +## Useful Links + +Learn more about the power of Turborepo: + +- [Tasks](https://turborepo.dev/docs/crafting-your-repository/running-tasks) +- [Caching](https://turborepo.dev/docs/crafting-your-repository/caching) +- [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching) +- [Filtering](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters) +- [Configuration Options](https://turborepo.dev/docs/reference/configuration) +- [CLI Usage](https://turborepo.dev/docs/reference/command-line-reference) diff --git a/apps/dashboard/.gitignore b/apps/dashboard/.gitignore new file mode 100644 index 0000000..8655a4c --- /dev/null +++ b/apps/dashboard/.gitignore @@ -0,0 +1,39 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage +/cypress/videos +/cypress/screenshots +/cypress/downloads + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/dashboard/.prettierignore b/apps/dashboard/.prettierignore new file mode 100644 index 0000000..461b008 --- /dev/null +++ b/apps/dashboard/.prettierignore @@ -0,0 +1,7 @@ +dist/ +node_modules/ +.next/ +.turbo/ +coverage/ +pnpm-lock.yaml +.pnpm-store/ \ No newline at end of file diff --git a/apps/dashboard/.prettierrc b/apps/dashboard/.prettierrc new file mode 100644 index 0000000..a8a2054 --- /dev/null +++ b/apps/dashboard/.prettierrc @@ -0,0 +1,11 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "app/globals.css", + "tailwindFunctions": ["cn", "cva"] +} diff --git a/apps/dashboard/.vscode/mcp.json b/apps/dashboard/.vscode/mcp.json new file mode 100644 index 0000000..6716ff9 --- /dev/null +++ b/apps/dashboard/.vscode/mcp.json @@ -0,0 +1,11 @@ +{ + "servers": { + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + } + } +} diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md new file mode 100644 index 0000000..1e66186 --- /dev/null +++ b/apps/dashboard/README.md @@ -0,0 +1,21 @@ +# Next.js template + +This is a Next.js template with shadcn/ui. + +## Adding components + +To add components to your app, run the following command: + +```bash +npx shadcn@latest add button +``` + +This will place the ui components in the `components` directory. + +## Using components + +To use the components in your app, import them as follows: + +```tsx +import { Button } from "@/components/ui/button"; +``` diff --git a/apps/dashboard/app/(auth)/login/page.tsx b/apps/dashboard/app/(auth)/login/page.tsx new file mode 100644 index 0000000..1817e2f --- /dev/null +++ b/apps/dashboard/app/(auth)/login/page.tsx @@ -0,0 +1,12 @@ +import { LoginForm } from "@/modules/auth/login-form"; + + +export default function Page() { + return ( +
+
+ +
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/items/parts/page.tsx b/apps/dashboard/app/(authenticated)/items/parts/page.tsx new file mode 100644 index 0000000..c3bdaf4 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/items/parts/page.tsx @@ -0,0 +1,90 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { PartForm } from "@/modules/parts/part-form" +import { Badge } from "@/shared/components/ui/badge" +import { PARTS_ROUTES } from "@garage/api" +import type { PartsClient } from "@garage/api" + +export default function PartsPage() { + return ( + + pageTitle="Parts" + title="Part" + routeKey={PARTS_ROUTES.INDEX} + getClient={(api) => api.parts} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + cell: ({ row }) => { + const r = row.original as any + return ( +
+ {r.title || "—"} + {r.sku && ( + {r.sku} + )} +
+ ) + }, + }, + { + accessorKey: "part_number", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).part_number || "—", + }, + { + accessorKey: "manufactured_by", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).manufactured_by || "—", + }, + { + accessorKey: "selling_price", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).selling_price + return val != null ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "purchase_price", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).purchase_price + return val != null ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "is_active", + header: ({ column }) => , + cell: ({ row }) => { + const active = (row.original as any).is_active + return ( + + {active ? "Active" : "Inactive"} + + ) + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).created_at + return val ? new Date(val).toLocaleDateString() : "—" + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/items/service-group/page.tsx b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx new file mode 100644 index 0000000..1a5bdf4 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx @@ -0,0 +1,72 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ServiceGroupForm } from "@/modules/service-groups/service-group-form" +import { Badge } from "@/shared/components/ui/badge" +import { SERVICE_GROUP_ROUTES } from "@garage/api" +import type { ServiceGroupsClient } from "@garage/api" + +export default function ServiceGroupPage() { + return ( + + pageTitle="Service Groups" + title="Service Group" + routeKey={SERVICE_GROUP_ROUTES.INDEX} + getClient={(api) => api.serviceGroups} + columns={({ actionsColumn }) => [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => { + const r = row.original as any + return ( +
+ {r.service_name || r.name || "—"} + {r.code && ( + {r.code} + )} +
+ ) + }, + }, + { + accessorKey: "selling_price", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).selling_price + return val != null ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "is_active", + header: ({ column }) => , + cell: ({ row }) => { + const active = (row.original as any).is_active + return ( + + {active ? "Active" : "Inactive"} + + ) + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).created_at + return val ? new Date(val).toLocaleDateString() : "—" + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/items/services/page.tsx b/apps/dashboard/app/(authenticated)/items/services/page.tsx new file mode 100644 index 0000000..0ccc839 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/items/services/page.tsx @@ -0,0 +1,69 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ServiceForm } from "@/modules/services/service-form" +import { SERVICE_ROUTES } from "@garage/api" +import type { ServicesClient } from "@garage/api" + +export default function ServicesPage() { + return ( + + pageTitle="Services" + title="Service" + routeKey={SERVICE_ROUTES.INDEX} + getClient={(api) => api.services} + columns={({ actionsColumn }) => [ + { + accessorKey: "labor_name", + header: ({ column }) => , + cell: ({ row }) => { + const r = row.original as any + return ( +
+ {r.labor_name || r.name || "—"} + {r.service_code && ( + {r.service_code} + )} +
+ ) + }, + }, + { + accessorKey: "description", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).description + return val + ? {val} + : "—" + }, + }, + { + accessorKey: "selling_price", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).selling_price + return val != null ? `$${Number(val).toFixed(2)}` : "—" + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).created_at + return val ? new Date(val).toLocaleDateString() : "—" + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/layout.tsx b/apps/dashboard/app/(authenticated)/layout.tsx new file mode 100644 index 0000000..bce734b --- /dev/null +++ b/apps/dashboard/app/(authenticated)/layout.tsx @@ -0,0 +1,219 @@ +"use client" + +import { Suspense } from "react" +import type { NavGroup } from "@/base/types/navigation" +import { + AlarmClockIcon, + AwardIcon, + BanknoteArrowDownIcon, + BarChart3Icon, + BellRingIcon, + BookIcon, + BriefcaseBusinessIcon, + Building2Icon, + CalendarCheck2Icon, + CalendarDaysIcon, + LayoutDashboardIcon, + ClipboardListIcon, + UsersIcon, + CalendarIcon, + CarIcon, + ClipboardCheckIcon, + Clock3Icon, + ClockIcon, + GemIcon, + GitBranchIcon, + HandCoinsIcon, + ListIcon, + ListTodoIcon, + MegaphoneIcon, + PackageIcon, + PhoneCallIcon, + PlugZapIcon, + ReceiptIcon, + ReceiptTextIcon, + SettingsIcon, + ShoppingBasketIcon, + CircleDollarSign, + StarIcon, + StoreIcon, + TimerIcon, + UserCogIcon, + WalletIcon, + WrenchIcon, + ShoppingCartIcon, +} from "lucide-react" +import Image from "next/image" +import { DashboardLayout } from "@/base/components/layout/dashboard" +import { useAuth } from "@/shared/hooks/use-auth" + +const navGroups: NavGroup[] = [ + { + items: [ + { + title: "Dashboard", + href: "/", + icon: , + }, + { + title: "Job Cards", + href: "/sales/workorder/list", + icon: , + }, + { + title: "Customer & Vehicles", + href: "/customer-vehicles", + icon: , + }, + { + title: "Reports", + href: "/reports", + icon: , + }, + ], + }, + { + label: "Management", + items: [ + { + title: "Calendars", + href: "/calendars", + icon: , + items: [ + { title: "Work Schedule", href: "/calendar/work-schedule/list", icon: }, + { title: "Appointments", href: "/calendar/appointment/list", icon: }, + ], + }, + { + title: "Sales", + href: "/sales", + icon: , + items: [ + { title: "Customers", href: "/sales/customers", icon: }, + { title: "Vehicles", href: "/sales/vehicles", icon: }, + { title: "Inspections", href: "/sales/inspections", icon: }, + { title: "Estimates", href: "/sales/estimate", icon: }, + { title: "Job Cards", href: "/sales/workorder/list", icon: }, + { title: "Invoices", href: "/sales/invoice", icon: }, + { title: "Payments Received", href: "/sales/payment-received", icon: }, + { title: "Credit Notes", href: "/sales/credit-notes", icon: }, + ], + }, + { + title: "Purchases", + href: "/purchases", + icon: , + items: [ + { title: "Vendors", href: "/purchase/vendor", icon: }, + { title: "Expenses", href: "/purchase/expense", icon: }, + { title: "Purchase Orders", href: "/purchase/purchase-order", icon: }, + { title: "Bills", href: "/purchase/bill", icon: }, + { title: "Payments Made", href: "/purchase/payments-made", icon: }, + { title: "Vendor Credits", href: "/purchase/vendor-credit", icon: }, + ], + }, + { + title: "CRM", + href: "/crm", + icon: , + items: [ + { title: "Leads", href: "/crm/leads/list", icon: }, + { title: "Calls", href: "/crm/calls-follow-up/list", icon: }, + { title: "Tasks", href: "/crm/tasks/list", icon: }, + ], + }, + { + title: "Marketing", + href: "/marketing", + icon: , + items: [ + { title: "Service Reminders", href: "/marketing/service-reminder/list", icon: }, + { title: "Rating & Reviews", href: "/marketing/rating-review", icon: }, + { title: "Google Business Reviews", href: "/marketing/google-rating-review", icon: }, + ], + }, + { + title: "Accountants", + href: "/accountants", + icon: , + items: [ + { title: "Manual Journals", href: "/accountants/manual-journal", icon: }, + { title: "Chart of Accounts", href: "/accountants/chart-of-account", icon: }, + ], + }, + { + title: "Employees", + href: "/productivity", + icon: , + items: [ + { title: "Employees", href: "/productivity/employees", icon: }, + { title: "Time Clocks", href: "/productivity/time-clocks", icon: }, + { title: "Time Sheets", href: "/productivity/timesheet", icon: }, + { title: "Payroll", href: "/productivity/payroll", icon: }, + { title: "Payments Made", href: "/productivity/employee-payments-made", icon: }, + { title: "Shop Calendars", href: "/productivity/shop-calendars", icon: }, + { title: "Shop Timing", href: "/productivity/shop-timings", icon: }, + { title: "Holidays", href: "/productivity/holidays", icon: }, + ], + }, + { + title: "Items", + href: "/items", + icon: , + items: [ + { title: "Services", href: "/items/services", icon: }, + { title: "Parts", href: "/items/parts", icon: }, + { title: "Expense Item", href: "/items/expense-item", icon: }, + { title: "Service Group", href: "/items/service-group", icon: }, + { title: "Inspections", href: "/items/inspection", icon: }, + { title: "Inventory Adjustments", href: "/items/adjustment", icon: }, + ], + }, + { + title: "Settings", + href: "/setting", + icon: , + items: [ + { title: "Company", href: "/setting/company", icon: }, + { title: "Shop Types", href: "/setting/shop-type", icon: }, + { title: "Tax & Rates", href: "/setting/tax-rates", icon: }, + { title: "Configurations", href: "/setting/configurations/preferences/sales", icon: }, + { title: "Templates", href: "/setting/templates", icon: }, + { title: "Integrations", href: "/setting/integrations/providers", icon: }, + { title: "Master", href: "/setting/master/body-type", icon: }, + ], + }, + ], + }, +] + +function Logo() { + return ( +
+ Logo +
+ ) +} + +export default function AuthenticatedLayout({ + children, +}: { + children: React.ReactNode +}) { + const { user } = useAuth() + + const userInfo = user + ? { + name: user.name, + email: user.email, + initials: user.name.charAt(0).toUpperCase(), + } + : undefined + + return ( + } user={userInfo}> + {children} + + ) +} + diff --git a/apps/dashboard/app/(authenticated)/page.tsx b/apps/dashboard/app/(authenticated)/page.tsx new file mode 100644 index 0000000..69396b0 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/page.tsx @@ -0,0 +1,14 @@ +import { DashboardHeader } from "@/base/components/layout/dashboard"; +import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"; +export default function page() { + return ( + } > +
+

Dashboard

+

+ Welcome to your dashboard. Select an item from the sidebar to get started. +

+
+
+ ) +} diff --git a/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx b/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx new file mode 100644 index 0000000..2751d08 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx @@ -0,0 +1,65 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { EmployeeForm } from "@/modules/employees/employee-form" +import { EMPLOYEE_ROUTES } from "@garage/api" +import type { EmployeesClient } from "@garage/api" + +export default function EmployeesPage() { + return ( + + pageTitle="Employees" + title="Employee" + routeKey={EMPLOYEE_ROUTES.INDEX} + getClient={(api) => api.employees} + columns={({ actionsColumn }) => [ + { + accessorKey: "first_name", + header: ({ column }) => , + cell: ({ row }) => { + const { first_name, last_name } = row.original + return `${first_name ?? ""} ${last_name ?? ""}`.trim() + }, + }, + { + accessorKey: "email", + header: ({ column }) => , + }, + { + accessorKey: "phone", + header: ({ column }) => , + }, + { + accessorKey: "position", + header: ({ column }) => , + }, + { + accessorKey: "department", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).department?.name ?? "—", + }, + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => { + const status = row.original.status + return ( + + {status} + + ) + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx b/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx new file mode 100644 index 0000000..c9dcf13 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx @@ -0,0 +1,50 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ShopCalendarForm } from "@/modules/shop-calendars/shop-calendar-form" +import { SHOP_CALENDAR_ROUTES } from "@garage/api" +import type { ShopCalendarsClient } from "@garage/api" +import { CheckCircle2Icon } from "lucide-react" + +export default function ShopCalendarsPage() { + return ( + + pageTitle="Shop Calendars" + title="Shop Calendar" + routeKey={SHOP_CALENDAR_ROUTES.INDEX} + getClient={(api) => api.shopCalendars} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "is_default", + header: ({ column }) => , + cell: ({ row }) => + (row.original as any).is_default ? ( + + ) : null, + }, + { + accessorKey: "shop_calender_days", + header: () => Days, + enableSorting: false, + cell: ({ row }) => { + const days = (row.original as any).shop_calender_days + return days?.length ?? 0 + }, + }, + actionsColumn({ onEdit: undefined }), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx b/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx new file mode 100644 index 0000000..46b7150 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx @@ -0,0 +1,57 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ShopTimingForm } from "@/modules/shop-timings/shop-timing-form" +import { SHOP_TIMING_ROUTES } from "@garage/api" +import type { ShopTimingsClient } from "@garage/api" +import { CheckCircle2Icon } from "lucide-react" + +export default function ShopTimingsPage() { + return ( + + pageTitle="Shop Timings" + title="Shop Timing" + routeKey={SHOP_TIMING_ROUTES.INDEX} + getClient={(api) => api.shopTimings} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "in_time", + header: ({ column }) => , + }, + { + accessorKey: "out_time", + header: ({ column }) => , + }, + { + accessorKey: "full_day_hours", + header: ({ column }) => , + }, + { + accessorKey: "half_day_hours", + header: ({ column }) => , + }, + { + accessorKey: "is_default", + header: ({ column }) => , + cell: ({ row }) => + row.original.is_default ? ( + + ) : null, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/sales/customers/page.tsx b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx new file mode 100644 index 0000000..0be2ff7 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx @@ -0,0 +1,54 @@ +"use client" + +import { ResourcePage } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import { CustomerForm } from '@/modules/customers/customer-form' +import { CUSTOMER_ROUTES } from '@garage/api' +import type { CustomersClient } from '@garage/api' +import { Building2Icon, UserIcon } from 'lucide-react' + +export default function CustomersPage() { + return ( + + pageTitle='Customers' + title="Customer" + routeKey={CUSTOMER_ROUTES.INDEX} + getClient={(api) => api.customers} + columns={({ actionsColumn }) => [ + + { + accessorKey: "first_name", + header: ({ column }) => , + cell: ({ row }) => { + const customerName = row.original.first_name + const isCompany = row.original.customer_type?.name?.toLocaleLowerCase() === "company"; + const companyName = row.original.company_name + const name = isCompany && companyName ? `${customerName} (${row.original.last_name})` : customerName + + return (
+ {isCompany ? : } + {name} +
+ ) + }, + }, + { + accessorKey: "email", + header: ({ column }) => , + }, + { + accessorKey: "phone", + header: ({ column }) => , + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} \ No newline at end of file diff --git a/apps/dashboard/app/(authenticated)/sales/inspections/page.tsx b/apps/dashboard/app/(authenticated)/sales/inspections/page.tsx new file mode 100644 index 0000000..58417c2 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/sales/inspections/page.tsx @@ -0,0 +1,65 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { InspectionForm } from "@/modules/inspections/inspection-form" +import { INSPECTION_ROUTES } from "@garage/api" +import type { InspectionsClient } from "@garage/api" + +export default function InspectionsPage() { + return ( + + pageTitle="Inspections" + title="Inspection" + routeKey={INSPECTION_ROUTES.INDEX} + getClient={(api) => api.inspections} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "customer", + header: ({ column }) => , + cell: ({ row }) => { + const c = (row.original as any).customer + return c ? `${c.first_name ?? ""} ${c.last_name ?? ""}`.trim() : "—" + }, + }, + { + accessorKey: "vehicle", + header: ({ column }) => , + cell: ({ row }) => { + const v = (row.original as any).vehicle + return v ? `${v.make ?? ""} ${v.model ?? ""}`.trim() : "—" + }, + }, + { + accessorKey: "inspection_category", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).inspection_category?.name ?? "—", + }, + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => { + const status = (row.original as any).status + return ( + + {status ?? "—"} + + ) + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx new file mode 100644 index 0000000..f310d47 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx @@ -0,0 +1,98 @@ +"use client" + +import { ResourcePage } from '@/shared/data-view/resource-page' +import { ColumnHeader } from '@/shared/data-view/table-view' +import { VehicleForm } from '@/modules/vehicles/vehicle-form' +import { VEHICLE_ROUTES } from '@garage/api' +import type { VehiclesClient } from '@garage/api' +import { CarIcon } from 'lucide-react' + +export default function VehiclesPage() { + return ( + + pageTitle="Vehicles" + title="Vehicle" + routeKey={VEHICLE_ROUTES.INDEX} + getClient={(api) => api.vehicles} + + + columns={({ actionsColumn }) => [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => { + const r = row.original as any + const make = r.make ?? "" + const model = r.model ?? "" + const display = r.name || `${make} ${model}`.trim() || "—" + return ( +
+ +
+ {display} + {r.sub_model && ( + {r.sub_model} + )} +
+
+ ) + }, + }, + { + accessorKey: "year", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).year ?? "—", + }, + { + accessorKey: "license_plate", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).license_plate + return val + ? {val} + : "—" + }, + }, + { + accessorKey: "vin_number", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).vin_number + return val + ? {val} + : "—" + }, + }, + { + accessorKey: "engine_size", + header: ({ column }) => , + cell: ({ row }) => (row.original as any).engine_size ?? "—", + }, + { + accessorKey: "mileage", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).mileage + return val != null ? `${Number(val).toLocaleString()} mi` : "—" + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => , + cell: ({ row }) => { + const val = (row.original as any).created_at + return val ? new Date(val).toLocaleDateString() : "—" + }, + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} \ No newline at end of file diff --git a/apps/dashboard/app/(authenticated)/settings/shop-type/page.tsx b/apps/dashboard/app/(authenticated)/settings/shop-type/page.tsx new file mode 100644 index 0000000..dca0f82 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/settings/shop-type/page.tsx @@ -0,0 +1,54 @@ +"use client" + +import { ResourcePage } from "@/shared/data-view/resource-page" +import { ColumnHeader } from "@/shared/data-view/table-view" +import { ShopTypeForm } from "@/modules/settings/shop-type/shop-type-form" +import { SHOP_TYPE_ROUTES } from "@garage/api" +import type { ShopTypesClient } from "@garage/api" +import { CheckIcon, XIcon } from "lucide-react" + +export default function ShopTypesPage() { + return ( + + pageTitle="Shop Types" + title="Shop Type" + routeKey={SHOP_TYPE_ROUTES.INDEX} + getClient={(api) => api.shopTypes} + columns={({ actionsColumn }) => [ + { + accessorKey: "title", + header: ({ column }) => , + }, + { + accessorKey: "shop_type", + header: ({ column }) => , + }, + { + accessorKey: "note", + header: ({ column }) => , + cell: ({ row }) => ( + + {(row.original as any).note ?? "—"} + + ), + }, + { + accessorKey: "is_default", + header: ({ column }) => , + cell: ({ row }) => + (row.original as any).is_default + ? + : , + }, + actionsColumn(), + ]} + renderForm={({ resourceId, initialData, onSuccess }) => ( + + )} + /> + ) +} diff --git a/apps/dashboard/app/favicon.ico b/apps/dashboard/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/apps/dashboard/app/globals.css b/apps/dashboard/app/globals.css new file mode 100644 index 0000000..d4cf055 --- /dev/null +++ b/apps/dashboard/app/globals.css @@ -0,0 +1,179 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-heading: var(--font-sans); + --font-sans: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); + + --shadow-glow : 0 0 10px var(--primary); +} + +:root { + --background: oklch(96.416% 0.00011 271.152); + --foreground: oklch(0.062 0 0); + --card: oklch(0.975 0 0); + --card-foreground: oklch(0.281 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.577 0.245 27.325); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.949 0 0); + --muted-foreground: oklch(0.6 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(75.417% 0.14818 18.15); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + } + + html { + @apply font-sans; + } +} + + +@layer utilities { + .dashboard-nav-item { + @apply + relative + overflow-hidden + data-active:bg-primary/10 + data-active:text-primary + data-active:hover:text-primary + data-active:hover:bg-primary/15 + transition-all + duration-300; + } + + /* Accent bar — only in expanded mode */ + /* .dashboard-nav-item:not([data-collapsed="true"])::after { + content: ""; + position: absolute; + inset-inline-end: 0.25rem; + height: 80%; + border-radius: var(--radius-md); + z-index: 10; + box-shadow: 0 0 6px var(--primary); + } + + .dashboard-nav-item:not([data-collapsed="true"])[data-active="true"]::after { + width: 0.25rem; + background-color: var(--primary); + } */ + + /* Collapsed mode: icon centered, no bar */ + .dashboard-nav-item[data-collapsed="true"] { + @apply justify-center; + } + + .dashboard-nav-sub-item { + @apply + transition-colors + duration-200 + data-active:text-primary + data-active:font-medium + data-active:bg-primary/5 + hover:text-primary/80; + } +} \ No newline at end of file diff --git a/apps/dashboard/app/layout.tsx b/apps/dashboard/app/layout.tsx new file mode 100644 index 0000000..326b949 --- /dev/null +++ b/apps/dashboard/app/layout.tsx @@ -0,0 +1,40 @@ +import { Geist_Mono, Inter } from "next/font/google" + +import "./globals.css" +import { QueryProvider } from "@/shared/components/query-provider" +import { ThemeProvider } from "@/shared/components/theme-provider" +import { Toaster } from "@/shared/components/ui/sonner" +import { ConfirmDialog } from "@/shared/components/confirm-dialog" +import { NuqsAdapter } from "nuqs/adapters/next/app" +import { cn } from "@/shared/lib/utils" + +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }) + +const fontMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-mono", +}) + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + + {children} + + + + + + + ) +} diff --git a/apps/dashboard/base/components/auth-store-initializer.tsx b/apps/dashboard/base/components/auth-store-initializer.tsx new file mode 100644 index 0000000..7018b2c --- /dev/null +++ b/apps/dashboard/base/components/auth-store-initializer.tsx @@ -0,0 +1,19 @@ +"use client" + +import { useRef } from "react" +import { useAuthStore } from "@/shared/stores/auth-store" +import type { AuthUser } from "@garage/api" + +/** + * Synchronously initializes the auth store from server-side token/user before + * any child component renders. This avoids the first-render race condition where + * useEffect-based hydration hasn't fired yet and API requests go out without a token. + */ +export function AuthStoreInitializer({ token, user }: { token: string; user: AuthUser }) { + const initialized = useRef(false) + if (!initialized.current) { + initialized.current = true + useAuthStore.setState({ token, user, isAuthenticated: true }) + } + return null +} diff --git a/apps/dashboard/base/components/layout/dashboard/app-sidebar.tsx b/apps/dashboard/base/components/layout/dashboard/app-sidebar.tsx new file mode 100644 index 0000000..7e89e1e --- /dev/null +++ b/apps/dashboard/base/components/layout/dashboard/app-sidebar.tsx @@ -0,0 +1,240 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { ChevronRight, Circle } from "lucide-react" + +import type { NavGroup, NavItem } from "@/base/types/navigation" +import { cn } from "@/shared/lib/utils" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/shared/components/ui/collapsible" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarRail, + useSidebar, +} from "@/shared/components/ui/sidebar" + +type AppSidebarProps = React.ComponentProps & { + navGroups: NavGroup[] + logo?: React.ReactNode +} + +export function AppSidebar({ navGroups, logo, ...props }: AppSidebarProps) { + const { state, isMobile } = useSidebar() + const isCollapsed = state === "collapsed" && !isMobile + + return ( + + {logo && ( + + {logo} + + )} + + {navGroups.map((group, groupIndex) => ( + + {group.label && ( + + {group.label} + + )} + + {group.items.map((item) => + item.items && item.items.length > 0 ? ( + + ) : ( + + ) + )} + + + ))} + + + + ) +} + +function SimpleNavItem({ item, isCollapsed }: { item: NavItem; isCollapsed: boolean }) { + const pathname = usePathname() + const isActive = item.isActive ?? pathname === item.href + + return ( + + + + {item.icon} + { + !isCollapsed && + {item.title} + } + + + + ) +} + +function CollapsibleNavItem({ item, isCollapsed }: { item: NavItem; isCollapsed: boolean }) { + const pathname = usePathname() + const isChildActive = item.items?.some((sub) => pathname === sub.href) + const isActive = item.isActive ?? (pathname === item.href || isChildActive === true) + + // Collapsed sidebar → flyout dropdown with sub-items + if (isCollapsed) { + return ( + + + + + + {item.icon} + + { + !isCollapsed && + {item.title} + } + + + + + {item.title} + + + {item.items?.map((sub) => { + const isSubActive = sub.isActive ?? pathname === sub.href + return ( + + + {sub.icon ? ( + svg]:size-4", isSubActive ? "text-primary" : "text-muted-foreground/70")}> + {sub.icon} + + ) : ( + + )} + {sub.title} + + + ) + })} + + + + ) + } + + // Expanded sidebar → collapsible/accordion sub-menu + return ( + + + + + + {item.icon} + + + + {item.title} + + + + + + + {item.items?.map((sub) => { + const isSubActive = sub.isActive ?? pathname === sub.href + return ( + + + + {sub.icon ? ( + svg]:size-4", isSubActive ? "text-primary" : "text-muted-foreground/70 group-hover/menu-sub-item:text-primary")}> + {sub.icon} + + ) : ( + + )} + {sub.title} + + + + ) + })} + + + + + ) +} diff --git a/apps/dashboard/base/components/layout/dashboard/dashboard-header.tsx b/apps/dashboard/base/components/layout/dashboard/dashboard-header.tsx new file mode 100644 index 0000000..17ab906 --- /dev/null +++ b/apps/dashboard/base/components/layout/dashboard/dashboard-header.tsx @@ -0,0 +1,210 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { useTheme } from "next-themes" +import { + BellIcon, + LogOutIcon, + MoonIcon, + SearchIcon, + SunIcon, + UserIcon, +} from "lucide-react" + +import type { UserInfo } from "@/base/types/navigation" +import { useAuthStore } from "@/shared/stores/auth-store" +import { cn } from "@/shared/lib/utils" +import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar" +import { Button } from "@/shared/components/ui/button" +import { SidebarTrigger } from "@/shared/components/ui/sidebar" +import { + CommandDialog, + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +} from "@/shared/components/ui/command" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu" +import { Separator } from "@/shared/components/ui/separator" + +type DashboardHeaderProps = { + user?: UserInfo + actions?: React.ReactNode + className?: string +} + +export function DashboardHeader({ actions, className }: DashboardHeaderProps) { + const { resolvedTheme, setTheme } = useTheme() + const [searchOpen, setSearchOpen] = useState(false) + const { logout, user } = useAuthStore((s) => s) + const router = useRouter() + + const handleLogout = useCallback(async () => { + await logout() + router.push("/login") + }, [logout, router]) + + useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault() + setSearchOpen((prev) => !prev) + } + } + window.addEventListener("keydown", onKeyDown) + return () => window.removeEventListener("keydown", onKeyDown) + }, []) + + const toggleTheme = useCallback(() => { + setTheme(resolvedTheme === "dark" ? "light" : "dark") + }, [resolvedTheme, setTheme]) + + return ( +
+ {/* Sidebar toggle — mobile: hamburger, desktop: collapse */} + + + + {/* Left side — default actions */} +
+ {/* User dropdown */} + {/* {user && ( */} + + + + + + + {/* User info header */} + +
+ + {user?.avatar && } + + {user?.initials ?? user?.name.charAt(0).toUpperCase()} + + +
+ {user?.name} + {user?.email && ( + {user?.email} + )} + {user?.role && ( + {user?.role} + )} +
+
+
+ + + + + + + + Profile + + + + + + + + + Logout + +
+
+ {/* )} */} + + + {/* Search trigger */} + + + {/* Mobile search icon */} + + + {/* Theme toggle */} + + + {/* Notifications */} + +
+ + {/* Search command dialog */} + + + + + No results found. + + Dashboard + Job Cards + Customers + + + + + + {/* Right side — custom actions */} + {actions && ( +
{actions}
+ )} +
+ ) +} diff --git a/apps/dashboard/base/components/layout/dashboard/dashboard-layout.tsx b/apps/dashboard/base/components/layout/dashboard/dashboard-layout.tsx new file mode 100644 index 0000000..4152104 --- /dev/null +++ b/apps/dashboard/base/components/layout/dashboard/dashboard-layout.tsx @@ -0,0 +1,41 @@ +"use client" + +import type { NavGroup, UserInfo } from "@/base/types/navigation" +import { SidebarInset, SidebarProvider } from "@/shared/components/ui/sidebar" +import { TooltipProvider } from "@/shared/components/ui/tooltip" +import { AppSidebar } from "./app-sidebar" +import { DashboardHeader } from "./dashboard-header" + +type DashboardLayoutProps = { + children: React.ReactNode + /** Navigation groups rendered in the sidebar */ + navGroups: NavGroup[] + /** Logo element displayed at the top of the sidebar */ + logo?: React.ReactNode + /** Current user info shown in the header */ + user?: UserInfo + /** Custom actions rendered in the header (e.g. session timer, clock-in button) */ + headerActions?: React.ReactNode + /** Default sidebar open state */ + defaultOpen?: boolean +} + +export function DashboardLayout({ + children, + navGroups, + logo, + user, + headerActions, + defaultOpen = true, +}: DashboardLayoutProps) { + return ( + + + + + {children} + + + + ) +} diff --git a/apps/dashboard/base/components/layout/dashboard/dashboard-page.tsx b/apps/dashboard/base/components/layout/dashboard/dashboard-page.tsx new file mode 100644 index 0000000..08325ae --- /dev/null +++ b/apps/dashboard/base/components/layout/dashboard/dashboard-page.tsx @@ -0,0 +1,20 @@ +import { cn } from '@/shared/lib/utils' +import { title } from 'process' +import React from 'react' + +export default function DashboardPage({ children, header, title, fullscreen }: { children: React.ReactNode, header: React.ReactNode, title?: string, fullscreen?: boolean }) { + return ( +
+
+ {header} +
+
+ { + title && +

{title}

+ } + {children} +
+
+ ) +} diff --git a/apps/dashboard/base/components/layout/dashboard/index.ts b/apps/dashboard/base/components/layout/dashboard/index.ts new file mode 100644 index 0000000..21d6b95 --- /dev/null +++ b/apps/dashboard/base/components/layout/dashboard/index.ts @@ -0,0 +1,3 @@ +export { DashboardLayout } from "./dashboard-layout" +export { AppSidebar } from "./app-sidebar" +export { DashboardHeader } from "./dashboard-header" diff --git a/apps/dashboard/base/types/navigation.ts b/apps/dashboard/base/types/navigation.ts new file mode 100644 index 0000000..93fd3c0 --- /dev/null +++ b/apps/dashboard/base/types/navigation.ts @@ -0,0 +1,31 @@ +import { ReactNode } from "react" + + +export type NavItem = { + title: string + href: string + icon?: ReactNode + isActive?: boolean + badge?: string | number + items?: NavSubItem[] +} + +export type NavSubItem = { + title: string + href: string + icon?: ReactNode + isActive?: boolean +} + +export type NavGroup = { + label?: string + items: NavItem[] +} + +export type UserInfo = { + name: string + email?: string + avatar?: string + initials?: string + role?: string +} diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json new file mode 100644 index 0000000..900b31e --- /dev/null +++ b/apps/dashboard/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-vega", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": true, + "aliases": { + "components": "@/shared/components", + "utils": "@/shared/lib/utils", + "ui": "@/shared/components/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/dashboard/cypress.config.ts b/apps/dashboard/cypress.config.ts new file mode 100644 index 0000000..830bcbc --- /dev/null +++ b/apps/dashboard/cypress.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "cypress" + +export default defineConfig({ + e2e: { + baseUrl: "http://localhost:3000", + env: { + NEXT_PUBLIC_API_URL: "https://newgarage.yslootahtech.com" + }, + specPattern: "cypress/e2e/**/*.cy.{ts,tsx}", + supportFile: "cypress/support/e2e.ts", + viewportWidth: 1280, + viewportHeight: 720, + defaultCommandTimeout: 10000, + requestTimeout: 10000, + }, +}) diff --git a/apps/dashboard/cypress/e2e/customers/customer-form-integration.cy.ts b/apps/dashboard/cypress/e2e/customers/customer-form-integration.cy.ts new file mode 100644 index 0000000..5141b99 --- /dev/null +++ b/apps/dashboard/cypress/e2e/customers/customer-form-integration.cy.ts @@ -0,0 +1,293 @@ +describe("Customer Form – Integration Tests", () => { + beforeEach(() => { + cy.login() + + cy.fixture("customers").then((data) => { + cy.intercept("GET", "**/api/referral-sources", { + statusCode: 200, + body: data.referral_sources, + }).as("getReferralSources") + + cy.intercept("GET", "**/api/payment-terms", { + statusCode: 200, + body: data.payment_terms, + }).as("getPaymentTerms") + + cy.intercept("GET", "**/api/countries", { + statusCode: 200, + body: data.countries, + }).as("getCountries") + + cy.intercept("GET", "**/api/states", { + statusCode: 200, + body: data.states, + }).as("getStates") + + cy.intercept("GET", "**/api/customers*", { + statusCode: 200, + body: { success: true, data: { data: [], pagination: { total: 0 } } }, + }).as("getCustomers") + }) + + cy.visit("/sales/customers") + cy.contains("button", "Create Customer").click() + cy.get("[role='dialog']").should("be.visible") + }) + + // ── Form interaction flow ── + + describe("Field interactions", () => { + it("should clear a text field after typing", () => { + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']") + .type("John") + .should("have.value", "John") + .clear() + .should("have.value", "") + }) + }) + + it("should handle special characters in text inputs", () => { + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("José-María").should("have.value", "José-María") + cy.get("input[name='last_name']").type("O'Brien").should("have.value", "O'Brien") + cy.get("input[name='company_name']").type("Smith & Co.").should("have.value", "Smith & Co.") + }) + }) + + it("should accept various email formats", () => { + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("Test") + cy.get("input[name='last_name']").type("User") + + // Valid email should not show error + cy.get("input[name='email']").type("user+tag@sub.domain.com") + cy.contains("button", "Create Customer").click() + cy.contains("Enter a valid email address").should("not.exist") + }) + }) + + it("should handle phone number input", () => { + cy.get("[role='dialog']").within(() => { + cy.get("input[name='phone']") + .type("0501234567") + .should("have.value", "0501234567") + + cy.get("input[name='alternate_phone']") + .type("+971501234567") + .should("have.value", "+971501234567") + }) + }) + }) + + // ── Async select integration ── + + describe("Async select fields", () => { + it("should show loading state while fetching referral sources", () => { + cy.intercept("GET", "**/api/referral-sources", { + statusCode: 200, + body: { success: true, data: { data: [{ id: 1, name: "Google" }] } }, + delay: 2000, + }).as("slowReferralSources") + + // Reload to get the delayed intercept + cy.visit("/sales/customers") + cy.contains("button", "Create Customer").click() + cy.get("[role='dialog']").should("be.visible") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Referral Source").parent().find("input").click() + }) + + // The component should show a loading spinner + cy.get("[role='listbox']").should("be.visible") + }) + + it("should filter options by text input in combobox", () => { + cy.wait("@getReferralSources") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Referral Source").parent().find("input").click().type("Goo") + }) + + // Should show Google, shouldn't show Friend Referral + cy.get("[role='option']").contains("Google").should("exist") + }) + + it("should show empty state when no options match", () => { + cy.wait("@getCountries") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Country").parent().find("input").click().type("zzzzz") + }) + + cy.contains("No results found").should("be.visible") + }) + + it("should select a payment term from the combobox", () => { + cy.wait("@getPaymentTerms") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Payment Terms").parent().find("input").click() + }) + + cy.get("[role='option']").contains("Net 30").click() + }) + + it("should select a state from the combobox", () => { + cy.wait("@getStates") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "State").parent().find("input").click() + }) + + cy.get("[role='option']").contains("Dubai").click() + }) + }) + + // ── Validation edge cases ── + + describe("Validation edge cases", () => { + it("should validate only on submit (not on blur)", () => { + cy.get("[role='dialog']").within(() => { + // Focus and blur first_name without typing + cy.get("input[name='first_name']").focus().blur() + + // Error should NOT appear yet (react-hook-form validates on submit by default) + cy.contains("First name is required").should("not.exist") + }) + }) + + it("should clear validation errors when user corrects input", () => { + cy.get("[role='dialog']").within(() => { + // Trigger validation + cy.contains("button", "Create Customer").click() + cy.contains("First name is required").should("be.visible") + + // Fix the error + cy.get("input[name='first_name']").type("John") + cy.contains("First name is required").should("not.exist") + }) + }) + + it("should trim whitespace-only inputs and still require first_name", () => { + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type(" ") + cy.get("input[name='last_name']").type("Doe") + cy.contains("button", "Create Customer").click() + }) + }) + + it("should allow submission with only required fields", () => { + cy.fixture("customers").then((data) => { + cy.intercept("POST", "**/api/customers", { + statusCode: 201, + body: data.customer_created, + }).as("createCustomer") + }) + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("Jane") + cy.get("input[name='last_name']").type("Smith") + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@createCustomer").its("request.body").should((body) => { + expect(body.first_name).to.eq("Jane") + expect(body.last_name).to.eq("Smith") + // Optional fields should be empty or undefined + expect(body.company_name).to.satisfy( + (v: unknown) => v === "" || v === undefined || v === null, + ) + }) + }) + }) + + // ── API error scenarios ── + + describe("API error handling", () => { + it("should handle network error gracefully", () => { + cy.intercept("POST", "**/api/customers", { forceNetworkError: true }).as( + "networkError", + ) + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@networkError") + + cy.get("[role='dialog']").within(() => { + cy.contains("Failed to create customer").should("be.visible") + }) + }) + + it("should handle 500 server error", () => { + cy.intercept("POST", "**/api/customers", { + statusCode: 500, + body: { success: false, message: "Internal server error" }, + }).as("serverError") + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@serverError") + + cy.get("[role='dialog']").within(() => { + cy.contains("Failed to create customer").should("be.visible") + }) + }) + + it("should handle 422 validation error from server", () => { + cy.intercept("POST", "**/api/customers", { + statusCode: 422, + body: { + success: false, + message: "The email has already been taken.", + errors: { email: ["The email has already been taken."] }, + }, + }).as("validationError") + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.get("input[name='email']").type("existing@example.com") + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@validationError") + + cy.get("[role='dialog']").within(() => { + cy.contains("Failed to create customer").should("be.visible") + }) + }) + + it("should re-enable submit button after a failed request", () => { + cy.intercept("POST", "**/api/customers", { + statusCode: 422, + body: { + success: false, + message: "Validation failed", + errors: {}, + }, + }).as("failedRequest") + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@failedRequest") + + cy.get("[role='dialog']").within(() => { + cy.contains("button", "Create Customer").should("not.be.disabled") + }) + }) + }) +}) diff --git a/apps/dashboard/cypress/e2e/customers/customer-form.cy.ts b/apps/dashboard/cypress/e2e/customers/customer-form.cy.ts new file mode 100644 index 0000000..7779ca5 --- /dev/null +++ b/apps/dashboard/cypress/e2e/customers/customer-form.cy.ts @@ -0,0 +1,347 @@ +describe("Customer Form", () => { + beforeEach(() => { + // Authenticate via API and set cookies + cy.login() + + // Intercept lookup APIs with fixture data + cy.fixture("customers").then((data) => { + cy.intercept("GET", "**/api/referral-sources", { + statusCode: 200, + body: data.referral_sources, + }).as("getReferralSources") + + cy.intercept("GET", "**/api/payment-terms", { + statusCode: 200, + body: data.payment_terms, + }).as("getPaymentTerms") + + cy.intercept("GET", "**/api/countries", { + statusCode: 200, + body: data.countries, + }).as("getCountries") + + cy.intercept("GET", "**/api/states", { + statusCode: 200, + body: data.states, + }).as("getStates") + + // Intercept customer list (GET) for the data table + cy.intercept("GET", "**/api/customers*", { + statusCode: 200, + body: { success: true, data: { data: [], pagination: { total: 0 } } }, + }).as("getCustomers") + }) + + cy.visit("/sales/customers") + }) + + function openCustomerDialog() { + cy.contains("button", "Create Customer").click() + cy.get("[role='dialog']").should("be.visible") + } + + // ── Rendering ── + + it("should open the create customer dialog", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.contains("Create Customer").should("exist") + }) + }) + + it("should display all form fields", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + // Text fields + cy.get("input[name='first_name']").should("exist") + cy.get("input[name='last_name']").should("exist") + cy.get("input[name='company_name']").should("exist") + cy.get("input[name='email']").should("exist") + cy.get("input[name='phone']").should("exist") + cy.get("input[name='alternate_phone']").should("exist") + cy.get("input[name='address_line_1']").should("exist") + cy.get("input[name='address_line_2']").should("exist") + cy.get("input[name='city']").should("exist") + cy.get("input[name='zip_code']").should("exist") + + // Labels + cy.contains("label", "First Name").should("exist") + cy.contains("label", "Last Name").should("exist") + cy.contains("label", "Email").should("exist") + cy.contains("label", "Salutation").should("exist") + cy.contains("label", "Customer Type").should("exist") + cy.contains("label", "Referral Source").should("exist") + cy.contains("label", "Payment Terms").should("exist") + cy.contains("label", "Country").should("exist") + cy.contains("label", "State").should("exist") + }) + }) + + // ── Validation ── + + it("should show validation errors for required fields", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.contains("button", "Create Customer").click() + + // first_name and last_name are required + cy.contains("First name is required").should("be.visible") + cy.contains("Last name is required").should("be.visible") + }) + }) + + it("should show email validation error for invalid email", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='email']").type("not-an-email") + cy.contains("button", "Create Customer").click() + + cy.contains("Enter a valid email address").should("be.visible") + }) + }) + + it("should not show email error when email is empty", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.contains("button", "Create Customer").click() + + cy.contains("Enter a valid email address").should("not.exist") + }) + }) + + // ── Text input ── + + it("should fill in text fields", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John").should("have.value", "John") + cy.get("input[name='last_name']").type("Doe").should("have.value", "Doe") + cy.get("input[name='company_name']").type("Acme Corp").should("have.value", "Acme Corp") + cy.get("input[name='email']").type("john@example.com").should("have.value", "john@example.com") + cy.get("input[name='phone']").type("0501234567").should("have.value", "0501234567") + cy.get("input[name='address_line_1']").type("123 Main St").should("have.value", "123 Main St") + cy.get("input[name='city']").type("Dubai").should("have.value", "Dubai") + cy.get("input[name='zip_code']").type("00000").should("have.value", "00000") + }) + }) + + // ── Select fields ── + + it("should select a salutation from the dropdown", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + // Click the Salutation select trigger + cy.contains("label", "Salutation") + .parent() + .find("[role='combobox'], button[data-slot='select-trigger']") + .click() + }) + + // Select option from the popover (may render outside the dialog) + cy.get("[role='option'], [role='listbox'] [data-value='Mr']") + .contains("Mr") + .click() + }) + + it("should select a customer type", () => { + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Customer Type") + .parent() + .find("[role='combobox'], button[data-slot='select-trigger']") + .click() + }) + + cy.get("[role='option']").contains("Individual").click() + }) + + // ── Async select (Combobox) fields ── + + it("should load and select a referral source", () => { + openCustomerDialog() + + cy.wait("@getReferralSources") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Referral Source") + .parent() + .find("input") + .click() + .type("Google") + }) + + cy.get("[role='option']").contains("Google").click() + }) + + it("should load and select a country", () => { + openCustomerDialog() + + cy.wait("@getCountries") + + cy.get("[role='dialog']").within(() => { + cy.contains("label", "Country") + .parent() + .find("input") + .click() + .type("United") + }) + + cy.get("[role='option']").contains("United Arab Emirates").click() + }) + + // ── Successful submission ── + + it("should submit the form successfully with required fields", () => { + cy.fixture("customers").then((data) => { + cy.intercept("POST", "**/api/customers", { + statusCode: 201, + body: data.customer_created, + }).as("createCustomer") + }) + + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@createCustomer").its("request.body").should((body) => { + expect(body.first_name).to.eq("John") + expect(body.last_name).to.eq("Doe") + }) + }) + + it("should submit a fully filled form", () => { + cy.fixture("customers").then((data) => { + cy.intercept("POST", "**/api/customers", { + statusCode: 201, + body: data.customer_created, + }).as("createCustomer") + }) + + openCustomerDialog() + + // Wait for async data + cy.wait("@getReferralSources") + cy.wait("@getPaymentTerms") + cy.wait("@getCountries") + cy.wait("@getStates") + + cy.get("[role='dialog']").within(() => { + // Text fields + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.get("input[name='company_name']").type("Doe Holdings") + cy.get("input[name='email']").type("john@example.com") + cy.get("input[name='phone']").type("0501234567") + cy.get("input[name='alternate_phone']").type("0551234567") + cy.get("input[name='address_line_1']").type("Street 10") + cy.get("input[name='address_line_2']").type("Near Central Plaza") + cy.get("input[name='city']").type("Dubai") + cy.get("input[name='zip_code']").type("00000") + + // Submit + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@createCustomer").its("request.body").should((body) => { + expect(body.first_name).to.eq("John") + expect(body.last_name).to.eq("Doe") + expect(body.company_name).to.eq("Doe Holdings") + expect(body.email).to.eq("john@example.com") + expect(body.phone).to.eq("0501234567") + }) + }) + + // ── Error handling ── + + it("should display API error on submission failure", () => { + cy.intercept("POST", "**/api/customers", { + statusCode: 422, + body: { + success: false, + message: "The given data was invalid.", + errors: { email: ["The email has already been taken."] }, + }, + }).as("createCustomerFail") + + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + cy.get("input[name='email']").type("john@example.com") + + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@createCustomerFail") + + cy.get("[role='dialog']").within(() => { + cy.contains("Failed to create customer").should("be.visible") + }) + }) + + it("should show loading state while submitting", () => { + cy.intercept("POST", "**/api/customers", { + statusCode: 201, + body: { success: true, data: { id: 1 } }, + delay: 1000, + }).as("createCustomerSlow") + + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + + cy.contains("button", "Create Customer").click() + + // Button should show loading text and be disabled + cy.contains("button", "Creating...").should("be.visible").and("be.disabled") + }) + }) + + // ── Form reset after success ── + + it("should reset the form after successful submission", () => { + cy.fixture("customers").then((data) => { + cy.intercept("POST", "**/api/customers", { + statusCode: 201, + body: data.customer_created, + }).as("createCustomer") + }) + + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").type("John") + cy.get("input[name='last_name']").type("Doe") + + cy.contains("button", "Create Customer").click() + }) + + cy.wait("@createCustomer") + + // After success, re-open the dialog and verify fields are empty + openCustomerDialog() + + cy.get("[role='dialog']").within(() => { + cy.get("input[name='first_name']").should("have.value", "") + cy.get("input[name='last_name']").should("have.value", "") + }) + }) +}) diff --git a/apps/dashboard/cypress/fixtures/customers.json b/apps/dashboard/cypress/fixtures/customers.json new file mode 100644 index 0000000..b232d94 --- /dev/null +++ b/apps/dashboard/cypress/fixtures/customers.json @@ -0,0 +1,47 @@ +{ + "referral_sources": { + "success": true, + "data": { + "data": [ + { "id": 1, "name": "Google" }, + { "id": 2, "name": "Friend Referral" }, + { "id": 3, "name": "Social Media" } + ] + } + }, + "payment_terms": { + "success": true, + "data": { + "data": [ + { "id": 1, "name": "Net 30" }, + { "id": 2, "name": "Net 60" }, + { "id": 3, "name": "Due on Receipt" } + ] + } + }, + "countries": { + "success": true, + "data": [ + { "id": 1, "name": "United Arab Emirates" }, + { "id": 2, "name": "Saudi Arabia" }, + { "id": 3, "name": "United States" } + ] + }, + "states": { + "success": true, + "data": [ + { "id": 1, "name": "Dubai" }, + { "id": 2, "name": "Abu Dhabi" }, + { "id": 3, "name": "Sharjah" } + ] + }, + "customer_created": { + "success": true, + "data": { + "id": 101, + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com" + } + } +} diff --git a/apps/dashboard/cypress/support/commands.ts b/apps/dashboard/cypress/support/commands.ts new file mode 100644 index 0000000..c1dd11e --- /dev/null +++ b/apps/dashboard/cypress/support/commands.ts @@ -0,0 +1,33 @@ +/// + +declare global { + namespace Cypress { + interface Chainable { + /** + * Log in via the API and set auth cookies so the app + * recognises the user as authenticated. + */ + login(email?: string, password?: string): Chainable + } + } +} + +Cypress.Commands.add("login", (email?: string, password?: string) => { + const userEmail = email ?? Cypress.env("TEST_USER_EMAIL") ?? "admin@admin.com" + const userPassword = password ?? Cypress.env("TEST_USER_PASSWORD") ?? "12345678" + + cy.request({ + method: "POST", + url: `${Cypress.env("API_URL") ?? "http://localhost:8000"}/api/login`, + body: { email: userEmail, password: userPassword }, + }).then((response) => { + const { token, user } = response.body + + cy.setCookie("auth_token", token, { path: "/" }) + cy.setCookie("auth_user", encodeURIComponent(JSON.stringify(user)), { + path: "/", + }) + }) +}) + +export {} diff --git a/apps/dashboard/cypress/support/e2e.ts b/apps/dashboard/cypress/support/e2e.ts new file mode 100644 index 0000000..b7cb303 --- /dev/null +++ b/apps/dashboard/cypress/support/e2e.ts @@ -0,0 +1 @@ +import "./commands" diff --git a/apps/dashboard/cypress/tsconfig.json b/apps/dashboard/cypress/tsconfig.json new file mode 100644 index 0000000..3b29d25 --- /dev/null +++ b/apps/dashboard/cypress/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["ES2017", "DOM"], + "types": ["cypress"], + "moduleResolution": "bundler", + "module": "ESNext", + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["../../*"] + } + }, + "include": ["**/*.ts", "../support/**/*.ts"] +} diff --git a/apps/dashboard/eslint.config.mjs b/apps/dashboard/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/apps/dashboard/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/apps/dashboard/modules/auth/auth.actions.ts b/apps/dashboard/modules/auth/auth.actions.ts new file mode 100644 index 0000000..c842aa4 --- /dev/null +++ b/apps/dashboard/modules/auth/auth.actions.ts @@ -0,0 +1,55 @@ +"use server" + +import { cookies } from "next/headers" +import type { AuthUser } from "@garage/api" + +const TOKEN_COOKIE = "auth_token" +const USER_COOKIE = "auth_user" +const DEFAULT_EXPIRES_IN = 60 * 60 * 24 * 7 // 7 days in seconds + +export async function setAuthCookies( + token: string, + user: AuthUser, + expiresIn: number = DEFAULT_EXPIRES_IN, +) { + const cookieStore = await cookies() + const expires = new Date(Date.now() + expiresIn * 1000) + + cookieStore.set(TOKEN_COOKIE, token, { + expires, + path: "/", + sameSite: "strict", + }) + + cookieStore.set(USER_COOKIE, JSON.stringify(user), { + expires, + path: "/", + sameSite: "strict", + }) +} + +export async function clearAuthCookies() { + const cookieStore = await cookies() + cookieStore.delete(TOKEN_COOKIE) + cookieStore.delete(USER_COOKIE) +} + +export async function getAuthCookies(): Promise<{ + token: string | undefined + user: AuthUser | undefined +}> { + const cookieStore = await cookies() + const token = cookieStore.get(TOKEN_COOKIE)?.value + const rawUser = cookieStore.get(USER_COOKIE)?.value + + let user: AuthUser | undefined + if (rawUser) { + try { + user = JSON.parse(rawUser) as AuthUser + } catch { + user = undefined + } + } + + return { token, user } +} diff --git a/apps/dashboard/modules/auth/login-form.schema.ts b/apps/dashboard/modules/auth/login-form.schema.ts new file mode 100644 index 0000000..dbe2b80 --- /dev/null +++ b/apps/dashboard/modules/auth/login-form.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +const loginFormSchema = z.object({ + email: z.string().trim().email("Enter a valid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), +}) + +type LoginFormValues = z.infer + +export { loginFormSchema } +export type { LoginFormValues } \ No newline at end of file diff --git a/apps/dashboard/modules/auth/login-form.tsx b/apps/dashboard/modules/auth/login-form.tsx new file mode 100644 index 0000000..cd620da --- /dev/null +++ b/apps/dashboard/modules/auth/login-form.tsx @@ -0,0 +1,148 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { Button } from "@/shared/components/ui/button" +import { api } from '@garage/api' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card" +import { + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, +} from "@/shared/components/ui/field" +import { Input } from "@/shared/components/ui/input" +import { useAppStore } from "@/shared/stores/app-store" +import { useAuthStore } from "@/shared/stores/auth-store" +import { cn } from "@/shared/lib/utils" +import Image from "next/image" +import { useRouter } from "next/navigation" + +import { loginFormSchema, type LoginFormValues } from "./login-form.schema" +import { useMutation } from "@tanstack/react-query" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { AlertTriangle } from "lucide-react" + +export function LoginForm({ + className, + ...props +}: React.ComponentProps<"div">) { + const lastLoginEmail = useAppStore((state) => state.lastLoginEmail) + const setLastLoginEmail = useAppStore((state) => state.setLastLoginEmail) + const login = useAuthStore((state) => state.login) + const router = useRouter() + const { + handleSubmit, + register, + formState: { errors, }, + } = useForm({ + resolver: zodResolver(loginFormSchema), + defaultValues: process.env.NODE_ENV === "development" ? { + "email": "admin@admin.com", + "password": "12345678" + } : { + email: lastLoginEmail, + password: "", + }, + }) + + const { mutate, error, isPending: isSubmitting } = useMutation({ + mutationFn: (values: LoginFormValues) => api.auth.login(values), + onSuccess: async (data) => { + if (data.token && data.user) { + await login(data.token, data.user as Parameters[1]) + router.push("/") + } + }, + }) + + + async function onSubmit(values: LoginFormValues) { + setLastLoginEmail(values.email) + mutate(values) + } + + return ( +
+ + + Logo + Login to your account + + Enter your email below to login to your account + + + + {error ? ( + + + Login failed + {error.message} + + ) : null} + +
+ + + Email + + + + + + + + + + + + {lastLoginEmail ? ( + + Last email used: {lastLoginEmail} + + ) : null} + {/* + Don't have an account? Sign up + */} + + +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/customers/customer-form.tsx b/apps/dashboard/modules/customers/customer-form.tsx new file mode 100644 index 0000000..7072d75 --- /dev/null +++ b/apps/dashboard/modules/customers/customer-form.tsx @@ -0,0 +1,264 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" + +import { + customerFormSchema, + type CustomerFormValues, +} from "./customer.schema" +import { CUSTOMER_ROUTES } from "@garage/api" + +// ── Constants ── + +const SALUTATION_OPTIONS = [ + { value: "Mr.", label: "Mr." }, + { value: "Mrs.", label: "Mrs." }, + { value: "Ms.", label: "Ms." }, + { value: "Miss", label: "Miss" }, + { value: "Dr.", label: "Dr." }, + { value: "Prof.", label: "Prof." }, +] + +// ── Props ── + +export type CustomerFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const CUSTOMER_DEFAULT_VALUES: CustomerFormValues = { + customer_type: null, + referral_source: null, + payment_terms: null, + country: null, + state: null, + salutation: "", + first_name: "", + last_name: "", + company_name: "", + email: "", + phone: "", + alternate_phone: "", + address_line_1: "", + address_line_2: "", + city: "", + zip_code: "", +} + +// ── Mapping helpers ── + +function mapCustomerToFormValues(data: unknown): CustomerFormValues { + const c = (data as any)?.data ?? data ?? {} + + return { + customer_type: toRelation(c.customer_type_id, c.customer_type_name), + referral_source: toRelation(c.referral_source_id, c.referral_source_name), + payment_terms: toRelation(c.payment_terms_id, c.payment_terms_name), + country: toRelation(c.country_id, c.country_name), + state: toRelation(c.state_id, c.state_name), + salutation: c.salutation || "", + first_name: c.first_name || "", + last_name: c.last_name || "", + company_name: c.company_name || "", + email: c.email || "", + phone: c.phone || "", + alternate_phone: c.alternate_phone || "", + address_line_1: c.address_line_1 || "", + address_line_2: c.address_line_2 || "", + city: c.city || "", + zip_code: c.zip_code || "", + } +} + +function mapFormToPayload(values: CustomerFormValues) { + return { + customer_type_id: toId(values.customer_type), + referral_source_id: toId(values.referral_source), + payment_terms_id: toId(values.payment_terms), + country_id: toId(values.country), + state_id: toId(values.state), + salutation: values.salutation || undefined, + first_name: values.first_name, + last_name: values.last_name, + company_name: values.company_name || undefined, + email: values.email || undefined, + phone: values.phone || undefined, + alternate_phone: values.alternate_phone || undefined, + address_line_1: values.address_line_1 || undefined, + address_line_2: values.address_line_2 || undefined, + city: values.city || undefined, + zip_code: values.zip_code || undefined, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name, +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: customerFormSchema, + defaultValues: CUSTOMER_DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api.customers.show(id), + queryKey: [CUSTOMER_ROUTES.BY_ID, resourceId], + mapToFormValues: mapCustomerToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: CustomerFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.customers.update(resourceId, payload) + : api.customers.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating customer..." : "Creating customer...", + success: isEditing ? "Customer updated successfully" : "Customer created successfully", + error: isEditing ? "Failed to update customer" : "Failed to create customer", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + {isEditing ? "Failed to update customer" : "Failed to create customer"} + {error.message} + + )} + + + + {/* Basic Info */} +
+ + api.customers.listCustomerTypes()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ + + {/* Name */} +
+ + +
+ + + + {/* Contact */} +
+ + +
+ + + + {/* Relations */} +
+ api.referralSources.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + api.paymentTerms.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ + {/* Address */} + + + +
+ api.geo.countries()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + api.geo.states()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ +
+ + +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/apps/dashboard/modules/customers/customer.schema.ts b/apps/dashboard/modules/customers/customer.schema.ts new file mode 100644 index 0000000..b17b582 --- /dev/null +++ b/apps/dashboard/modules/customers/customer.schema.ts @@ -0,0 +1,44 @@ +import { z } from "zod" + +/** + * Reusable schema for relation/lookup fields stored as `{ value, label }` objects. + * Use `.nullable()` when the field is optional but explicitly clearable. + */ +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +type RelationField = z.infer + +const customerFormSchema = z.object({ + // ── Relations (stored as objects, mapped to IDs on submit) ── + customer_type: relationFieldSchema, + referral_source: relationFieldSchema, + payment_terms: relationFieldSchema, + country: relationFieldSchema, + state: relationFieldSchema, + + // ── Basic info ── + salutation: z.string().optional(), + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + company_name: z.string().optional(), + + // ── Contact ── + email: z + .union([z.string().email("Enter a valid email address"), z.literal("")]) + .optional(), + phone: z.string().optional(), + alternate_phone: z.string().optional(), + + // ── Address ── + address_line_1: z.string().optional(), + address_line_2: z.string().optional(), + city: z.string().optional(), + zip_code: z.string().optional(), +}) + +type CustomerFormValues = z.infer + +export { customerFormSchema, relationFieldSchema } +export type { CustomerFormValues, RelationField } \ No newline at end of file diff --git a/apps/dashboard/modules/employees/employee-form.tsx b/apps/dashboard/modules/employees/employee-form.tsx new file mode 100644 index 0000000..404a7de --- /dev/null +++ b/apps/dashboard/modules/employees/employee-form.tsx @@ -0,0 +1,236 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfSelectField, + RhfAsyncSelectField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" + +import { + employeeFormSchema, + type EmployeeFormValues, +} from "./employee.schema" +import { EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, SHOP_TIMING_ROUTES, SHOP_CALENDAR_ROUTES } from "@garage/api" + +// ── Constants ── + +const STATUS_OPTIONS = [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, +] + +const TYPE_OPTIONS = [ + { value: "employee", label: "Employee" }, + { value: "contractor", label: "Contractor" }, +] + +// ── Props ── + +export type EmployeeFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: EmployeeFormValues = { + department: null, + shop_calender: null, + shop_timing: null, + first_name: "", + last_name: "", + email: "", + phone: "", + position: "", + status: "active", + type: "employee", + track_attendance: true, + notify_owner_when_punch_in_out: false, + geo_fence_radius: 100, +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): EmployeeFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + department: toRelation(d.department_id, d.department?.name), + shop_calender: toRelation(d.shop_calender_id, d.shop_calender?.title), + shop_timing: toRelation(d.shop_timing_id, d.shop_timing?.title), + first_name: d.first_name || "", + last_name: d.last_name || "", + email: d.email || "", + phone: d.phone || "", + position: d.position || "", + status: d.status || "active", + type: d.type || "employee", + track_attendance: d.track_attendance ?? true, + notify_owner_when_punch_in_out: d.notify_owner_when_punch_in_out ?? false, + geo_fence_radius: d.geo_fence_radius ?? 100, + } +} + +function mapFormToPayload(values: EmployeeFormValues) { + return { + department_id: toId(values.department), + shop_calender_id: toId(values.shop_calender), + shop_timing_id: toId(values.shop_timing), + first_name: values.first_name, + last_name: values.last_name, + email: values.email || undefined, + phone: values.phone || undefined, + position: values.position || undefined, + status: values.status || undefined, + type: values.type || undefined, + track_attendance: values.track_attendance, + notify_owner_when_punch_in_out: values.notify_owner_when_punch_in_out, + geo_fence_radius: values.geo_fence_radius, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function EmployeeForm({ resourceId, initialData, onSuccess }: EmployeeFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: employeeFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api.employees.show(id), + queryKey: [EMPLOYEE_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: EmployeeFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.employees.update(resourceId, payload) + : api.employees.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating employee..." : "Creating employee...", + success: isEditing ? "Employee updated successfully" : "Employee created successfully", + error: isEditing ? "Failed to update employee" : "Failed to create employee", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update employee" : "Failed to create employee"} + + {error.message} + + )} + + +
+ + +
+ +
+ + +
+ +
+ + api.departments.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ +
+ + +
+ +
+ api.shopCalendars.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> + api.shopTimings.list()} + mapOption={mapLookupOption} + {...STORE_OBJECT} + /> +
+ + + +
+ + +
+ + +
+
+ ) +} diff --git a/apps/dashboard/modules/employees/employee.schema.ts b/apps/dashboard/modules/employees/employee.schema.ts new file mode 100644 index 0000000..6a0f374 --- /dev/null +++ b/apps/dashboard/modules/employees/employee.schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod" + +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +const STATUS_OPTIONS = ["active", "inactive"] as const +const TYPE_OPTIONS = ["employee", "contractor"] as const + +const employeeFormSchema = z.object({ + department: relationFieldSchema, + shop_calender: relationFieldSchema, + shop_timing: relationFieldSchema, + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + email: z.union([ + z.string().email("Enter a valid email address"), + z.literal(""), + ]).optional(), + phone: z.string().optional(), + position: z.string().optional(), + status: z.string().optional(), + type: z.string().optional(), + track_attendance: z.boolean(), + notify_owner_when_punch_in_out: z.boolean(), + geo_fence_radius: z.coerce.number().min(0).optional(), +}) + +type EmployeeFormValues = z.infer + +export { employeeFormSchema, relationFieldSchema, STATUS_OPTIONS, TYPE_OPTIONS } +export type { EmployeeFormValues } diff --git a/apps/dashboard/modules/inspections/inline-forms/inspection-category-inline-form.tsx b/apps/dashboard/modules/inspections/inline-forms/inspection-category-inline-form.tsx new file mode 100644 index 0000000..215f0bc --- /dev/null +++ b/apps/dashboard/modules/inspections/inline-forms/inspection-category-inline-form.tsx @@ -0,0 +1,57 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + inspection_name: z.string().min(1, "Name is required"), +}) + +type FormValues = z.infer + +export function InspectionCategoryInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { inspection_name: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.inspections.createCategory({ + inspection_name: values.inspection_name, + }) + toast.success("Inspection category created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.name ?? values.inspection_name }) + } catch { + toast.error("Failed to create inspection category") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/inspections/inspection-form.tsx b/apps/dashboard/modules/inspections/inspection-form.tsx new file mode 100644 index 0000000..ce5f091 --- /dev/null +++ b/apps/dashboard/modules/inspections/inspection-form.tsx @@ -0,0 +1,231 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" +import { InspectionCategoryInlineForm } from "./inline-forms/inspection-category-inline-form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" + +import { + inspectionFormSchema, + type InspectionFormValues, +} from "./inspection.schema" +import { + INSPECTION_ROUTES, + CUSTOMER_ROUTES, + VEHICLE_ROUTES, + DEPARTMENT_ROUTES, + EMPLOYEE_ROUTES, +} from "@garage/api" + +// ── Props ── + +export type InspectionFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: InspectionFormValues = { + customer: null, + vehicle: null, + department: null, + inspection_category: null, + employee: null, + title: "", + order_number: "", + date: "", + time: "", +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): InspectionFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + customer: toRelation(d.customer_id, d.customer?.first_name ? `${d.customer.first_name} ${d.customer.last_name ?? ""}`.trim() : undefined), + vehicle: toRelation(d.vehicle_id, d.vehicle?.make ? `${d.vehicle.make} ${d.vehicle.model ?? ""}`.trim() : undefined), + department: toRelation(d.department_id, d.department?.name), + inspection_category: toRelation(d.inspection_category_id, d.inspection_category?.name), + employee: toRelation(d.employee_id, d.employee?.first_name ? `${d.employee.first_name} ${d.employee.last_name ?? ""}`.trim() : undefined), + title: d.title ?? "", + order_number: d.order_number ?? "", + date: d.date ?? "", + time: d.time ?? "", + } +} + +function mapFormToPayload(values: InspectionFormValues) { + return { + customer_id: toId(values.customer), + vehicle_id: toId(values.vehicle), + department_id: toId(values.department), + inspection_category_id: toId(values.inspection_category), + employee_id: toId(values.employee), + title: values.title, + order_number: values.order_number || undefined, + date: values.date || undefined, + time: values.time || undefined, + } +} + +// ── Shared mapOption for async selects ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const mapCustomerOption = (item: any) => ({ + value: String(item.id), + label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id), +}) + +const mapVehicleOption = (item: any) => ({ + value: String(item.id), + label: `${item.make ?? ""} ${item.model ?? ""} ${item.year ?? ""}`.trim() || String(item.id), +}) + +const mapEmployeeOption = (item: any) => ({ + value: String(item.id), + label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +// ── Component ── + +export function InspectionForm({ resourceId, initialData, onSuccess }: InspectionFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: inspectionFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + queryKey: [INSPECTION_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: InspectionFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.inspections.update(resourceId, payload) + : api.inspections.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating inspection..." : "Creating inspection...", + success: isEditing ? "Inspection updated successfully" : "Inspection created successfully", + error: isEditing ? "Failed to update inspection" : "Failed to create inspection", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update inspection" : "Failed to create inspection"} + + {error.message} + + )} + + + + +
+ api.customers.list()} + mapOption={mapCustomerOption} + {...STORE_OBJECT} + /> + api.vehicles.list()} + mapOption={mapVehicleOption} + {...STORE_OBJECT} + /> +
+ +
+ api.departments.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> + api.inspections.listCategories()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Inspection Category" + {...STORE_OBJECT} + /> +
+ + api.employees.list()} + mapOption={mapEmployeeOption} + {...STORE_OBJECT} + /> + + + +
+ + +
+ + +
+
+ ) +} diff --git a/apps/dashboard/modules/inspections/inspection.schema.ts b/apps/dashboard/modules/inspections/inspection.schema.ts new file mode 100644 index 0000000..05cdc71 --- /dev/null +++ b/apps/dashboard/modules/inspections/inspection.schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +const inspectionFormSchema = z.object({ + customer: relationFieldSchema, + vehicle: relationFieldSchema, + department: relationFieldSchema, + inspection_category: relationFieldSchema, + employee: relationFieldSchema, + title: z.string().min(1, "Title is required"), + order_number: z.string().optional(), + date: z.string().optional(), + time: z.string().optional(), +}) + +type InspectionFormValues = z.infer + +export { inspectionFormSchema, relationFieldSchema } +export type { InspectionFormValues } diff --git a/apps/dashboard/modules/parts/part-form.tsx b/apps/dashboard/modules/parts/part-form.tsx new file mode 100644 index 0000000..ede4b09 --- /dev/null +++ b/apps/dashboard/modules/parts/part-form.tsx @@ -0,0 +1,242 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { ShopTypeInlineForm } from "@/modules/vehicles/inline-forms/shop-type-inline-form" +import { InventoryCategoryInlineForm } from "@/modules/services/inline-forms/inventory-category-inline-form" +import { UnitTypeInlineForm } from "@/modules/services/inline-forms/unit-type-inline-form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toId } from "@/shared/lib/utils" + +import { partFormSchema, type PartFormValues } from "./part.schema" +import { PARTS_ROUTES } from "@garage/api" + +// ── Props ── + +export type PartFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: PartFormValues = { + shop_type: null, + category: null, + unit_type: null, + department: null, + title: "", + sku: "", + description: "", + selling_price: undefined, + purchase_price: undefined, +} + +// ── Mapping helpers ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +function mapToFormValues(data: unknown): PartFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + shop_type: null, + category: null, + unit_type: null, + department: null, + title: d.title ?? d.name ?? "", + sku: d.sku ?? "", + description: d.description ?? "", + selling_price: d.selling_price ?? undefined, + purchase_price: d.purchase_price ?? undefined, + } +} + +function mapCreatePayload(values: PartFormValues) { + return { + shop_type_id: toId(values.shop_type), + category_id: toId(values.category), + unit_type_id: toId(values.unit_type), + department_id: toId(values.department), + title: values.title, + sku: values.sku || undefined, + description: values.description || undefined, + selling_price: values.selling_price, + purchase_price: values.purchase_price, + } +} + +function mapUpdatePayload(values: PartFormValues) { + return { + title: values.title, + selling_price: values.selling_price, + } +} + +// ── Component ── + +export function PartForm({ resourceId, initialData, onSuccess }: PartFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: partFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: PartFormValues) => { + const promise = isEditing && resourceId + ? api.parts.update(resourceId, mapUpdatePayload(values)) + : api.parts.create(mapCreatePayload(values)) + toast.promise(promise, { + loading: isEditing ? "Updating part..." : "Creating part...", + success: isEditing ? "Part updated successfully" : "Part created successfully", + error: isEditing ? "Failed to update part" : "Failed to create part", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update part" : "Failed to create part"} + + {error.message} + + )} + + +
+ + +
+ + {!isEditing && ( + <> +
+ api.shopTypes.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Shop Type" + {...STORE_OBJECT} + /> + api.inventory.listCategories()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Category" + {...STORE_OBJECT} + /> +
+ +
+ api.inventory.listUnitTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Unit Type" + {...STORE_OBJECT} + /> + api.departments.list()} + mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> +
+ + )} + +
+ + {!isEditing && ( + + )} +
+ + +
+ +
+ +
+
+ ) +} diff --git a/apps/dashboard/modules/parts/part.schema.ts b/apps/dashboard/modules/parts/part.schema.ts new file mode 100644 index 0000000..c07cc37 --- /dev/null +++ b/apps/dashboard/modules/parts/part.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +export const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +export const partFormSchema = z.object({ + shop_type: relationFieldSchema, + category: relationFieldSchema, + unit_type: relationFieldSchema, + department: relationFieldSchema, + title: z.string().min(1, "Title is required"), + sku: z.string().optional(), + description: z.string().optional(), + selling_price: z.coerce.number().min(0).optional(), + purchase_price: z.coerce.number().min(0).optional(), +}) + +export type PartFormValues = z.infer diff --git a/apps/dashboard/modules/service-groups/service-group-form.tsx b/apps/dashboard/modules/service-groups/service-group-form.tsx new file mode 100644 index 0000000..f43fe0f --- /dev/null +++ b/apps/dashboard/modules/service-groups/service-group-form.tsx @@ -0,0 +1,274 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfAsyncSelectField, + RhfCheckboxField, +} from "@/shared/components/form" +import { ShopTypeInlineForm } from "@/modules/vehicles/inline-forms/shop-type-inline-form" +import { InventoryCategoryInlineForm } from "@/modules/services/inline-forms/inventory-category-inline-form" +import { UnitTypeInlineForm } from "@/modules/services/inline-forms/unit-type-inline-form" +import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toId } from "@/shared/lib/utils" + +import { serviceGroupFormSchema, type ServiceGroupFormValues } from "./service-group.schema" +import { SERVICE_GROUP_ROUTES } from "@garage/api" + +// ── Props ── + +export type ServiceGroupFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ServiceGroupFormValues = { + shop_type: null, + inventory_category: null, + unit_type: null, + department: null, + service_name: "", + code: "", + service_description: "", + selling_price: undefined, + selling_chart_of_account: "", + show_as_lump_sum: false, + mark_as_recommended: false, + set_packaged_pricing: false, + is_active: true, +} + +// ── Mapping helpers ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +function mapToFormValues(data: unknown): ServiceGroupFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + shop_type: null, + inventory_category: null, + unit_type: null, + department: null, + service_name: d.service_name ?? d.name ?? "", + code: d.code ?? "", + service_description: d.service_description ?? "", + selling_price: d.selling_price ?? undefined, + selling_chart_of_account: d.selling_chart_of_account ?? "", + show_as_lump_sum: d.show_as_lump_sum ?? false, + mark_as_recommended: d.mark_as_recommended ?? false, + set_packaged_pricing: d.set_packaged_pricing ?? false, + is_active: d.is_active ?? true, + } +} + +function mapCreatePayload(values: ServiceGroupFormValues) { + return { + service_name: values.service_name, + shop_type_id: toId(values.shop_type), + code: values.code || undefined, + inventory_category_id: toId(values.inventory_category), + unit_type_id: toId(values.unit_type), + department_id: toId(values.department), + service_description: values.service_description || undefined, + show_as_lump_sum: values.show_as_lump_sum, + mark_as_recommended: values.mark_as_recommended, + set_packaged_pricing: values.set_packaged_pricing, + selling_price: values.selling_price, + selling_chart_of_account: values.selling_chart_of_account || undefined, + is_active: values.is_active, + } +} + +function mapUpdatePayload(values: ServiceGroupFormValues) { + return { + service_name: values.service_name, + selling_price: values.selling_price, + is_active: values.is_active, + } +} + +// ── Component ── + +export function ServiceGroupForm({ resourceId, initialData, onSuccess }: ServiceGroupFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: serviceGroupFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ServiceGroupFormValues) => { + const promise = isEditing && resourceId + ? api.serviceGroups.update(resourceId, mapUpdatePayload(values)) + : api.serviceGroups.create(mapCreatePayload(values)) + toast.promise(promise, { + loading: isEditing ? "Updating service group..." : "Creating service group...", + success: isEditing ? "Service group updated" : "Service group created", + error: isEditing ? "Failed to update service group" : "Failed to create service group", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update service group" : "Failed to create service group"} + + {error.message} + + )} + + +
+ + +
+ + {!isEditing && ( + <> +
+ api.shopTypes.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Shop Type" + {...STORE_OBJECT} + /> + api.inventory.listCategories()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Category" + {...STORE_OBJECT} + /> +
+ +
+ api.inventory.listUnitTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Unit Type" + {...STORE_OBJECT} + /> + api.departments.list()} + mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> +
+ + )} + +
+ + +
+ + + +
+ + + + +
+ + +
+ +
+ +
+
+ ) +} diff --git a/apps/dashboard/modules/service-groups/service-group.schema.ts b/apps/dashboard/modules/service-groups/service-group.schema.ts new file mode 100644 index 0000000..01cc51c --- /dev/null +++ b/apps/dashboard/modules/service-groups/service-group.schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +export const serviceGroupFormSchema = z.object({ + shop_type: relationFieldSchema, + inventory_category: relationFieldSchema, + unit_type: relationFieldSchema, + department: relationFieldSchema, + service_name: z.string().min(1, "Service name is required"), + code: z.string().optional(), + service_description: z.string().optional(), + selling_price: z.coerce.number().min(0).optional(), + selling_chart_of_account: z.string().optional(), + show_as_lump_sum: z.boolean().optional(), + mark_as_recommended: z.boolean().optional(), + set_packaged_pricing: z.boolean().optional(), + is_active: z.boolean().optional(), +}) + +export type ServiceGroupFormValues = z.infer diff --git a/apps/dashboard/modules/services/department-assignment-types.ts b/apps/dashboard/modules/services/department-assignment-types.ts new file mode 100644 index 0000000..44a1af1 --- /dev/null +++ b/apps/dashboard/modules/services/department-assignment-types.ts @@ -0,0 +1,7 @@ +export const DEPARTMENT_ASSIGNMENT_TYPE_OPTIONS = [ + { value: "none", label: "None" }, + { value: "bays", label: "Bays" }, + { value: "outsourced", label: "Outsourced" }, +] as const + +export type DepartmentAssignmentType = typeof DEPARTMENT_ASSIGNMENT_TYPE_OPTIONS[number]["value"] diff --git a/apps/dashboard/modules/services/inline-forms/category-inline-form.tsx b/apps/dashboard/modules/services/inline-forms/category-inline-form.tsx new file mode 100644 index 0000000..82a67b6 --- /dev/null +++ b/apps/dashboard/modules/services/inline-forms/category-inline-form.tsx @@ -0,0 +1,2 @@ +// Renamed to inventory-category-inline-form.tsx +export { InventoryCategoryInlineForm as CategoryInlineForm } from "./inventory-category-inline-form" diff --git a/apps/dashboard/modules/services/inline-forms/department-inline-form.tsx b/apps/dashboard/modules/services/inline-forms/department-inline-form.tsx new file mode 100644 index 0000000..2bfff41 --- /dev/null +++ b/apps/dashboard/modules/services/inline-forms/department-inline-form.tsx @@ -0,0 +1,66 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, RhfSelectField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { DEPARTMENT_ASSIGNMENT_TYPE_OPTIONS } from "../department-assignment-types" + +const schema = z.object({ + name: z.string().min(1, "Name is required"), + assignment_type: z.string().optional(), +}) + +type FormValues = z.infer + +export function DepartmentInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: "", assignment_type: "none" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.departments.create({ + name: values.name, + assignment_type: values.assignment_type || undefined, + }) + toast.success("Department created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.name ?? String(item.id) }) + } catch { + toast.error("Failed to create department") + } + } + + return ( + + + + + + + + ) +} diff --git a/apps/dashboard/modules/services/inline-forms/inventory-category-inline-form.tsx b/apps/dashboard/modules/services/inline-forms/inventory-category-inline-form.tsx new file mode 100644 index 0000000..5bcf2c8 --- /dev/null +++ b/apps/dashboard/modules/services/inline-forms/inventory-category-inline-form.tsx @@ -0,0 +1,78 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, RhfAsyncSelectField, type InlineCreateFormProps } from "@/shared/components/form" +import { ShopTypeInlineForm } from "@/modules/vehicles/inline-forms/shop-type-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), + shop_type: z.object({ value: z.string(), label: z.string() }).nullable(), +}) + +type FormValues = z.infer + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +export function InventoryCategoryInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "", shop_type: null }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.inventory.createCategory({ + title: values.title, + shop_type_id: values.shop_type ? Number(values.shop_type.value) : undefined, + }) + toast.success("Category created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? item.name ?? String(item.id) }) + } catch { + toast.error("Failed to create category") + } + } + + return ( + + + + api.shopTypes.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Shop Type" + {...STORE_OBJECT} + /> + + + + ) +} diff --git a/apps/dashboard/modules/services/inline-forms/unit-type-inline-form.tsx b/apps/dashboard/modules/services/inline-forms/unit-type-inline-form.tsx new file mode 100644 index 0000000..70c7bf0 --- /dev/null +++ b/apps/dashboard/modules/services/inline-forms/unit-type-inline-form.tsx @@ -0,0 +1,55 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), +}) + +type FormValues = z.infer + +export function UnitTypeInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.inventory.createUnitType({ title: values.title }) + toast.success("Unit type created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? item.name ?? String(item.id) }) + } catch { + toast.error("Failed to create unit type") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/services/service-form.tsx b/apps/dashboard/modules/services/service-form.tsx new file mode 100644 index 0000000..044e761 --- /dev/null +++ b/apps/dashboard/modules/services/service-form.tsx @@ -0,0 +1,238 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { ShopTypeInlineForm } from "@/modules/vehicles/inline-forms/shop-type-inline-form" +import { InventoryCategoryInlineForm } from "./inline-forms/inventory-category-inline-form" +import { UnitTypeInlineForm } from "./inline-forms/unit-type-inline-form" +import { DepartmentInlineForm } from "./inline-forms/department-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toId } from "@/shared/lib/utils" + +import { serviceFormSchema, type ServiceFormValues } from "./service.schema" +import { SERVICE_ROUTES } from "@garage/api" + +// ── Props ── + +export type ServiceFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ServiceFormValues = { + shop_type: null, + category: null, + unit_type: null, + department: null, + labor_name: "", + service_code: "", + labor_matrix: "", + description: "", + selling_price: undefined, +} + +// ── Mapping helpers ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +function mapToFormValues(data: unknown): ServiceFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + shop_type: null, + category: null, + unit_type: null, + department: null, + labor_name: d.name || d.labor_name || "", + service_code: d.service_code || "", + labor_matrix: d.labor_matrix || "", + description: d.description || "", + selling_price: d.selling_price ?? undefined, + } +} + +function mapCreatePayload(values: ServiceFormValues) { + return { + shop_type_id: toId(values.shop_type), + category_id: toId(values.category), + unit_type_id: toId(values.unit_type), + department_id: toId(values.department), + labor_name: values.labor_name, + service_code: values.service_code || undefined, + labor_matrix: values.labor_matrix || undefined, + description: values.description || undefined, + selling_price: values.selling_price, + } +} + +function mapUpdatePayload(values: ServiceFormValues) { + return { + labor_name: values.labor_name, + selling_price: values.selling_price, + } +} + +// ── Component ── + +export function ServiceForm({ resourceId, initialData, onSuccess }: ServiceFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: serviceFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ServiceFormValues) => { + const promise = isEditing && resourceId + ? api.services.update(resourceId, mapUpdatePayload(values)) + : api.services.create(mapCreatePayload(values)) + toast.promise(promise, { + loading: isEditing ? "Updating service..." : "Creating service...", + success: isEditing ? "Service updated successfully" : "Service created successfully", + error: isEditing ? "Failed to update service" : "Failed to create service", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update service" : "Failed to create service"} + + {error.message} + + )} + + +
+ + +
+ + {!isEditing && ( + <> +
+ api.shopTypes.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Shop Type" + {...STORE_OBJECT} + /> + api.inventory.listCategories()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Category" + {...STORE_OBJECT} + /> +
+ +
+ api.inventory.listUnitTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Unit Type" + {...STORE_OBJECT} + /> + api.departments.list()} + mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} + createForm={(props) => } + createLabel="Department" + {...STORE_OBJECT} + /> +
+ + + + )} + +
+ +
+ + + + +
+
+ ) +} diff --git a/apps/dashboard/modules/services/service.schema.ts b/apps/dashboard/modules/services/service.schema.ts new file mode 100644 index 0000000..116b02f --- /dev/null +++ b/apps/dashboard/modules/services/service.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +export const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +export const serviceFormSchema = z.object({ + shop_type: relationFieldSchema, + category: relationFieldSchema, + unit_type: relationFieldSchema, + department: relationFieldSchema, + labor_name: z.string().min(1, "Labor name is required"), + service_code: z.string().optional(), + labor_matrix: z.string().optional(), + description: z.string().optional(), + selling_price: z.coerce.number().min(0).optional(), +}) + +export type ServiceFormValues = z.infer diff --git a/apps/dashboard/modules/settings/shop-type/shop-type-form.tsx b/apps/dashboard/modules/settings/shop-type/shop-type-form.tsx new file mode 100644 index 0000000..e107164 --- /dev/null +++ b/apps/dashboard/modules/settings/shop-type/shop-type-form.tsx @@ -0,0 +1,157 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfCheckboxField, + RhfFileField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" + +import { shopTypeFormSchema, type ShopTypeFormValues } from "./shop-type.schema" +import { SHOP_TYPE_ROUTES } from "@garage/api" + +// ── Props ── + +export type ShopTypeFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ShopTypeFormValues = { + title: "", + shop_type: "", + note: "", + is_default: false, + inspection: null, + image: null, +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): ShopTypeFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + title: d.title || "", + shop_type: d.shop_type || "", + note: d.note || "", + is_default: d.is_default ?? false, + // File fields cannot be pre-filled from URL strings + inspection: null, + image: null, + } +} + +function mapFormToPayload(values: ShopTypeFormValues) { + return { + title: values.title, + shop_type: values.shop_type || undefined, + note: values.note || undefined, + is_default: values.is_default, + inspection: values.inspection ?? undefined, + image: values.image ?? undefined, + } +} + +// ── Component ── + +export function ShopTypeForm({ resourceId, initialData, onSuccess }: ShopTypeFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: shopTypeFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ShopTypeFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.shopTypes.update(resourceId, payload) + : api.shopTypes.create({ ...payload, title: values.title }) + toast.promise(promise, { + loading: isEditing ? "Updating shop type..." : "Creating shop type...", + success: isEditing ? "Shop type updated successfully" : "Shop type created successfully", + error: isEditing ? "Failed to update shop type" : "Failed to create shop type", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update shop type" : "Failed to create shop type"} + + {error.message} + + )} + + + + + + +
+ + +
+ + +
+
+ ) +} diff --git a/apps/dashboard/modules/settings/shop-type/shop-type.schema.ts b/apps/dashboard/modules/settings/shop-type/shop-type.schema.ts new file mode 100644 index 0000000..19c5690 --- /dev/null +++ b/apps/dashboard/modules/settings/shop-type/shop-type.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +export const shopTypeFormSchema = z.object({ + title: z.string().min(1, "Title is required"), + shop_type: z.string().optional(), + note: z.string().optional(), + is_default: z.boolean().optional(), + inspection: z.any().optional(), + image: z.any().optional(), +}) + +export type ShopTypeFormValues = { + title: string + shop_type?: string + note?: string + is_default?: boolean + inspection?: File | null + image?: File | null +} diff --git a/apps/dashboard/modules/shop-calendars/shop-calendar-form.tsx b/apps/dashboard/modules/shop-calendars/shop-calendar-form.tsx new file mode 100644 index 0000000..7b3634a --- /dev/null +++ b/apps/dashboard/modules/shop-calendars/shop-calendar-form.tsx @@ -0,0 +1,99 @@ +"use client" + +import { AlertTriangle, Plus } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" + +import { + shopCalendarFormSchema, + type ShopCalendarFormValues, +} from "./shop-calendar.schema" +import { SHOP_CALENDAR_ROUTES } from "@garage/api" + +// ── Props ── + +export type ShopCalendarFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ShopCalendarFormValues = { + title: "", + is_default: false, +} + +// ── Component ── + +export function ShopCalendarForm({ resourceId, onSuccess }: ShopCalendarFormProps) { + const api = useAuthApi() + + const { form } = useResourceForm({ + schema: shopCalendarFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId: null, + queryKey: [SHOP_CALENDAR_ROUTES.INDEX], + mapToFormValues: (data: unknown) => { + const d = (data as any)?.data ?? data ?? {} + return { + title: d.title ?? "", + is_default: d.is_default ?? false, + } + }, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ShopCalendarFormValues) => { + const payload = { + title: values.title, + is_default: values.is_default, + } + const promise = api.shopCalendars.create(payload) + toast.promise(promise, { + loading: "Creating shop calendar...", + success: "Shop calendar created successfully", + error: "Failed to create shop calendar", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + Failed to create shop calendar + {error.message} + + )} + + + + + + + + + ) +} diff --git a/apps/dashboard/modules/shop-calendars/shop-calendar.schema.ts b/apps/dashboard/modules/shop-calendars/shop-calendar.schema.ts new file mode 100644 index 0000000..c9bbf14 --- /dev/null +++ b/apps/dashboard/modules/shop-calendars/shop-calendar.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +const shopCalendarFormSchema = z.object({ + title: z.string().min(1, "Title is required"), + is_default: z.boolean(), +}) + +type ShopCalendarFormValues = z.infer + +export { shopCalendarFormSchema } +export type { ShopCalendarFormValues } diff --git a/apps/dashboard/modules/shop-timings/shop-timing-form.tsx b/apps/dashboard/modules/shop-timings/shop-timing-form.tsx new file mode 100644 index 0000000..879b2e4 --- /dev/null +++ b/apps/dashboard/modules/shop-timings/shop-timing-form.tsx @@ -0,0 +1,161 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfCheckboxField, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" + +import { + shopTimingFormSchema, + type ShopTimingFormValues, +} from "./shop-timing.schema" +import { SHOP_TIMING_ROUTES } from "@garage/api" + +// ── Props ── + +export type ShopTimingFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: ShopTimingFormValues = { + title: "", + in_time: "", + out_time: "", + full_day_hours: "", + half_day_hours: "", + punch_in: "", + punch_out: "", + before_time: "", + after_time: "", + is_default: false, +} + +// ── Mapping helpers ── + +function mapToFormValues(data: unknown): ShopTimingFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + title: d.title ?? "", + in_time: d.in_time ?? "", + out_time: d.out_time ?? "", + full_day_hours: d.full_day_hours ?? "", + half_day_hours: d.half_day_hours ?? "", + punch_in: d.punch_in ?? "", + punch_out: d.punch_out ?? "", + before_time: d.before_time ?? "", + after_time: d.after_time ?? "", + is_default: d.is_default ?? false, + } +} + +function mapFormToPayload(values: ShopTimingFormValues) { + return { + title: values.title, + in_time: values.in_time, + out_time: values.out_time, + full_day_hours: values.full_day_hours || undefined, + half_day_hours: values.half_day_hours || undefined, + punch_in: values.punch_in || undefined, + punch_out: values.punch_out || undefined, + before_time: values.before_time || undefined, + after_time: values.after_time || undefined, + is_default: values.is_default, + } +} + +// ── Component ── + +export function ShopTimingForm({ resourceId, initialData, onSuccess }: ShopTimingFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: shopTimingFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + initialize: (id) => api.shopTimings.show(id), + queryKey: [SHOP_TIMING_ROUTES.BY_ID, resourceId], + mapToFormValues: mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: ShopTimingFormValues) => { + const payload = mapFormToPayload(values) + const promise = isEditing && resourceId + ? api.shopTimings.update(resourceId, payload) + : api.shopTimings.create(payload) + toast.promise(promise, { + loading: isEditing ? "Updating shop timing..." : "Creating shop timing...", + success: isEditing ? "Shop timing updated successfully" : "Shop timing created successfully", + error: isEditing ? "Failed to update shop timing" : "Failed to create shop timing", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update shop timing" : "Failed to create shop timing"} + + {error.message} + + )} + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ ) +} diff --git a/apps/dashboard/modules/shop-timings/shop-timing.schema.ts b/apps/dashboard/modules/shop-timings/shop-timing.schema.ts new file mode 100644 index 0000000..7d66abe --- /dev/null +++ b/apps/dashboard/modules/shop-timings/shop-timing.schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +const shopTimingFormSchema = z.object({ + title: z.string().min(1, "Title is required"), + in_time: z.string().min(1, "In time is required"), + out_time: z.string().min(1, "Out time is required"), + full_day_hours: z.string().optional(), + half_day_hours: z.string().optional(), + punch_in: z.string().optional(), + punch_out: z.string().optional(), + before_time: z.string().optional(), + after_time: z.string().optional(), + is_default: z.boolean().default(false), +}) + +type ShopTimingFormValues = z.infer + +export { shopTimingFormSchema } +export type { ShopTimingFormValues } diff --git a/apps/dashboard/modules/vehicles/inline-forms/body-type-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/body-type-inline-form.tsx new file mode 100644 index 0000000..4d0507c --- /dev/null +++ b/apps/dashboard/modules/vehicles/inline-forms/body-type-inline-form.tsx @@ -0,0 +1,55 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), +}) + +type FormValues = z.infer + +export function BodyTypeInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.vehicleAttributes.createBodyType({ title: values.title }) + toast.success("Body type created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? String(item.id) }) + } catch { + toast.error("Failed to create body type") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/vehicles/inline-forms/color-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/color-inline-form.tsx new file mode 100644 index 0000000..1e97e71 --- /dev/null +++ b/apps/dashboard/modules/vehicles/inline-forms/color-inline-form.tsx @@ -0,0 +1,55 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), +}) + +type FormValues = z.infer + +export function ColorInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.vehicleAttributes.createColor({ title: values.title }) + toast.success("Color created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? String(item.id) }) + } catch { + toast.error("Failed to create color") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/vehicles/inline-forms/fuel-type-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/fuel-type-inline-form.tsx new file mode 100644 index 0000000..0f3cc0c --- /dev/null +++ b/apps/dashboard/modules/vehicles/inline-forms/fuel-type-inline-form.tsx @@ -0,0 +1,55 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), +}) + +type FormValues = z.infer + +export function FuelTypeInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.vehicleAttributes.createFuelType({ title: values.title }) + toast.success("Fuel type created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? String(item.id) }) + } catch { + toast.error("Failed to create fuel type") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/vehicles/inline-forms/shop-type-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/shop-type-inline-form.tsx new file mode 100644 index 0000000..6cdbac4 --- /dev/null +++ b/apps/dashboard/modules/vehicles/inline-forms/shop-type-inline-form.tsx @@ -0,0 +1,114 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfCheckboxField, + RhfFileField, + type InlineCreateFormProps, +} from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), + shop_type: z.string().optional(), + note: z.string().optional(), + is_default: z.boolean().optional(), + inspection: z.any().optional(), + image: z.any().optional(), +}) + +type FormValues = { + title: string + shop_type?: string + note?: string + is_default?: boolean + inspection?: File | null + image?: File | null +} + +export function ShopTypeInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + title: "", + shop_type: "", + note: "", + is_default: false, + inspection: null, + image: null, + }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.shopTypes.create({ + title: values.title, + shop_type: values.shop_type || undefined, + note: values.note || undefined, + is_default: values.is_default, + inspection: values.inspection ?? undefined, + image: values.image ?? undefined, + }) + toast.success("Shop type created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? String(item.id) }) + } catch { + toast.error("Failed to create shop type") + } + } + + return ( + + + + + + + + + + + + ) +} + diff --git a/apps/dashboard/modules/vehicles/inline-forms/transmission-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/transmission-inline-form.tsx new file mode 100644 index 0000000..f21c98b --- /dev/null +++ b/apps/dashboard/modules/vehicles/inline-forms/transmission-inline-form.tsx @@ -0,0 +1,55 @@ +"use client" + +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { FieldGroup } from "@/shared/components/ui/field" +import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" + +const schema = z.object({ + title: z.string().min(1, "Title is required"), +}) + +type FormValues = z.infer + +export function TransmissionInlineForm({ onSuccess }: InlineCreateFormProps) { + const api = useAuthApi() + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { title: "" }, + }) + + const handleSubmit = async (values: FormValues) => { + try { + const result = await api.vehicleAttributes.createTransmission({ title: values.title }) + toast.success("Transmission created") + form.reset() + const item = (result as any)?.data ?? result + onSuccess({ value: String(item.id), label: item.title ?? String(item.id) }) + } catch { + toast.error("Failed to create transmission") + } + } + + return ( + + + + + + + ) +} diff --git a/apps/dashboard/modules/vehicles/vehicle-form.tsx b/apps/dashboard/modules/vehicles/vehicle-form.tsx new file mode 100644 index 0000000..29c9a71 --- /dev/null +++ b/apps/dashboard/modules/vehicles/vehicle-form.tsx @@ -0,0 +1,267 @@ +"use client" + +import { AlertTriangle, Plus, Save } from "lucide-react" + +import { Button } from "@/shared/components/ui/button" +import { Alert, AlertTitle } from "@/shared/components/ui/alert" +import { FieldGroup } from "@/shared/components/ui/field" +import { + Rhform, + RhfTextField, + RhfTextareaField, + RhfAsyncSelectField, +} from "@/shared/components/form" +import { ShopTypeInlineForm } from "./inline-forms/shop-type-inline-form" +import { BodyTypeInlineForm } from "./inline-forms/body-type-inline-form" +import { FuelTypeInlineForm } from "./inline-forms/fuel-type-inline-form" +import { TransmissionInlineForm } from "./inline-forms/transmission-inline-form" +import { ColorInlineForm } from "./inline-forms/color-inline-form" +import { toast } from "sonner" +import { useAuthApi } from "@/shared/useApi" +import { useResourceForm } from "@/shared/hooks/use-resource-form" +import { useFormMutation } from "@/shared/hooks/use-form-mutation" +import { toRelation, toId } from "@/shared/lib/utils" + +import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema" +import { VEHICLE_ROUTES } from "@garage/api" + +// ── Props ── + +export type VehicleFormProps = { + resourceId?: string | null + initialData?: unknown + onSuccess?: () => void +} + +// ── Default values ── + +const DEFAULT_VALUES: VehicleFormValues = { + shop_type: null, + vehicle_body_type: null, + vehicle_fuel_type: null, + vehicle_transmission: null, + vehicle_color: null, + make: "", + model: "", + year: "", + sub_model: "", + license_plate: "", + vin_number: "", + engine_size: "", + drivetrain: "", + mileage: "", + owners_number: "", + note: "", +} + +// ── Mapping helpers ── + +const mapLookupOption = (item: any) => ({ + value: String(item.id), + label: item.name ?? item.title ?? String(item.id), +}) + +const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } + +function mapToFormValues(data: unknown): VehicleFormValues { + const d = (data as any)?.data ?? data ?? {} + + return { + shop_type: toRelation(d.shop_type_id, d.shop_type?.title), + vehicle_body_type: toRelation(d.vehicle_body_type_id, d.vehicle_body_type?.title), + vehicle_fuel_type: toRelation(d.vehicle_fuel_type_id, d.vehicle_fuel_type?.title), + vehicle_transmission: toRelation(d.vehicle_transmission_id, d.vehicle_transmission?.title), + vehicle_color: toRelation(d.vehicle_color_id, d.vehicle_color?.title), + make: d.make || "", + model: d.model || "", + year: d.year || "", + sub_model: d.sub_model || "", + license_plate: d.license_plate || "", + vin_number: d.vin_number || "", + engine_size: d.engine_size || "", + drivetrain: d.drivetrain || "", + mileage: d.mileage || "", + owners_number: d.owners_number || "", + note: d.note || "", + } +} + +function mapCreatePayload(values: VehicleFormValues) { + return { + shop_type_id: toId(values.shop_type), + vehicle_body_type_id: toId(values.vehicle_body_type), + vehicle_fuel_type_id: toId(values.vehicle_fuel_type), + vehicle_transmission_id: toId(values.vehicle_transmission), + vehicle_color_id: toId(values.vehicle_color), + make: values.make, + model: values.model, + year: values.year, + sub_model: values.sub_model || undefined, + license_plate: values.license_plate || undefined, + vin_number: values.vin_number || undefined, + engine_size: values.engine_size || undefined, + drivetrain: values.drivetrain || undefined, + mileage: values.mileage || undefined, + owners_number: values.owners_number || undefined, + note: values.note || undefined, + } +} + +function mapUpdatePayload(values: VehicleFormValues) { + return { + mileage: values.mileage || undefined, + license_plate: values.license_plate || undefined, + } +} + +// ── Component ── + +export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormProps) { + const api = useAuthApi() + + const { form, isEditing } = useResourceForm({ + schema: vehicleFormSchema, + defaultValues: DEFAULT_VALUES, + resourceId, + initialData, + mapToFormValues, + }) + + const { mutate, error, isPending } = useFormMutation(form, { + mutationFn: (values: VehicleFormValues) => { + const promise = isEditing && resourceId + ? api.vehicles.update(resourceId, mapUpdatePayload(values)) + : api.vehicles.create(mapCreatePayload(values)) + toast.promise(promise, { + loading: isEditing ? "Updating vehicle..." : "Creating vehicle...", + success: isEditing ? "Vehicle updated successfully" : "Vehicle created successfully", + error: isEditing ? "Failed to update vehicle" : "Failed to create vehicle", + }) + return promise + }, + onSuccess: () => { + form.reset() + onSuccess?.() + }, + }) + + return ( + mutate(values)}> + {error && ( + + + + {isEditing ? "Failed to update vehicle" : "Failed to create vehicle"} + + {error.message} + + )} + + + {!isEditing && ( + <> + {/* Vehicle identity */} +
+ + + +
+ + + + {/* Associations */} +
+ api.shopTypes.list()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Shop Type" + {...STORE_OBJECT} + /> + api.vehicleAttributes.listBodyTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Body Type" + {...STORE_OBJECT} + /> +
+ +
+ api.vehicleAttributes.listFuelTypes()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Fuel Type" + {...STORE_OBJECT} + /> + api.vehicleAttributes.listTransmissions()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Transmission" + {...STORE_OBJECT} + /> +
+ +
+ api.vehicleAttributes.listColors()} + mapOption={mapLookupOption} + createForm={(props) => } + createLabel="Color" + {...STORE_OBJECT} + /> + +
+ + {/* Technical specs */} +
+ + +
+ + + + )} + + {/* Editable in both create and update */} +
+ + +
+ + {!isEditing && ( + + )} + + +
+
+ ) +} diff --git a/apps/dashboard/modules/vehicles/vehicle.schema.ts b/apps/dashboard/modules/vehicles/vehicle.schema.ts new file mode 100644 index 0000000..343acda --- /dev/null +++ b/apps/dashboard/modules/vehicles/vehicle.schema.ts @@ -0,0 +1,35 @@ +import { z } from "zod" + +export const relationFieldSchema = z + .object({ value: z.string(), label: z.string() }) + .nullable() + +export const vehicleFormSchema = z.object({ + // ── Relations ── + shop_type: relationFieldSchema, + vehicle_body_type: relationFieldSchema, + vehicle_fuel_type: relationFieldSchema, + vehicle_transmission: relationFieldSchema, + vehicle_color: relationFieldSchema, + + // ── Vehicle identity ── + make: z.string().optional(), + model: z.string().optional(), + year: z.string().optional(), + sub_model: z.string().optional(), + + // ── License & identifiers ── + license_plate: z.string().optional(), + vin_number: z.string().optional(), + + // ── Technical specs ── + engine_size: z.string().optional(), + drivetrain: z.string().optional(), + mileage: z.string().optional(), + owners_number: z.string().optional(), + + // ── Notes ── + note: z.string().optional(), +}) + +export type VehicleFormValues = z.infer diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs new file mode 100644 index 0000000..1d61478 --- /dev/null +++ b/apps/dashboard/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +export default nextConfig diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json new file mode 100644 index 0000000..23276c1 --- /dev/null +++ b/apps/dashboard/package.json @@ -0,0 +1,65 @@ +{ + "name": "@garage/dashboard", + "version": "0.0.1", + "type": "module", + "private": true, + "scripts": { + "predev": "pnpm --filter @garage/api run generate", + "dev": "next dev --turbopack", + "prebuild": "pnpm --filter @garage/api run generate", + "build": "next build", + "start": "next start", + "lint": "eslint", + "format": "prettier --write \"**/*.{ts,tsx}\"", + "typecheck": "tsc --noEmit", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "test:e2e": "cypress run" + }, + "dependencies": { + "@base-ui/react": "^1.3.0", + "@hookform/resolvers": "^5.2.2", + "@garage/api": "workspace:*", + "@tanstack/react-query": "^5.95.2", + "@tanstack/react-table": "^8.21.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.577.0", + "next": "16.1.7", + "next-themes": "^0.4.6", + "nuqs": "^2.8.9", + "radix-ui": "^1.4.3", + "react": "^19.2.4", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.4", + "react-hook-form": "^7.72.0", + "react-resizable-panels": "^4.7.5", + "recharts": "3.8.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4.2.1", + "@types/node": "^25.5.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "cypress": "^15.13.0", + "eslint": "^9.39.4", + "eslint-config-next": "16.1.7", + "postcss": "^8", + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "shadcn": "^4.1.0", + "tailwindcss": "^4.2.1", + "typescript": "^5.9.3" + } +} diff --git a/apps/dashboard/postcss.config.mjs b/apps/dashboard/postcss.config.mjs new file mode 100644 index 0000000..f6c75ff --- /dev/null +++ b/apps/dashboard/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/apps/dashboard/public/.gitkeep b/apps/dashboard/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/dashboard/public/assets/logo.png b/apps/dashboard/public/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c20846b4b92cb4f331682f45b61e89ec14a2db0b GIT binary patch literal 59735 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 { + const { token } = await getAuthCookies(); + console.log(`Auth Token: ${token}`); + const api = createApi({ headers: token ? { Authorization: `Bearer ${token}` } : undefined }); + return api; +} + + diff --git a/apps/dashboard/shared/components/.gitkeep b/apps/dashboard/shared/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/dashboard/shared/components/confirm-dialog.tsx b/apps/dashboard/shared/components/confirm-dialog.tsx new file mode 100644 index 0000000..87b04ea --- /dev/null +++ b/apps/dashboard/shared/components/confirm-dialog.tsx @@ -0,0 +1,120 @@ +"use client" + +import { create } from "zustand" +import { Trash2 } from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogTitle, +} from "@/shared/components/ui/alert-dialog" + +// ── Types ── + +export type ConfirmOptions = { + title?: string + description?: string + confirmLabel?: string + cancelLabel?: string + variant?: "destructive" | "default" +} + +type ConfirmStore = { + open: boolean + options: ConfirmOptions + resolve: ((value: boolean) => void) | null + _show: (options: ConfirmOptions) => Promise + _close: (confirmed: boolean) => void +} + +// ── Store ── + +const useConfirmStore = create((set, get) => ({ + open: false, + options: {}, + resolve: null, + _show: (options) => + new Promise((resolve) => { + set({ open: true, options, resolve }) + }), + _close: (confirmed) => { + const { resolve } = get() + resolve?.(confirmed) + set({ open: false, resolve: null }) + }, +})) + +// ── Imperative API (usage: `await confirm({ ... })`) ── + +export function confirm(options: ConfirmOptions = {}) { + if (process.env.NODE_ENV === "development") { + const state = useConfirmStore.getState() + if (state.open) { + console.warn("[ConfirmDialog] A confirm dialog is already open. Nested confirms are not supported.") + } + // Detect missing mount: if `resolve` is never set after a tick, the dialog component is not mounted. + const result = state._show(options) + setTimeout(() => { + const current = useConfirmStore.getState() + if (current.open && current.resolve === null) { + console.warn( + "[ConfirmDialog] confirm() was called but does not appear to be mounted. " + + "Make sure is rendered in your root layout.", + ) + } + }, 100) + return result + } + return useConfirmStore.getState()._show(options) +} + +// ── Dialog component (mount once in the root layout) ── + +export function ConfirmDialog() { + const { open, options, _close } = useConfirmStore() + + const isDestructive = options.variant === "destructive" + + return ( + { + if (!v) _close(false) + }} + > + + + {isDestructive && ( + + + + )} + + {options.title ?? "Are you sure?"} + + {options.description && ( + + {options.description} + + )} + + + _close(false)}> + {options.cancelLabel ?? "Cancel"} + + _close(true)} + > + {options.confirmLabel ?? "Confirm"} + + + + + ) +} diff --git a/apps/dashboard/shared/components/form-dialog.tsx b/apps/dashboard/shared/components/form-dialog.tsx new file mode 100644 index 0000000..f62feb5 --- /dev/null +++ b/apps/dashboard/shared/components/form-dialog.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { useQueryStates, parseAsBoolean, parseAsString } from 'nuqs' +import { Button } from '@/shared/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/components/ui/dialog' +import { ScrollArea } from '@/shared/components/ui/scroll-area' +import { Plus } from 'lucide-react' + +export const formDialogParams = { + dialog: parseAsBoolean.withDefault(false), + resourceId: parseAsString, +} + +export function useFormDialog(paramKey?: string) { + // Default (no paramKey) uses the standard `dialog` and `resourceId` params + const defaultState = useQueryStates(formDialogParams) + + // When a paramKey is provided, use prefixed params to avoid URL collisions + const prefixedState = useQueryStates({ + [`${paramKey ?? "_"}_dialog`]: parseAsBoolean.withDefault(false), + [`${paramKey ?? "_"}_resourceId`]: parseAsString, + }) + + if (paramKey) { + const [params, setParams] = prefixedState + const dialogKey = `${paramKey}_dialog` + const resourceIdKey = `${paramKey}_resourceId` + + const open = (resourceId?: string) => { + setParams({ [dialogKey]: true, [resourceIdKey]: resourceId ?? null }) + } + const close = () => { + setParams({ [dialogKey]: false, [resourceIdKey]: null }) + } + + return { + isOpen: (params as Record)[dialogKey] as boolean, + resourceId: (params as Record)[resourceIdKey] as string | null, + open, + close, + } + } + + const [params, setParams] = defaultState + + const open = (resourceId?: string) => { + setParams({ dialog: true, resourceId: resourceId ?? null }) + } + const close = () => { + setParams({ dialog: false, resourceId: null }) + } + + return { + isOpen: params.dialog, + resourceId: params.resourceId, + open, + close, + } +} + +export default function FormDialog(props: { + children: (resourceId: string | null) => React.ReactNode + title: string + paramKey?: string +}) { + const { isOpen, resourceId, open, close } = useFormDialog(props.paramKey) + + return ( + <> + +

{ if (!v) close() }}> + + + + {props.title} + + + + {props.children(resourceId)} + + + + + ) +} diff --git a/apps/dashboard/shared/components/form/controls/async-select-field.tsx b/apps/dashboard/shared/components/form/controls/async-select-field.tsx new file mode 100644 index 0000000..f2bf0ae --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/async-select-field.tsx @@ -0,0 +1,160 @@ +"use client" + +import { useRef } from "react" +import type { AsyncOption, BaseFieldControlProps } from "../types" +import { Loader2 } from "lucide-react" +import { + Combobox, + ComboboxInput, + ComboboxContent, + ComboboxList, + ComboboxItem, + ComboboxEmpty, +} from "@/shared/components/ui/combobox" + +const defaultGetOptionValue = (opt: any) => opt.value +const defaultGetOptionLabel = (opt: any) => opt.label +function defaultGetOptionKey(opt: any): string { + const v = defaultGetOptionValue(opt) + if (typeof v === "string" || typeof v === "number") return String(v) + return String(opt.id ?? JSON.stringify(v)) +} + +// ── Single-select ── + +export type AsyncSelectFieldProps = BaseFieldControlProps & { + options: TOption[] + loading?: boolean + onInputValueChange?: (value: string) => void + placeholder?: string + getOptionValue?: (option: TOption) => any + getOptionLabel?: (option: TOption) => string + getOptionKey?: (option: TOption) => string +} + +export function AsyncSelectField({ + value, + onChange, + onBlur, + disabled, + invalid, + options, + loading, + onInputValueChange, + placeholder = "Search...", + getOptionValue = defaultGetOptionValue, + getOptionLabel = defaultGetOptionLabel, + getOptionKey = defaultGetOptionKey, +}: AsyncSelectFieldProps) { + const anchorRef = useRef(null) + + return ( +
+ onChange(val)} + disabled={disabled} + onInputValueChange={(val, { reason }) => { + if (reason === "input-change") { + onInputValueChange?.(val) + } + }} + > + + + + {loading && ( +
+ +
+ )} + {!loading && + options.map((opt) => ( + + {getOptionLabel(opt)} + + ))} + {!loading && options.length === 0 && ( + No results found + )} +
+
+
+
+ ) +} + +// ── Multi-select ── + +export type AsyncMultiSelectFieldProps = BaseFieldControlProps & { + options: TOption[] + loading?: boolean + onInputValueChange?: (value: string) => void + placeholder?: string + getOptionValue?: (option: TOption) => any + getOptionLabel?: (option: TOption) => string + getOptionKey?: (option: TOption) => string +} + +export function AsyncMultiSelectField({ + value, + onChange, + onBlur, + disabled, + invalid, + options, + loading, + onInputValueChange, + placeholder = "Search...", + getOptionValue = defaultGetOptionValue, + getOptionLabel = defaultGetOptionLabel, + getOptionKey = defaultGetOptionKey, +}: AsyncMultiSelectFieldProps) { + const anchorRef = useRef(null) + + return ( +
+ onChange(val as any[])} + disabled={disabled} + onInputValueChange={(val, { reason }) => { + if (reason === "input-change") { + onInputValueChange?.(val) + } + }} + > + 0} + onBlur={onBlur} + aria-invalid={invalid || undefined} + /> + + + {loading && ( +
+ +
+ )} + {!loading && + options.map((opt) => ( + + {getOptionLabel(opt)} + + ))} + {!loading && options.length === 0 && ( + No results found + )} +
+
+
+
+ ) +} diff --git a/apps/dashboard/shared/components/form/controls/checkbox-field.tsx b/apps/dashboard/shared/components/form/controls/checkbox-field.tsx new file mode 100644 index 0000000..4756e3d --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/checkbox-field.tsx @@ -0,0 +1,28 @@ +"use client" + +import type { BaseFieldControlProps } from "../types" +import { Switch } from "@/shared/components/ui/switch" + +export type CheckboxFieldProps = BaseFieldControlProps & { + label?: string +} + +export function CheckboxField({ + value, + onChange, + onBlur, + name, + disabled, + invalid, +}: CheckboxFieldProps) { + return ( + onChange(checked === true)} + onBlur={onBlur} + name={name} + disabled={disabled} + aria-invalid={invalid || undefined} + /> + ) +} diff --git a/apps/dashboard/shared/components/form/controls/file-input-field.tsx b/apps/dashboard/shared/components/form/controls/file-input-field.tsx new file mode 100644 index 0000000..e6f83db --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/file-input-field.tsx @@ -0,0 +1,28 @@ +import type { BaseFieldControlProps } from "../types" +import { Input } from "@/shared/components/ui/input" + +export type FileInputFieldProps = BaseFieldControlProps & { + accept?: string +} + +export function FileInputField({ + // value intentionally unused — file inputs cannot be controlled + onBlur, + name, + disabled, + invalid, + accept, + onChange, +}: FileInputFieldProps) { + return ( + onChange(e.target.files?.[0] ?? null)} + /> + ) +} diff --git a/apps/dashboard/shared/components/form/controls/select-field.tsx b/apps/dashboard/shared/components/form/controls/select-field.tsx new file mode 100644 index 0000000..b8999ee --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/select-field.tsx @@ -0,0 +1,45 @@ +"use client" + +import type { BaseFieldControlProps, SelectOption } from "../types" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" + +export type SelectFieldProps = BaseFieldControlProps & { + placeholder?: string + options: SelectOption[] +} + +export function SelectField({ + value, + onChange, + onBlur, + name, + disabled, + invalid, + placeholder, + options, +}: SelectFieldProps) { + return ( + + ) +} diff --git a/apps/dashboard/shared/components/form/controls/text-input-field.tsx b/apps/dashboard/shared/components/form/controls/text-input-field.tsx new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/text-input-field.tsx @@ -0,0 +1,31 @@ +import type { BaseFieldControlProps } from "../types" +import { Input } from "@/shared/components/ui/input" + +export type TextInputFieldProps = BaseFieldControlProps & { + placeholder?: string + type?: React.HTMLInputTypeAttribute +} + +export function TextInputField({ + value, + onChange, + onBlur, + name, + disabled, + invalid, + placeholder, + type = "text", +}: TextInputFieldProps) { + return ( + onChange(e.target.value)} + onBlur={onBlur} + name={name} + disabled={disabled} + aria-invalid={invalid || undefined} + placeholder={placeholder} + type={type} + /> + ) +} diff --git a/apps/dashboard/shared/components/form/controls/textarea-field.tsx b/apps/dashboard/shared/components/form/controls/textarea-field.tsx new file mode 100644 index 0000000..41a08b2 --- /dev/null +++ b/apps/dashboard/shared/components/form/controls/textarea-field.tsx @@ -0,0 +1,31 @@ +import type { BaseFieldControlProps } from "../types" +import { Textarea } from "@/shared/components/ui/textarea" + +export type TextareaFieldProps = BaseFieldControlProps & { + placeholder?: string + rows?: number +} + +export function TextareaField({ + value, + onChange, + onBlur, + name, + disabled, + invalid, + placeholder, + rows, +}: TextareaFieldProps) { + return ( +