diff --git a/components.json b/components.json new file mode 100644 index 0000000..0f21f26 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 51185dd..e6c2123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,47 @@ "name": "cosmos-prototype-application", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lowdb": "^7.0.1", + "lucide-react": "^0.525.0", "next": "15.3.5", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-day-picker": "^9.8.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.60.0", + "sonner": "^2.0.6", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.74" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/lowdb": "^1.0.15", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.5", "tailwindcss": "^4", + "tw-animate-css": "^1.3.5", "typescript": "^5" } }, @@ -51,6 +79,12 @@ "node": ">=6.0.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -238,6 +272,56 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", + "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -957,6 +1041,900 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", + "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -971,6 +1949,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1262,6 +2246,39 @@ "tailwindcss": "4.1.11" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -1294,6 +2311,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lowdb": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.15.tgz", + "integrity": "sha512-xaMNIveDCryK4UvnUJOc2BCOH0lPivdvWHrutsLryo9r9Id3RqZq2RDmT4eddiEPYzu7nJMw6nFIcVifcqjWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "20.19.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz", @@ -1308,7 +2342,7 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1318,7 +2352,7 @@ "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -1943,6 +2977,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2130,6 +3176,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2156,6 +3208,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2231,7 +3294,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2315,12 +3377,33 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2366,6 +3449,18 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2392,7 +3487,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2456,6 +3551,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2517,6 +3628,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2527,6 +3647,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2544,7 +3670,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2649,7 +3774,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2659,7 +3783,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2697,7 +3820,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2710,7 +3832,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3317,6 +4438,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3333,11 +4474,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3378,7 +4534,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3399,11 +4554,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3491,7 +4654,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3570,7 +4732,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3583,7 +4744,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3599,7 +4759,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4499,6 +5658,30 @@ "loose-envify": "cli.js" } }, + "node_modules/lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "license": "MIT", + "dependencies": { + "steno": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/lucide-react": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -4513,7 +5696,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4543,6 +5725,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4707,6 +5910,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5046,6 +6259,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5086,6 +6305,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.0.tgz", + "integrity": "sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -5098,6 +6338,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.60.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5105,6 +6361,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5499,6 +6824,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonner": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz", + "integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5515,6 +6850,18 @@ "dev": true, "license": "MIT" }, + "node_modules/steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5722,6 +7069,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -5847,6 +7204,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", + "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6023,6 +7390,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6160,6 +7570,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.74", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.74.tgz", + "integrity": "sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 8939d6b..4078fe0 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,47 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lowdb": "^7.0.1", + "lucide-react": "^0.525.0", + "next": "15.3.5", + "next-themes": "^0.4.6", "react": "^19.0.0", + "react-day-picker": "^9.8.0", "react-dom": "^19.0.0", - "next": "15.3.5" + "react-hook-form": "^7.60.0", + "sonner": "^2.0.6", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.74" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/lowdb": "^1.0.15", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.5", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "tw-animate-css": "^1.3.5", + "typescript": "^5" } } diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts new file mode 100644 index 0000000..c142c4e --- /dev/null +++ b/src/app/api/auth/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db, initDB, resetDB } from '@/database/database'; + +export async function POST(req: NextRequest) { + await initDB(); + const { username, password } = await req.json(); + const user = db.data?.users.find( + (u) => u.username === username && u.password === password && !u.isDeleted + ); + if (user) { + // In a real app, return a JWT or session token + return NextResponse.json({ success: true, user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName } }); + } else { + return NextResponse.json({ success: false, message: 'Invalid credentials' }, { status: 401 }); + } +} diff --git a/src/app/api/customer-dependant/[id]/route.ts b/src/app/api/customer-dependant/[id]/route.ts new file mode 100644 index 0000000..ae76621 --- /dev/null +++ b/src/app/api/customer-dependant/[id]/route.ts @@ -0,0 +1,210 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/database/database"; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Initialize database + await db.read(); + + // Parse dependant ID from params + const dependantId = parseInt(params.id); + + if (isNaN(dependantId)) { + return NextResponse.json( + { + success: false, + message: "Invalid dependant ID provided", + }, + { status: 400 } + ); + } + + // Get customer dependants + const customerDependants = db.data!.customerDependants; + + // Find the specific dependant + const dependant = customerDependants.find((d) => d.id === dependantId); + + if (!dependant) { + return NextResponse.json( + { + success: false, + message: "Dependant not found", + }, + { status: 404 } + ); + } + + return NextResponse.json( + { + success: true, + data: dependant, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error fetching dependant:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to fetch dependant", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Initialize database + await db.read(); + + // Parse dependant ID from params + const dependantId = parseInt(params.id); + + if (isNaN(dependantId)) { + return NextResponse.json( + { + success: false, + message: "Invalid dependant ID provided", + }, + { status: 400 } + ); + } + + // Parse and validate request body + const body = await request.json(); + console.log("Received dependant update data:", body); + + // Find the dependant to update + const dependantIndex = db.data!.customerDependants.findIndex( + (d) => d.id === dependantId + ); + + if (dependantIndex === -1) { + return NextResponse.json( + { + success: false, + message: "Dependant not found", + }, + { status: 404 } + ); + } + + // Update dependant data + const updatedDependant = { + ...db.data!.customerDependants[dependantIndex], + firstNameEn: body.dependantInfo.firstNameEn, + lastNameEn: body.dependantInfo.lastNameEn, + originAdd1: body.dependantInfo.originAdd1, + localAdd1: body.dependantInfo.localAdd1, + email: body.dependantContact.email, + mobile: body.dependantContact.mobile, + }; + + db.data!.customerDependants[dependantIndex] = updatedDependant; + + // Save to database + await db.write(); + console.log("Dependant updated successfully"); + + // Return success response + return NextResponse.json( + { + success: true, + message: "Dependant updated successfully", + data: updatedDependant, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error updating dependant:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to update dependant", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Initialize database + await db.read(); + + // Parse dependant ID from params + const dependantId = parseInt(params.id); + + if (isNaN(dependantId)) { + return NextResponse.json( + { + success: false, + message: "Invalid dependant ID provided", + }, + { status: 400 } + ); + } + + // Find the dependant to delete + const dependantIndex = db.data!.customerDependants.findIndex( + (d) => d.id === dependantId + ); + + if (dependantIndex === -1) { + return NextResponse.json( + { + success: false, + message: "Dependant not found", + }, + { status: 404 } + ); + } + + // Get dependant info before deletion + const dependantToDelete = db.data!.customerDependants[dependantIndex]; + + // Delete the dependant + db.data!.customerDependants.splice(dependantIndex, 1); + + // Save to database + await db.write(); + + console.log(`Dependant ${dependantId} deleted successfully`); + + // Return success response + return NextResponse.json( + { + success: true, + message: "Dependant deleted successfully", + data: dependantToDelete, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error deleting dependant:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to delete dependant", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/customer/[id]/route.ts b/src/app/api/customer/[id]/route.ts new file mode 100644 index 0000000..6516e7d --- /dev/null +++ b/src/app/api/customer/[id]/route.ts @@ -0,0 +1,262 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db, getNextId } from "@/database/database"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + // Initialize database + await db.read(); + + // Parse customer ID from params + const { id } = await params; + const customerId = parseInt(id); + + if (isNaN(customerId)) { + return NextResponse.json( + { + success: false, + message: "Invalid customer ID provided", + }, + { status: 400 } + ); + } + + // Get all customers and customer dependants + const customers = db.data!.customers; + const customerDependants = db.data!.customerDependants; + + // Find the specific customer + const customer = customers.find((c) => c.id === customerId); + + if (!customer) { + return NextResponse.json( + { + success: false, + message: "Customer not found", + }, + { status: 404 } + ); + } + + // Get customer dependants for this customer + const dependants = customerDependants.filter( + (dependant) => dependant.custId === customerId + ); + + // Return customer with dependants + const customerWithDependants = { + ...customer, + dependants: dependants, + }; + + return NextResponse.json( + { + success: true, + data: customerWithDependants, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error fetching customer:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to fetch customer", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + // Initialize database + await db.read(); + + // Parse customer ID from params + const { id } = await params; + const customerId = parseInt(id); + + if (isNaN(customerId)) { + return NextResponse.json( + { + success: false, + message: "Invalid customer ID provided", + }, + { status: 400 } + ); + } + + // Parse and validate request body + const body = await request.json(); + console.log("Received customer update data:", body); + + // Find the customer to update + const customerIndex = db.data!.customers.findIndex((c) => c.id === customerId); + + if (customerIndex === -1) { + return NextResponse.json( + { + success: false, + message: "Customer not found", + }, + { status: 404 } + ); + } + + // Update customer data + const updatedCustomer = { + ...db.data!.customers[customerIndex], + firstNameEn: body.customerInfo.firstNameEn, + lastNameEn: body.customerInfo.lastNameEn, + originAdd1: body.customerInfo.originAdd1, + localAdd1: body.customerInfo.localAdd1, + email: body.customerContact.email, + mobile: body.customerContact.mobile, + }; + + db.data!.customers[customerIndex] = updatedCustomer; + + // Handle dependants - remove existing ones and add new ones + db.data!.customerDependants = db.data!.customerDependants.filter( + (dependant) => dependant.custId !== customerId + ); + + // Add new dependants + const newDependants = []; + if (body.customerDependants && body.customerDependants.length > 0) { + for (const dependant of body.customerDependants) { + const newDependant = { + id: dependant.id && !isNaN(parseInt(dependant.id)) ? + parseInt(dependant.id) : + getNextId(db.data!.customerDependants), + custId: customerId, + firstNameEn: dependant.dependantInfo.firstNameEn, + lastNameEn: dependant.dependantInfo.lastNameEn, + originAdd1: dependant.dependantInfo.originAdd1, + localAdd1: dependant.dependantInfo.localAdd1, + email: dependant.dependantContact.email, + mobile: dependant.dependantContact.mobile, + }; + newDependants.push(newDependant); + db.data!.customerDependants.push(newDependant); + } + } + + // Save to database + await db.write(); + console.log("Customer updated successfully"); + + // Return success response + return NextResponse.json( + { + success: true, + message: "Customer updated successfully", + data: { + customer: updatedCustomer, + dependants: newDependants, + }, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error updating customer:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to update customer", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + // Initialize database + await db.read(); + + // Parse customer ID from params + const { id } = await params; + const customerId = parseInt(id); + + if (isNaN(customerId)) { + return NextResponse.json( + { + success: false, + message: "Invalid customer ID provided", + }, + { status: 400 } + ); + } + + // Find the customer to delete + const customerIndex = db.data!.customers.findIndex((c) => c.id === customerId); + + if (customerIndex === -1) { + return NextResponse.json( + { + success: false, + message: "Customer not found", + }, + { status: 404 } + ); + } + + // Get customer info before deletion + const customerToDelete = db.data!.customers[customerIndex]; + + // Delete customer dependants first + const dependantsToDelete = db.data!.customerDependants.filter( + (dependant) => dependant.custId === customerId + ); + + db.data!.customerDependants = db.data!.customerDependants.filter( + (dependant) => dependant.custId !== customerId + ); + + // Delete the customer + db.data!.customers.splice(customerIndex, 1); + + // Save to database + await db.write(); + + console.log(`Customer ${customerId} and ${dependantsToDelete.length} dependants deleted successfully`); + + // Return success response + return NextResponse.json( + { + success: true, + message: `Customer and ${dependantsToDelete.length} dependant(s) deleted successfully`, + data: { + deletedCustomer: customerToDelete, + deletedDependants: dependantsToDelete, + }, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error deleting customer:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to delete customer", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/customer/route.ts b/src/app/api/customer/route.ts new file mode 100644 index 0000000..28ef9d5 --- /dev/null +++ b/src/app/api/customer/route.ts @@ -0,0 +1,189 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db, getNextId } from "@/database/database"; +import { Customer, CustomerDependant } from "@/database/database.schema"; +import { customerFormSchema } from "@/schemas/customer.schema"; + +export async function POST(request: NextRequest) { + try { + // Initialize database + await db.read(); + + // Parse and validate request body + const body = await request.json(); + console.log("Received customer data:", body); + + const validatedData = customerFormSchema.parse(body); + + // Create customer object + const newCustomer: Customer = { + id: getNextId(db.data!.customers), + firstNameEn: validatedData.customerInfo.firstNameEn, + lastNameEn: validatedData.customerInfo.lastNameEn, + email: validatedData.customerContact.email, + mobile: validatedData.customerContact.mobile, + originAdd1: validatedData.customerInfo.originAdd1, + localAdd1: validatedData.customerInfo.localAdd1, + }; + + // Add customer to database + db.data!.customers.push(newCustomer); + console.log("Customer added to database:", newCustomer); + + // Create customer dependants if any + const customerDependants: CustomerDependant[] = []; + if (validatedData.customerDependants && validatedData.customerDependants.length > 0) { + for (const dependant of validatedData.customerDependants) { + const newDependant: CustomerDependant = { + id: getNextId(db.data!.customerDependants), + custId: newCustomer.id, + firstNameEn: dependant.dependantInfo.firstNameEn, + lastNameEn: dependant.dependantInfo.lastNameEn, + email: dependant.dependantContact.email, + mobile: dependant.dependantContact.mobile, + originAdd1: dependant.dependantInfo.originAdd1, + localAdd1: dependant.dependantInfo.localAdd1, + }; + customerDependants.push(newDependant); + db.data!.customerDependants.push(newDependant); + } + console.log("Customer dependants added:", customerDependants); + } + + // Save to database + await db.write(); + console.log("Database updated successfully"); + + // Return success response + return NextResponse.json( + { + success: true, + message: "Customer and dependants saved successfully", + data: { + customer: newCustomer, + dependants: customerDependants, + }, + }, + { status: 201 } + ); + } catch (error) { + console.error("Error saving customer:", error); + + // Handle validation errors + if (error instanceof Error && error.name === "ZodError") { + return NextResponse.json( + { + success: false, + message: "Invalid data provided", + errors: error.message, + }, + { status: 400 } + ); + } + + // Handle other errors + return NextResponse.json( + { + success: false, + message: "Failed to save customer", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + +export async function GET(request: NextRequest) { + try { + // Initialize database + await db.read(); + + // Get query parameters + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get("page") || "1"); + const pageSize = parseInt(searchParams.get("pageSize") || "10"); + const search = searchParams.get("search") || ""; + const sortBy = searchParams.get("sortBy") || "id"; + const sortOrder = searchParams.get("sortOrder") || "asc"; + + // Get all customers with their dependants + const customers = db.data!.customers; + const customerDependants = db.data!.customerDependants; + + // Group dependants by customer ID + let customersWithDependants = customers.map((customer) => ({ + ...customer, + name: `${customer.firstNameEn} ${customer.lastNameEn}`, + dependants: customerDependants.filter( + (dependant) => dependant.custId === customer.id + ), + })); + + // Apply search filter + if (search) { + const searchLower = search.toLowerCase(); + customersWithDependants = customersWithDependants.filter((customer) => + customer.firstNameEn.toLowerCase().includes(searchLower) || + customer.lastNameEn.toLowerCase().includes(searchLower) || + customer.email.toLowerCase().includes(searchLower) || + customer.mobile.includes(search) + ); + } + + // Apply sorting + customersWithDependants.sort((a, b) => { + let aValue: string | number = a[sortBy as keyof typeof a] as string | number; + let bValue: string | number = b[sortBy as keyof typeof b] as string | number; + + // Handle special cases + if (sortBy === "name") { + aValue = `${a.firstNameEn} ${a.lastNameEn}`; + bValue = `${b.firstNameEn} ${b.lastNameEn}`; + } + + if (typeof aValue === "string") { + aValue = aValue.toLowerCase(); + bValue = (bValue as string).toLowerCase(); + } + + if (sortOrder === "desc") { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } else { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } + }); + + // Calculate pagination + const total = customersWithDependants.length; + const totalPages = Math.ceil(total / pageSize); + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedCustomers = customersWithDependants.slice(startIndex, endIndex); + + return NextResponse.json( + { + success: true, + data: paginatedCustomers, + pagination: { + page, + pageSize, + total, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + }, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error fetching customers:", error); + + return NextResponse.json( + { + success: false, + message: "Failed to fetch customers", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/permissions/route.ts b/src/app/api/permissions/route.ts new file mode 100644 index 0000000..88a4827 --- /dev/null +++ b/src/app/api/permissions/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import db from "@/database/db.json"; + +// GET /api/permissions - Get all available permissions +export async function GET() { + try { + // Get all active permissions + const permissions = db.permissions?.filter(p => p.isActive) || []; + + return NextResponse.json(permissions); + } catch (error) { + console.error("Error fetching permissions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/[id]/permissions/route.ts b/src/app/api/user/[id]/permissions/route.ts new file mode 100644 index 0000000..de75e28 --- /dev/null +++ b/src/app/api/user/[id]/permissions/route.ts @@ -0,0 +1,234 @@ +import { NextRequest, NextResponse } from "next/server"; +import db from "@/database/db.json"; +import { writeFileSync } from "fs"; +import { join } from "path"; +import { UserPermission, User, Permission } from "@/database/database.schema"; + +// Helper function to write to db.json +function writeDB() { + const dbPath = join(process.cwd(), "src/database/db.json"); + writeFileSync(dbPath, JSON.stringify(db, null, 2)); +} + +// GET /api/user/[id]/permissions - Get all permissions for a user +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json({ error: "Invalid user ID" }, { status: 400 }); + } + + // Check if user exists + const user = db.users?.find((u: User) => u.id === userId); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Get user permissions with permission details + const userPermissions = db.userPermissions?.filter((up: UserPermission) => up.userId === userId) || []; + const userPermissionsWithDetails = userPermissions.map((up: UserPermission) => { + const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId); + return { + ...up, + permission + }; + }); + + return NextResponse.json(userPermissionsWithDetails); + } catch (error) { + console.error("Error fetching user permissions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// POST /api/user/[id]/permissions - Add permissions to a user +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json({ error: "Invalid user ID" }, { status: 400 }); + } + + // Check if user exists + const user = db.users?.find((u: User) => u.id === userId); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const { permissionIds } = await request.json(); + + if (!Array.isArray(permissionIds)) { + return NextResponse.json({ error: "Permission IDs must be an array" }, { status: 400 }); + } + + // Validate permission IDs + const validPermissionIds = permissionIds.filter((id: number) => + db.permissions?.find((p: Permission) => p.id === id && p.isActive) + ); + + if (validPermissionIds.length !== permissionIds.length) { + return NextResponse.json({ error: "Some permission IDs are invalid" }, { status: 400 }); + } + + // Remove existing permissions for this user + if (db.userPermissions) { + db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId); + } + + // Add new permissions + const currentUserPermissions = db.userPermissions || []; + const maxId = currentUserPermissions.length > 0 ? Math.max(...currentUserPermissions.map((up: UserPermission) => up.id)) : 0; + + const newUserPermissions: UserPermission[] = validPermissionIds.map((permissionId: number, index: number) => ({ + id: maxId + index + 1, + userId, + permissionId + })); + + if (db.userPermissions) { + db.userPermissions.push(...newUserPermissions); + } else { + db.userPermissions = newUserPermissions; + } + + writeDB(); + + // Get updated permissions with details + const updatedPermissions = newUserPermissions.map((up: UserPermission) => { + const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId); + return { + ...up, + permission + }; + }); + + return NextResponse.json(updatedPermissions); + } catch (error) { + console.error("Error updating user permissions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// PUT /api/user/[id]/permissions - Update permissions for a user +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json({ error: "Invalid user ID" }, { status: 400 }); + } + + // Check if user exists + const user = db.users?.find((u: User) => u.id === userId); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const { permissionIds } = await request.json(); + + if (!Array.isArray(permissionIds)) { + return NextResponse.json({ error: "Permission IDs must be an array" }, { status: 400 }); + } + + // Validate permission IDs + const validPermissionIds = permissionIds.filter((id: number) => + db.permissions?.find((p: Permission) => p.id === id && p.isActive) + ); + + if (validPermissionIds.length !== permissionIds.length) { + return NextResponse.json({ error: "Some permission IDs are invalid" }, { status: 400 }); + } + + // Remove existing permissions for this user + if (db.userPermissions) { + db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId); + } + + // Add new permissions + const currentUserPermissions = db.userPermissions || []; + const maxId = currentUserPermissions.length > 0 ? Math.max(...currentUserPermissions.map((up: UserPermission) => up.id)) : 0; + + const newUserPermissions: UserPermission[] = validPermissionIds.map((permissionId: number, index: number) => ({ + id: maxId + index + 1, + userId, + permissionId + })); + + if (db.userPermissions) { + db.userPermissions.push(...newUserPermissions); + } else { + db.userPermissions = newUserPermissions; + } + + writeDB(); + + // Get updated permissions with details + const updatedPermissions = newUserPermissions.map((up: UserPermission) => { + const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId); + return { + ...up, + permission + }; + }); + + return NextResponse.json(updatedPermissions); + } catch (error) { + console.error("Error updating user permissions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// DELETE /api/user/[id]/permissions - Remove all permissions from a user +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json({ error: "Invalid user ID" }, { status: 400 }); + } + + // Check if user exists + const user = db.users?.find((u: User) => u.id === userId); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Remove all permissions for this user + if (db.userPermissions) { + db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId); + } + + writeDB(); + + return NextResponse.json({ message: "All permissions removed successfully" }); + } catch (error) { + console.error("Error removing user permissions:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/[id]/route.ts b/src/app/api/user/[id]/route.ts new file mode 100644 index 0000000..f0219a0 --- /dev/null +++ b/src/app/api/user/[id]/route.ts @@ -0,0 +1,172 @@ +import { NextResponse } from "next/server"; +import db from "@/database/db.json"; +import { writeFileSync } from "fs"; +import { join } from "path"; + +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json( + { success: false, message: "Invalid user ID" }, + { status: 400 } + ); + } + + const user = db.users.find(u => u.id === userId && !u.isDeleted); + + if (!user) { + return NextResponse.json( + { success: false, message: "User not found" }, + { status: 404 } + ); + } + + // Return user without password + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: _, ...userResponse } = user; + + return NextResponse.json({ + success: true, + data: userResponse, + }); + } catch (error) { + console.error("Error fetching user:", error); + return NextResponse.json( + { success: false, message: "Failed to fetch user" }, + { status: 500 } + ); + } +} + +export async function PUT( + request: Request, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json( + { success: false, message: "Invalid user ID" }, + { status: 400 } + ); + } + + const body = await request.json(); + const { username, email, firstName, lastName, password } = body; + + // Validate required fields + if (!username || !email || !firstName || !lastName) { + return NextResponse.json( + { success: false, message: "Username, email, firstName, and lastName are required" }, + { status: 400 } + ); + } + + // Find user + const userIndex = db.users.findIndex(u => u.id === userId && !u.isDeleted); + + if (userIndex === -1) { + return NextResponse.json( + { success: false, message: "User not found" }, + { status: 404 } + ); + } + + // Check if username or email already exists (excluding current user) + const existingUser = db.users.find( + (user) => + user.id !== userId && + (user.username === username || user.email === email) && + !user.isDeleted + ); + + if (existingUser) { + return NextResponse.json( + { success: false, message: "Username or email already exists" }, + { status: 400 } + ); + } + + // Update user + const updatedUser = { + ...db.users[userIndex], + username, + email, + firstName, + lastName, + ...(password && { password }), // Only update password if provided + }; + + db.users[userIndex] = updatedUser; + + // Write to file + const dbPath = join(process.cwd(), "src/database/db.json"); + writeFileSync(dbPath, JSON.stringify(db, null, 2)); + + // Return success response (without password) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: _, ...userResponse } = updatedUser; + + return NextResponse.json({ + success: true, + data: userResponse, + message: "User updated successfully", + }); + } catch (error) { + console.error("Error updating user:", error); + return NextResponse.json( + { success: false, message: "Failed to update user" }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: Request, + { params }: { params: { id: string } } +) { + try { + const userId = parseInt(params.id); + + if (isNaN(userId)) { + return NextResponse.json( + { success: false, message: "Invalid user ID" }, + { status: 400 } + ); + } + + // Find user + const userIndex = db.users.findIndex(u => u.id === userId && !u.isDeleted); + + if (userIndex === -1) { + return NextResponse.json( + { success: false, message: "User not found" }, + { status: 404 } + ); + } + + // Soft delete user + db.users[userIndex].isDeleted = true; + + // Write to file + const dbPath = join(process.cwd(), "src/database/db.json"); + writeFileSync(dbPath, JSON.stringify(db, null, 2)); + + return NextResponse.json({ + success: true, + message: "User deleted successfully", + }); + } catch (error) { + console.error("Error deleting user:", error); + return NextResponse.json( + { success: false, message: "Failed to delete user" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts new file mode 100644 index 0000000..816c773 --- /dev/null +++ b/src/app/api/user/route.ts @@ -0,0 +1,150 @@ +import { NextResponse } from "next/server"; +import db from "@/database/db.json"; +import { writeFileSync } from "fs"; +import { join } from "path"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get("page") || "1", 10); + const pageSize = parseInt(searchParams.get("pageSize") || "10", 10); + const search = (searchParams.get("search") || "").toLowerCase(); + const sortBy = searchParams.get("sortBy") || "id"; + const sortOrder = searchParams.get("sortOrder") || "asc"; + + const users = db.users || []; + + // Filter users based on search query + let filtered = users.filter(user => !user.isDeleted); + if (search) { + filtered = filtered.filter( + (u) => + `${u.firstName} ${u.lastName}`.toLowerCase().includes(search) || + u.email.toLowerCase().includes(search) || + u.username.toLowerCase().includes(search) + ); + } + + // Sort users + filtered.sort((a, b) => { + let aValue: string | number | boolean = a[sortBy as keyof typeof a]; + let bValue: string | number | boolean = b[sortBy as keyof typeof b]; + + // Handle special cases + if (sortBy === "fullName") { + aValue = `${a.firstName} ${a.lastName}`; + bValue = `${b.firstName} ${b.lastName}`; + } + + // Convert to comparable values + const aCompare = typeof aValue === "string" ? aValue.toLowerCase() : aValue; + const bCompare = typeof bValue === "string" ? bValue.toLowerCase() : bValue; + + if (sortOrder === "desc") { + return aCompare > bCompare ? -1 : aCompare < bCompare ? 1 : 0; + } else { + return aCompare < bCompare ? -1 : aCompare > bCompare ? 1 : 0; + } + }); + + const total = filtered.length; + const totalPages = Math.ceil(total / pageSize); + const start = (page - 1) * pageSize; + const end = start + pageSize; + const data = filtered.slice(start, end); + + const pagination = { + page, + pageSize, + total, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + }; + + return NextResponse.json({ + success: true, + data, + pagination, + }); + } catch (error) { + console.error("Error fetching users:", error); + return NextResponse.json({ + success: false, + data: [], + pagination: { + page: 1, + pageSize: 10, + total: 0, + totalPages: 0, + hasNextPage: false, + hasPreviousPage: false, + }, + message: "Failed to fetch users", + }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { username, email, firstName, lastName, password } = body; + + // Validate required fields + if (!username || !email || !firstName || !lastName || !password) { + return NextResponse.json( + { success: false, message: "All fields are required" }, + { status: 400 } + ); + } + + // Check if username or email already exists + const existingUser = db.users.find( + (user) => + (user.username === username || user.email === email) && + !user.isDeleted + ); + + if (existingUser) { + return NextResponse.json( + { success: false, message: "Username or email already exists" }, + { status: 400 } + ); + } + + // Generate new ID + const newId = Math.max(...db.users.map(u => u.id), 0) + 1; + + // Create new user + const newUser = { + id: newId, + username, + email, + firstName, + lastName, + password, // Note: In production, this should be hashed + isDeleted: false, + }; + + // Add to database + db.users.push(newUser); + + // Write to file + const dbPath = join(process.cwd(), "src/database/db.json"); + writeFileSync(dbPath, JSON.stringify(db, null, 2)); + + // Return success response (without password) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: _, ...userResponse } = newUser; + return NextResponse.json( + { success: true, data: userResponse, message: "User created successfully" }, + { status: 201 } + ); + } catch (error) { + console.error("Error creating user:", error); + return NextResponse.json( + { success: false, message: "Failed to create user" }, + { status: 500 } + ); + } +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..5fe8ece --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import axios from "axios"; +import { useState } from "react"; +import { loginSchema } from "@/schemas/auth.schema"; +import { Form } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +type LoginForm = z.infer; + +export default function LoginPage() { + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { username: "", password: "" }, + }); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const onSubmit = async (data: LoginForm) => { + setError(""); + setLoading(true); + try { + const res = await axios.post("/api/auth", data); + if (res.data.success) { + // You can redirect or set session here + window.location.href = "/modules/user"; // Redirect to user page on successful login + } else { + setError(res.data.message || "Login failed"); + } + } catch (e: any) { + setError(e.response?.data?.message || "Login failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Login

+
+ +
+ + + {form.formState.errors.username && ( +

+ {form.formState.errors.username.message as string} +

+ )} +
+
+ + + {form.formState.errors.password && ( +

+ {form.formState.errors.password.message as string} +

+ )} +
+ {error &&
{error}
} + +
+ +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..9711daf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --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); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..c2c7a9b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; const geistSans = Geist({ @@ -28,6 +29,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} + ); diff --git a/src/app/modules/customer/[id]/page.tsx b/src/app/modules/customer/[id]/page.tsx new file mode 100644 index 0000000..cc577cb --- /dev/null +++ b/src/app/modules/customer/[id]/page.tsx @@ -0,0 +1,630 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useParams, useRouter } from "next/navigation"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Plus, Trash2, ArrowLeft, Save, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { + customerFormSchema, + customerDependantFormSchema, + type CustomerForm, + type CustomerDependantForm, +} from "@/schemas/customer.schema"; +import axios from "axios"; +import { Header } from "@/components/common/header"; + +export default function CustomerEditPage() { + const params = useParams(); + const router = useRouter(); + const customerId = params.id as string; + + const [activeTab, setActiveTab] = useState("info"); + const [dependantDialogOpen, setDependantDialogOpen] = useState(false); + const [dependantDialogTab, setDependantDialogTab] = useState("info"); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [editingDependant, setEditingDependant] = useState(null); + + // Main form + const form = useForm({ + resolver: zodResolver(customerFormSchema), + defaultValues: { + customerInfo: { + firstNameEn: "", + lastNameEn: "", + originAdd1: "", + localAdd1: "", + }, + customerContact: { + email: "", + mobile: "", + }, + customerDependants: [], + }, + }); + + // Dependant dialog form + const dependantForm = useForm({ + resolver: zodResolver(customerDependantFormSchema), + defaultValues: { + dependantInfo: { + firstNameEn: "", + lastNameEn: "", + originAdd1: "", + localAdd1: "", + }, + dependantContact: { + email: "", + mobile: "", + }, + }, + }); + + const watchedDependants = form.watch("customerDependants"); + + // Fetch customer data on component mount + useEffect(() => { + const fetchCustomerData = async () => { + try { + setIsLoading(true); + const response = await axios.get(`/api/customer/${customerId}`); + + if (response.data.success) { + const customerData = response.data.data; + + // Populate form with customer data + form.setValue("customerInfo.firstNameEn", customerData.firstNameEn); + form.setValue("customerInfo.lastNameEn", customerData.lastNameEn); + form.setValue("customerInfo.originAdd1", customerData.originAdd1); + form.setValue("customerInfo.localAdd1", customerData.localAdd1); + form.setValue("customerContact.email", customerData.email); + form.setValue("customerContact.mobile", customerData.mobile); + + // Convert dependants to the expected format + const dependants: CustomerDependantForm[] = customerData.dependants.map((dep: { + id: number; + firstNameEn: string; + lastNameEn: string; + email: string; + mobile: string; + originAdd1: string; + localAdd1: string; + }) => ({ + id: dep.id, + dependantInfo: { + firstNameEn: dep.firstNameEn, + lastNameEn: dep.lastNameEn, + originAdd1: dep.originAdd1, + localAdd1: dep.localAdd1, + }, + dependantContact: { + email: dep.email, + mobile: dep.mobile, + }, + })); + + form.setValue("customerDependants", dependants); + + toast.success("Customer data loaded successfully"); + } else { + toast.error(response.data.message || "Failed to load customer data"); + } + } catch (error) { + console.error("Error fetching customer data:", error); + toast.error("Failed to load customer data"); + } finally { + setIsLoading(false); + } + }; + + if (customerId) { + fetchCustomerData(); + } + }, [customerId, form]); + + const onSubmit = async (data: CustomerForm) => { + try { + setIsSaving(true); + console.log("Submitting customer data:", data); + + const response = await axios.put(`/api/customer/${customerId}`, data); + console.log("API response:", response.data); + + if (response.data.success) { + toast.success("Customer updated successfully!"); + router.push("/modules/customer"); + } else { + throw new Error(response.data.message || "Failed to update customer"); + } + } catch (error) { + console.error("Error updating customer:", error); + toast.error("Failed to update customer. Please try again."); + } finally { + setIsSaving(false); + } + }; + + // handleSaveClick removed: now using form submit + + const onDependantSubmit = async (data: CustomerDependantForm) => { + try { + // Validate the entire dependant form + const isValid = await dependantForm.trigger(); + + if (!isValid) { + toast.error("Please fill in all required fields."); + return; + } + + const currentDependants = form.getValues("customerDependants"); + + if (editingDependant) { + // Update existing dependant + const updatedDependants = currentDependants.map((dep) => + dep.id === editingDependant.id + ? { + ...dep, + firstNameEn: data.dependantInfo.firstNameEn, + lastNameEn: data.dependantInfo.lastNameEn, + originAdd1: data.dependantInfo.originAdd1, + localAdd1: data.dependantInfo.localAdd1, + email: data.dependantContact.email, + mobile: data.dependantContact.mobile, + } + : dep + ); + form.setValue("customerDependants", updatedDependants); + toast.success("Dependant updated successfully!"); + } else { + // Add new dependant - just add to the table, don't submit parent form + const newDependant: CustomerDependantForm = { + id: Date.now(), + dependantInfo: { + firstNameEn: data.dependantInfo.firstNameEn, + lastNameEn: data.dependantInfo.lastNameEn, + originAdd1: data.dependantInfo.originAdd1, + localAdd1: data.dependantInfo.localAdd1, + }, + dependantContact: { + email: data.dependantContact.email, + mobile: data.dependantContact.mobile, + }, + }; + + // Add new row to dependant table without submitting parent form + form.setValue("customerDependants", [...currentDependants, newDependant]); + toast.success("Dependant added successfully!"); + } + + // Reset dependant form and close dialog + dependantForm.reset(); + setDependantDialogOpen(false); + setDependantDialogTab("info"); + setEditingDependant(null); + } catch (error) { + console.error("Error handling dependant:", error); + toast.error("Failed to save dependant. Please try again."); + } + }; + + const handleEditDependant = (dependant: CustomerDependantForm) => { + setEditingDependant(dependant); + dependantForm.setValue("dependantInfo.firstNameEn", dependant.dependantInfo.firstNameEn); + dependantForm.setValue("dependantInfo.lastNameEn", dependant.dependantInfo.lastNameEn); + dependantForm.setValue("dependantInfo.originAdd1", dependant.dependantInfo.originAdd1); + dependantForm.setValue("dependantInfo.localAdd1", dependant.dependantInfo.localAdd1); + dependantForm.setValue("dependantContact.email", dependant.dependantContact.email); + dependantForm.setValue("dependantContact.mobile", dependant.dependantContact.mobile); + setDependantDialogOpen(true); + setDependantDialogTab("info"); + }; + + const handleAddDependant = () => { + setEditingDependant(null); + dependantForm.reset(); + setDependantDialogOpen(true); + setDependantDialogTab("info"); + }; + + const removeDependant = (dependantId: number | undefined) => { + if (!dependantId) return; + + const currentDependants = form.getValues("customerDependants"); + const updatedDependants = currentDependants.filter( + (dependant) => dependant.id !== dependantId + ); + form.setValue("customerDependants", updatedDependants); + toast.success("Dependant removed successfully!"); + }; + + if (isLoading) { + return ( +
+
+ + Loading customer data... +
+
+ ); + } + + return ( +
+
+
+
+
+ +

Edit Customer

+
+ +
+ +
+ + + + Customer Info + Contact Details + Dependants + + + + + + Customer Information + Edit the customer's basic information + + +
+ ( + + First Name (English) + + + + + + )} + /> + ( + + Last Name (English) + + + + + + )} + /> +
+
+ ( + + Origin Address + + + + + + )} + /> + ( + + Local Address + + + + + + )} + /> +
+
+
+
+ + + + + Contact Details + Edit the customer's contact information + + + ( + + Email + + + + + + )} + /> + ( + + Mobile + + + + + + )} + /> + + + + + + + + Customer Dependants + Manage customer dependants + + +
+

+ Dependants ({watchedDependants.length}) +

+ + + + + + + + {editingDependant ? "Edit Dependant" : "Add New Dependant"} + + + + + + + Personal Info + Contact Details + + + +
+ ( + + First Name (English) + + + + + + )} + /> + ( + + Last Name (English) + + + + + + )} + /> +
+
+ ( + + Origin Address + + + + + + )} + /> + ( + + Local Address + + + + + + )} + /> +
+
+ + + ( + + Email + + + + + + )} + /> + ( + + Mobile + + + + + + )} + /> + +
+ +
+ + +
+ + +
+
+
+ + {watchedDependants.length > 0 ? ( + + + + Name + Email + Mobile + Origin Address + Local Address + Actions + + + + {watchedDependants.map((dependant) => ( + + + {dependant.dependantInfo.firstNameEn} {dependant.dependantInfo.lastNameEn} + + {dependant.dependantContact.email} + {dependant.dependantContact.mobile} + {dependant.dependantInfo.originAdd1} + {dependant.dependantInfo.localAdd1} + +
+ + +
+
+
+ ))} +
+
+ ) : ( +
+ No dependants added yet. Click "Add Dependant" to get started. +
+ )} +
+
+
+
+ + +
+
+ ); +} diff --git a/src/app/modules/customer/add/page.tsx b/src/app/modules/customer/add/page.tsx new file mode 100644 index 0000000..aed4af3 --- /dev/null +++ b/src/app/modules/customer/add/page.tsx @@ -0,0 +1,588 @@ +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Plus, Trash2, ArrowLeft } from "lucide-react"; +import { toast } from "sonner"; +import { + customerFormSchema, + customerDependantFormSchema, + type CustomerForm, + type CustomerDependantForm, +} from "@/schemas/customer.schema"; +import axios from "axios"; +import { Header } from "@/components/common/header"; + +export default function CustomerAddPage() { + const router = useRouter(); + const [activeTab, setActiveTab] = useState("info"); + const [dependantDialogOpen, setDependantDialogOpen] = useState(false); + const [dependantDialogTab, setDependantDialogTab] = useState("info"); + + // Main form + const form = useForm({ + resolver: zodResolver(customerFormSchema), + defaultValues: { + customerInfo: { + firstNameEn: "", + lastNameEn: "", + originAdd1: "", + localAdd1: "", + }, + customerContact: { + email: "", + mobile: "", + }, + customerDependants: [], + }, + }); + + // Dependant dialog form + const dependantForm = useForm({ + resolver: zodResolver(customerDependantFormSchema), + defaultValues: { + dependantInfo: { + firstNameEn: "", + lastNameEn: "", + originAdd1: "", + localAdd1: "", + }, + dependantContact: { + email: "", + mobile: "", + }, + }, + }); + + const watchedDependants = form.watch("customerDependants"); + + const onSubmit = async (data: CustomerForm) => { + try { + const response = await axios.post("/api/customer", data); + + if (response.status !== 200 && response.status !== 201) { + throw new Error(response.data?.message || "Failed to save customer"); + } + + console.log("Customer saved successfully:", response.data); + toast.success("Customer added successfully!"); + + // Reset form after successful submission + form.reset(); + + // Redirect to customer page + router.push("/modules/customer"); + } catch (error) { + console.error("Error submitting form:", error); + toast.error("Failed to add customer. Please try again."); + } + }; + + const onDependantSubmit = async (data: CustomerDependantForm) => { + try { + // Validate the entire dependant form + const isValid = await dependantForm.trigger(); + + if (!isValid) { + toast.error("Please fill in all required fields."); + return; + } + + const newDependant: CustomerDependantForm = { + id: Date.now(), // Generate unique ID + dependantInfo: { + firstNameEn: data.dependantInfo.firstNameEn, + lastNameEn: data.dependantInfo.lastNameEn, + originAdd1: data.dependantInfo.originAdd1, + localAdd1: data.dependantInfo.localAdd1, + }, + dependantContact: { + email: data.dependantContact.email, + mobile: data.dependantContact.mobile, + }, + }; + + const currentDependants = form.getValues("customerDependants"); + form.setValue("customerDependants", [...currentDependants, newDependant]); + + // Reset dependant form and close dialog + dependantForm.reset(); + setDependantDialogOpen(false); + setDependantDialogTab("info"); + + toast.success("Dependant added successfully!"); + } catch (error) { + console.error("Error adding dependant:", error); + toast.error("Failed to add dependant. Please try again."); + } + }; + + const removeDependant = (dependantId: number) => { + const currentDependants = form.getValues("customerDependants"); + const updatedDependants = currentDependants.filter(dep => dep.id !== dependantId); + form.setValue("customerDependants", updatedDependants); + toast.success("Dependant removed successfully!"); + }; + + const handleNextTab = async () => { + if (activeTab === "info") { + // Validate customer info fields before proceeding + const isValid = await form.trigger([ + "customerInfo.firstNameEn", + "customerInfo.lastNameEn", + "customerInfo.originAdd1", + "customerInfo.localAdd1" + ]); + if (isValid) { + setActiveTab("contact"); + } else { + toast.error("Please fill in all required fields in Customer Info tab."); + } + } else if (activeTab === "contact") { + // Validate customer contact fields before proceeding + const isValid = await form.trigger(["customerContact.email", "customerContact.mobile"]); + if (isValid) { + setActiveTab("dependants"); + } else { + toast.error("Please fill in all required fields in Customer Contact tab."); + } + } + }; + + const handlePreviousTab = () => { + if (activeTab === "contact") { + setActiveTab("info"); + } else if (activeTab === "dependants") { + setActiveTab("contact"); + } + }; + + const isTabValid = (tabName: string) => { + const values = form.getValues(); + const errors = form.formState.errors; + + if (tabName === "info") { + return ( + values.customerInfo?.firstNameEn?.trim() !== "" && + values.customerInfo?.lastNameEn?.trim() !== "" && + values.customerInfo?.originAdd1?.trim() !== "" && + values.customerInfo?.localAdd1?.trim() !== "" && + !errors.customerInfo?.firstNameEn && + !errors.customerInfo?.lastNameEn && + !errors.customerInfo?.originAdd1 && + !errors.customerInfo?.localAdd1 + ); + } else if (tabName === "contact") { + return ( + values.customerContact?.email?.trim() !== "" && + values.customerContact?.mobile?.trim() !== "" && + !errors.customerContact?.email && + !errors.customerContact?.mobile + ); + } + return true; + }; + + return ( +
+
+
+
+
+ +

Add New Customer

+
+
+ + + + Please fill in the customer information across the three tabs below. + + + +
+ + + + + Customer Info + + + Customer Contact + + + Customer Dependants + + + + {/* Tab 1: Customer Info */} + +
+ ( + + First Name (English) + + + + + + )} + /> + ( + + Last Name (English) + + + + + + )} + /> +
+
+ ( + + Origin Address + + + + + + )} + /> + ( + + Local Address + + + + + + )} + /> +
+
+ +
+
+ + {/* Tab 2: Customer Contact */} + +
+ ( + + Email + + + + + + )} + /> + ( + + Mobile Number + + + + + + )} + /> +
+
+ + +
+
+ + {/* Tab 3: Customer Dependants */} + +
+

Customer Dependants

+ + + + + + + Add New Dependant + + + + + + Dependant Info + Dependant Contact + + + +
+ ( + + First Name (English) + + + + + + )} + /> + ( + + Last Name (English) + + + + + + )} + /> +
+
+ ( + + Origin Address + + + + + + )} + /> + ( + + Local Address + + + + + + )} + /> +
+
+ +
+
+ + +
+ ( + + Email + + + + + + )} + /> + ( + + Mobile Number + + + + + + )} + /> +
+
+ +
+
+
+
+ + +
+ + +
+
+
+ + {watchedDependants.length > 0 ? ( +
+ + + + First Name + Last Name + Email + Mobile + Origin Address + Local Address + Actions + + + + {watchedDependants.map((dependant) => ( + + {dependant.dependantInfo.firstNameEn} + {dependant.dependantInfo.lastNameEn} + {dependant.dependantContact.email} + {dependant.dependantContact.mobile} + {dependant.dependantInfo.originAdd1} + {dependant.dependantInfo.localAdd1} + + + + + ))} + +
+
+ ) : ( +
+ No dependants added yet. Click "Add Dependant" to add one. +
+ )} + +
+ + +
+
+
+ + +
+
+
+
+ ); +} diff --git a/src/app/modules/customer/page.tsx b/src/app/modules/customer/page.tsx new file mode 100644 index 0000000..14894fd --- /dev/null +++ b/src/app/modules/customer/page.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Loader2, Users, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; +import axios from "axios"; +import { ServerDataTable } from "@/components/ui/server-data-table"; +import { Customer, createCustomerColumns } from "@/components/customers/customer-columns"; +import { Header } from "@/components/common/header"; + +interface PaginationInfo { + page: number; + pageSize: number; + total: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +interface CustomerResponse { + success: boolean; + data: Customer[]; + pagination: PaginationInfo; + message?: string; +} + +export default function CustomerPage() { + const router = useRouter(); + const [customers, setCustomers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [query, setQuery] = useState({ + page: 1, + pageSize: 10, + search: "", + sortBy: "id", + sortOrder: "asc" as "asc" | "desc", + }); + const [pagination, setPagination] = useState({ + page: 1, + pageSize: 10, + total: 0, + totalPages: 0, + hasNextPage: false, + hasPreviousPage: false, + }); + // Initialize search and sorting state to match query + const [searchValue, setSearchValue] = useState(query.search); + const [sorting, setSorting] = useState<{ id: string; desc: boolean }[]>( + query.sortBy ? [{ id: query.sortBy, desc: query.sortOrder === "desc" }] : [] + ); + + const fetchCustomers = useCallback(async () => { + try { + setIsLoading(true); + const params = new URLSearchParams({ + page: query.page.toString(), + pageSize: query.pageSize.toString(), + search: query.search, + sortBy: query.sortBy, + sortOrder: query.sortOrder, + }); + const response = await axios.get(`/api/customer?${params}`); + if (response.data.success) { + setCustomers(response.data.data); + setPagination(response.data.pagination); + } else { + toast.error(response.data.message || "Failed to load customers"); + } + } catch (error) { + console.error("Error fetching customers:", error); + toast.error("Failed to load customers"); + } finally { + setIsLoading(false); + setIsRefreshing(false); + } + }, [query]); + + // Fetch customers when query changes + useEffect(() => { + fetchCustomers(); + }, [fetchCustomers]); + + const handleAddCustomer = useCallback(() => { + router.push("/modules/customer/add"); + }, [router]); + + const handleEditCustomer = useCallback((customerId: number) => { + router.push(`/modules/customer/${customerId}`); + }, [router]); + + const handleDeleteCustomer = useCallback(async (customerId: number) => { + // Find the customer to get their name for confirmation + const customerToDelete = customers.find(c => c.id === customerId); + const customerName = customerToDelete ? `${customerToDelete.firstNameEn} ${customerToDelete.lastNameEn}` : `Customer #${customerId}`; + + if (!window.confirm(`Are you sure you want to delete ${customerName} and all their dependants? This action cannot be undone.`)) { + return; + } + + try { + const response = await axios.delete(`/api/customer/${customerId}`); + if (response.data.success) { + toast.success(response.data.message || "Customer deleted successfully"); + // Refresh the customer list + await fetchCustomers(); + } else { + toast.error(response.data.message || "Failed to delete customer"); + } + } catch (error) { + console.error("Error deleting customer:", error); + toast.error("Failed to delete customer"); + } + }, [fetchCustomers, customers]); + + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + await fetchCustomers(); + toast.success("Customer list refreshed"); + }, [fetchCustomers]); + + const handlePaginationChange = useCallback((page: number, pageSize: number) => { + setQuery((prev) => ({ ...prev, page, pageSize })); + }, []); + + // Search handler - will be called from ServerDataTable after debounce + const handleSearchChange = useCallback((search: string) => { + setQuery((prev) => ({ ...prev, search, page: 1 })); + }, []); + + // Sorting handler - will be called from ServerDataTable + const handleSortingChange = useCallback((sortByField: string, sortOrderValue: "asc" | "desc") => { + setQuery((prev) => ({ ...prev, sortBy: sortByField, sortOrder: sortOrderValue })); + setSorting([{ id: sortByField, desc: sortOrderValue === "desc" }]); + }, []); + + const columns = useMemo(() => createCustomerColumns({ + onEdit: handleEditCustomer, + onDelete: handleDeleteCustomer, + }), [handleEditCustomer, handleDeleteCustomer]); + + if (isLoading) { + return ( +
+
+ + Loading customers... +
+
+ ); + } + + return ( +
+
+
+
+
+ +

Customer Management

+
+
+ + +
+
+ + + + Customers + + Manage your customer database ({pagination.total} total) + + + + {pagination.total > 0 ? ( + + ) : ( +
+ +

No customers found

+

Get started by adding your first customer

+ +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/modules/layout.tsx b/src/app/modules/layout.tsx new file mode 100644 index 0000000..13636b9 --- /dev/null +++ b/src/app/modules/layout.tsx @@ -0,0 +1,22 @@ +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { + SidebarInset, + SidebarProvider, +} from "@/components/ui/sidebar"; + +export default function ModulesLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ + + + {children} + + +
+ ); +} diff --git a/src/app/modules/mail-template/page.tsx b/src/app/modules/mail-template/page.tsx new file mode 100644 index 0000000..e56d251 --- /dev/null +++ b/src/app/modules/mail-template/page.tsx @@ -0,0 +1,237 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { ChevronDown, ChevronUp, Download, X, Settings, Mail, Send } from "lucide-react" +import { Header } from "@/components/common/header" + +export default function MailTemplatePage() { + const [bodyExpanded, setBodyExpanded] = useState(true) + const [footerExpanded, setFooterExpanded] = useState(true) + const [bodyText, setBodyText] = useState("HBS健康診断受診者 各位\n\nいつも、お世話になっております。") + const [footerText, setFooterText] = useState("※本件に関するお問い合わせは、\n健康管理室\nメールアドレス") + + const attachments = [ + "1-1_2021年度健康診断受診のお知らせ.doc", + "1-2_2021年度受診料金.doc", + "1-3_【別紙】健康診断受診案内.xlsx", + "", + "", + ] + + return ( +
+
+
+
+
+ +

Mail Template

+
+
+ + + +
+
+ + + + Email Template Configuration + + Configure your email template settings and content + + + + {/* Email Groups */} +
+ + +
+ + +
+
+ + +
+
+
+ + {/* Types of Emails */} +
+ + +
+ + {/* Recipient */} +
+ + +
+ + {/* Source Address */} +
+ + +
+ + {/* Submission Content (Subject) */} +
+ + +
+ + {/* Contents of Transmission (Body) */} +
+
+ +
+ + + {bodyText.length} / 1500 + +
+
+ {bodyExpanded && ( +