From 55bd885976a7d8d14432e086b9f6fe4f8f339a83 Mon Sep 17 00:00:00 2001 From: abhishek Date: Mon, 29 Jun 2026 15:19:19 +0530 Subject: [PATCH] initailization on the POS --- .gitignore | 25 + index.html | 13 + package-lock.json | 2934 +++++++++++++++++ package.json | 31 + postcss.config.js | 6 + remove_hover.js | 29 + replace_colors.js | 34 + src/App.tsx | 35 + src/components/layout/AppLayout.tsx | 103 + src/components/ui/Avatar.tsx | 22 + src/components/ui/Badge.tsx | 23 + src/components/ui/Button.tsx | 53 + src/components/ui/Card.tsx | 22 + src/components/ui/Input.tsx | 27 + src/components/ui/Modal.tsx | 63 + src/components/ui/SearchBar.tsx | 24 + src/components/ui/Select.tsx | 32 + src/components/ui/Skeleton.tsx | 5 + src/components/ui/Spinner.tsx | 10 + src/components/ui/StatCard.tsx | 33 + src/components/ui/Table.tsx | 82 + src/components/ui/Tabs.tsx | 33 + src/components/ui/TopbarAction.tsx | 14 + src/components/ui/index.ts | 14 + src/data/categories.ts | 10 + src/data/customers.ts | 12 + src/data/dashboard.ts | 33 + src/data/products.ts | 34 + src/data/promotions.ts | 9 + src/data/sales.ts | 24 + src/data/staff.ts | 16 + src/data/suppliers.ts | 45 + src/hooks/useBarcodeScanner.ts | 61 + src/index.css | 17 + src/lib/utils.ts | 40 + src/main.tsx | 10 + src/pages/auth/LoginPage.tsx | 122 + src/pages/customers/AddCustomerModal.tsx | 120 + src/pages/customers/CustomersPage.tsx | 273 ++ src/pages/dashboard/DashboardPage.tsx | 165 + src/pages/inventory/AdjustStockModal.tsx | 88 + src/pages/inventory/InventoryPage.tsx | 304 ++ src/pages/pos/CustomerIdentifyPanel.tsx | 237 ++ src/pages/pos/POSPage.tsx | 389 +++ src/pages/pos/PaymentModal.tsx | 225 ++ src/pages/pos/ReceiptModal.tsx | 159 + src/pages/pos/RefundModal.tsx | 588 ++++ src/pages/products/ProductDrawer.tsx | 176 + src/pages/products/ProductsPage.tsx | 222 ++ .../promotions/CreatePromotionDrawer.tsx | 180 + src/pages/promotions/PromotionsPage.tsx | 189 ++ src/pages/reports/ReportsPage.tsx | 255 ++ src/pages/settings/SettingsPage.tsx | 254 ++ src/pages/suppliers/AddSupplierModal.tsx | 56 + src/pages/suppliers/CreatePODrawer.tsx | 142 + src/pages/suppliers/PODetailModal.tsx | 72 + src/pages/suppliers/ReceiveStockModal.tsx | 55 + src/pages/suppliers/SuppliersPage.tsx | 161 + src/stores/authStore.ts | 30 + src/stores/cartStore.ts | 147 + src/types/index.ts | 140 + src/vite-env.d.ts | 1 + tailwind.config.js | 29 + tsconfig.app.json | 29 + tsconfig.json | 7 + tsconfig.node.json | 11 + vite.config.ts | 32 + 67 files changed, 8836 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 remove_hover.js create mode 100644 replace_colors.js create mode 100644 src/App.tsx create mode 100644 src/components/layout/AppLayout.tsx create mode 100644 src/components/ui/Avatar.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/SearchBar.tsx create mode 100644 src/components/ui/Select.tsx create mode 100644 src/components/ui/Skeleton.tsx create mode 100644 src/components/ui/Spinner.tsx create mode 100644 src/components/ui/StatCard.tsx create mode 100644 src/components/ui/Table.tsx create mode 100644 src/components/ui/Tabs.tsx create mode 100644 src/components/ui/TopbarAction.tsx create mode 100644 src/components/ui/index.ts create mode 100644 src/data/categories.ts create mode 100644 src/data/customers.ts create mode 100644 src/data/dashboard.ts create mode 100644 src/data/products.ts create mode 100644 src/data/promotions.ts create mode 100644 src/data/sales.ts create mode 100644 src/data/staff.ts create mode 100644 src/data/suppliers.ts create mode 100644 src/hooks/useBarcodeScanner.ts create mode 100644 src/index.css create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/pages/auth/LoginPage.tsx create mode 100644 src/pages/customers/AddCustomerModal.tsx create mode 100644 src/pages/customers/CustomersPage.tsx create mode 100644 src/pages/dashboard/DashboardPage.tsx create mode 100644 src/pages/inventory/AdjustStockModal.tsx create mode 100644 src/pages/inventory/InventoryPage.tsx create mode 100644 src/pages/pos/CustomerIdentifyPanel.tsx create mode 100644 src/pages/pos/POSPage.tsx create mode 100644 src/pages/pos/PaymentModal.tsx create mode 100644 src/pages/pos/ReceiptModal.tsx create mode 100644 src/pages/pos/RefundModal.tsx create mode 100644 src/pages/products/ProductDrawer.tsx create mode 100644 src/pages/products/ProductsPage.tsx create mode 100644 src/pages/promotions/CreatePromotionDrawer.tsx create mode 100644 src/pages/promotions/PromotionsPage.tsx create mode 100644 src/pages/reports/ReportsPage.tsx create mode 100644 src/pages/settings/SettingsPage.tsx create mode 100644 src/pages/suppliers/AddSupplierModal.tsx create mode 100644 src/pages/suppliers/CreatePODrawer.tsx create mode 100644 src/pages/suppliers/PODetailModal.tsx create mode 100644 src/pages/suppliers/ReceiveStockModal.tsx create mode 100644 src/pages/suppliers/SuppliersPage.tsx create mode 100644 src/stores/authStore.ts create mode 100644 src/stores/cartStore.ts create mode 100644 src/types/index.ts create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f524f06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Dependencies +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/index.html b/index.html new file mode 100644 index 0000000..3c3b23c --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Nearle Daily — POS Terminal + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d2fbb47 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2934 @@ +{ + "name": "nearle-pos", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nearle-pos", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^0.470.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.5.1", + "react-router-dom": "^7.1.1", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", + "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "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" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.470.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.470.0.tgz", + "integrity": "sha512-tqYODeoB3qU5gxH33IbL7IcF05EYYzsZQJqdM9HGGHwmoJMPvVLPHO6Plu6HNAfntucZQ41taEw6aUpwOtaoXg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0147be --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "nearle-pos", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.470.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.5.1", + "react-router-dom": "^7.1.1", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/remove_hover.js b/remove_hover.js new file mode 100644 index 0000000..731e8d8 --- /dev/null +++ b/remove_hover.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); + +function walk(dir) { + let results = []; + const list = fs.readdirSync(dir); + list.forEach(function(file) { + file = dir + '/' + file; + const stat = fs.statSync(file); + if (stat && stat.isDirectory()) { + results = results.concat(walk(file)); + } else { + if (file.endsWith('.tsx') || file.endsWith('.ts')) { + results.push(file); + } + } + }); + return results; +} + +const files = walk('./src'); +files.forEach(file => { + let content = fs.readFileSync(file, 'utf8'); + if (content.includes('hover:')) { + const newContent = content.replace(/hover:[\w\-\/]+/g, ''); + fs.writeFileSync(file, newContent, 'utf8'); + console.log(`Updated ${file}`); + } +}); diff --git a/replace_colors.js b/replace_colors.js new file mode 100644 index 0000000..ed3289d --- /dev/null +++ b/replace_colors.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +const dir = path.join(__dirname, 'src', 'pages', 'pos'); +const files = ['POSPage.tsx', 'CustomerIdentifyPanel.tsx', 'PaymentModal.tsx']; + +files.forEach(file => { + const filePath = path.join(dir, file); + let content = fs.readFileSync(filePath, 'utf-8'); + + content = content.replace(/bg-blue-600/g, 'bg-primary'); + content = content.replace(/text-blue-600/g, 'text-primary'); + content = content.replace(/border-blue-600/g, 'border-primary'); + content = content.replace(/ring-blue-600/g, 'ring-primary'); + content = content.replace(/shadow-blue-600/g, 'shadow-primary'); + + content = content.replace(/bg-blue-700/g, 'bg-primary/90'); + content = content.replace(/text-blue-700/g, 'text-primary'); + + content = content.replace(/bg-blue-800/g, 'bg-primary'); + content = content.replace(/text-blue-800/g, 'text-primary'); + + content = content.replace(/bg-blue-50/g, 'bg-primary/10'); + content = content.replace(/text-blue-500/g, 'text-primary/70'); + + content = content.replace(/bg-blue-100/g, 'bg-primary/20'); + content = content.replace(/border-blue-100/g, 'border-primary/20'); + + content = content.replace(/bg-blue-200/g, 'bg-primary/30'); + content = content.replace(/border-blue-200/g, 'border-primary/30'); + + fs.writeFileSync(filePath, content); + console.log(`Updated ${file}`); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b5d851b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,35 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import AppLayout from './components/layout/AppLayout'; +import POSPage from './pages/pos/POSPage'; +import DashboardPage from './pages/dashboard/DashboardPage'; +import ProductsPage from './pages/products/ProductsPage'; +import InventoryPage from './pages/inventory/InventoryPage'; +import CustomersPage from './pages/customers/CustomersPage'; +import PromotionsPage from './pages/promotions/PromotionsPage'; +import ReportsPage from './pages/reports/ReportsPage'; +import SuppliersPage from './pages/suppliers/SuppliersPage'; +import SettingsPage from './pages/settings/SettingsPage'; +import LoginPage from './pages/auth/LoginPage'; + +export default function App() { + return ( + + + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..935dd75 --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,103 @@ +import { useEffect } from 'react'; +import { Outlet, NavLink, useLocation, Navigate } from 'react-router-dom'; +import { ShoppingCart, LayoutDashboard, Package, Archive, Users, Tag, FileText, Truck, Settings } from 'lucide-react'; +import { products } from '@/data/products'; +import { purchaseOrders } from '@/data/suppliers'; +import logo from '../../../../logo.png'; +import { useAuthStore } from '@/stores/authStore'; + +const NAV_ITEMS = [ + { path: '/pos', label: 'POS', icon: ShoppingCart }, + { path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }, + { path: '/products', label: 'Products', icon: Package }, + { path: '/inventory', label: 'Inventory', icon: Archive }, + { path: '/customers', label: 'Customers', icon: Users }, + { path: '/promotions', label: 'Promos', icon: Tag }, + { path: '/reports', label: 'Reports', icon: FileText }, + { path: '/suppliers', label: 'Suppliers', icon: Truck }, + { path: '/settings', label: 'Settings', icon: Settings }, +]; + +export default function AppLayout() { + const location = useLocation(); + const currentNav = NAV_ITEMS.find(item => item.path === location.pathname); + const { currentUser, logout } = useAuthStore(); + + // Calculate badges + const lowStockCount = products.filter(p => p.stock <= p.reorderPoint).length; + const pendingPoCount = purchaseOrders.filter(po => po.status === 'pending').length; + + useEffect(() => { + document.title = `Nearle Daily — ${currentNav?.label || 'POS'}`; + }, [currentNav]); + + const getBadge = (path: string) => { + if (path === '/inventory' && lowStockCount > 0) return lowStockCount; + if (path === '/suppliers' && pendingPoCount > 0) return pendingPoCount; + return 0; + }; + + if (!currentUser) { + return ; + } + + return ( +
+ {/* Topbar */} +
+
+ Logo +
+
+
+
+
+
+
{currentUser.name}
+
{currentUser.role}
+
+ +
+
+
+ + {/* Content Area */} +
+
+ +
+
+ + {/* Bottom Nav */} +
+ {NAV_ITEMS.map((item) => { + const isActive = location.pathname === item.path; + return ( + +
+ + {getBadge(item.path) > 0 && ( + + {getBadge(item.path)} + + )} +
+ {item.label} +
+ ); + })} +
+
+ ); +} diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx new file mode 100644 index 0000000..28e0754 --- /dev/null +++ b/src/components/ui/Avatar.tsx @@ -0,0 +1,22 @@ + + +interface AvatarProps { + initials: string; + size?: 'sm' | 'md' | 'lg'; + color?: string; + className?: string; +} + +export default function Avatar({ initials, size = 'md', color = 'bg-primary', className = '' }: AvatarProps) { + const sizes = { + sm: 'w-8 h-8 text-xs', + md: 'w-10 h-10 text-sm', + lg: 'w-12 h-12 text-base', + }; + + return ( +
+ {initials.substring(0, 2).toUpperCase()} +
+ ); +} diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..fd88f04 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +interface BadgeProps { + variant?: 'green' | 'red' | 'amber' | 'blue' | 'gray'; + children: React.ReactNode; + className?: string; +} + +export default function Badge({ variant = 'gray', children, className = '' }: BadgeProps) { + const variants = { + green: 'bg-green-100 text-green-700', + red: 'bg-red-100 text-red-700', + amber: 'bg-amber-100 text-amber-700', + blue: 'bg-blue-100 text-blue-700', + gray: 'bg-gray-100 text-gray-700', + }; + + return ( + + {children} + + ); +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..e0c4c19 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,53 @@ +import React, { ButtonHTMLAttributes } from 'react'; +import Spinner from './Spinner'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'outline' | 'danger' | 'success' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + loading?: boolean; + icon?: React.ReactNode; +} + +export default function Button({ + variant = 'primary', + size = 'md', + loading = false, + icon, + children, + className = '', + disabled, + ...props +}: ButtonProps) { + const baseClasses = 'inline-flex items-center justify-center rounded-lg font-semibold select-none touch-manipulation transition-transform active:scale-[0.97] active:opacity-90'; + + const variants = { + primary: 'bg-primary text-white border border-transparent', + outline: 'bg-white text-gray-700 border border-gray-300', + danger: 'bg-red-600 text-white border border-transparent', + success: 'bg-green-600 text-white border border-transparent', + ghost: 'bg-transparent text-gray-700 border border-transparent active:bg-gray-100', + }; + + const sizes = { + sm: 'min-h-[40px] px-3 text-sm', + md: 'min-h-[48px] px-5 text-base', + lg: 'min-h-[56px] px-8 text-lg', + }; + + const isDisabled = disabled || loading; + + return ( + + ); +} diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..f5d9d36 --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +interface CardProps { + title?: string; + action?: React.ReactNode; + children: React.ReactNode; + className?: string; +} + +export default function Card({ title, action, children, className = '' }: CardProps) { + return ( +
+ {(title || action) && ( +
+ {title &&

{title}

} + {action &&
{action}
} +
+ )} +
{children}
+
+ ); +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..3a1eaeb --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,27 @@ +import React, { InputHTMLAttributes } from 'react'; + +interface InputProps extends InputHTMLAttributes { + label?: string; + icon?: React.ReactNode; + error?: string; +} + +export default function Input({ label, icon, error, className = '', ...props }: InputProps) { + return ( +
+ {label && } +
+ {icon && ( +
+ {icon} +
+ )} + +
+ {error && {error}} +
+ ); +} diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..e4fb94a --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { X } from 'lucide-react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: React.ReactNode; + footer?: React.ReactNode; +} + +export default function Modal({ isOpen, onClose, title, children, footer }: ModalProps) { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + // We don't need body overflow hidden here because our app is fixed 1366x768, + // but it's good practice. + } + + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop tap zone */} +
+ + {/* Modal Content */} +
+ {title && ( +
+

{title}

+ +
+ )} + +
+ {children} +
+ + {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/SearchBar.tsx b/src/components/ui/SearchBar.tsx new file mode 100644 index 0000000..e782cb6 --- /dev/null +++ b/src/components/ui/SearchBar.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Search } from 'lucide-react'; + +interface SearchBarProps { + placeholder?: string; + value: string; + onChange: (e: React.ChangeEvent) => void; + className?: string; +} + +export default function SearchBar({ placeholder = 'Search...', value, onChange, className = '' }: SearchBarProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..7da04dd --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,32 @@ +import { SelectHTMLAttributes } from 'react'; + +interface SelectOption { + value: string | number; + label: string; +} + +interface SelectProps extends SelectHTMLAttributes { + label?: string; + options: SelectOption[]; + error?: string; +} + +export default function Select({ label, options, error, className = '', ...props }: SelectProps) { + return ( +
+ {label && } + + {error && {error}} +
+ ); +} diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx new file mode 100644 index 0000000..c69e986 --- /dev/null +++ b/src/components/ui/Skeleton.tsx @@ -0,0 +1,5 @@ +export default function Skeleton({ className = '' }: { className?: string }) { + return ( +
+ ); +} diff --git a/src/components/ui/Spinner.tsx b/src/components/ui/Spinner.tsx new file mode 100644 index 0000000..297a3a1 --- /dev/null +++ b/src/components/ui/Spinner.tsx @@ -0,0 +1,10 @@ + + +export default function Spinner({ className = 'w-6 h-6' }: { className?: string }) { + return ( + + + + + ); +} diff --git a/src/components/ui/StatCard.tsx b/src/components/ui/StatCard.tsx new file mode 100644 index 0000000..6887460 --- /dev/null +++ b/src/components/ui/StatCard.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +interface StatCardProps { + label: string; + value: string | number; + change?: string | number; + changeType?: 'up' | 'down'; + icon: React.ReactNode; + iconBg?: string; +} + +export default function StatCard({ label, value, change, changeType, icon, iconBg = 'bg-primary/10 text-primary' }: StatCardProps) { + return ( +
+
+ {label} +
svg]:w-4 [&>svg]:h-4 ${iconBg}`}> + {icon} +
+
+
+ {value} + {change && ( + + {changeType === 'up' ? '↑' : changeType === 'down' ? '↓' : ''} {change} + + )} +
+
+ ); +} diff --git a/src/components/ui/Table.tsx b/src/components/ui/Table.tsx new file mode 100644 index 0000000..ce14a23 --- /dev/null +++ b/src/components/ui/Table.tsx @@ -0,0 +1,82 @@ +import React from 'react'; + +export interface Column { + key: string; + label: string; + sortable?: boolean; + render?: (item: T) => React.ReactNode; +} + +interface TableProps { + columns: Column[]; + data: T[]; + loading?: boolean; + onRowClick?: (item: T) => void; + emptyMessage?: string; + onSort?: (key: string) => void; + sortConfig?: { key: string; direction: 'asc' | 'desc' } | null; +} + +export default function Table({ + columns, + data, + loading = false, + onRowClick, + emptyMessage = 'No data found', + onSort, + sortConfig, +}: TableProps) { + return ( +
+ + + + {columns.map((col) => ( + + ))} + + + + {loading ? ( + + + + ) : data.length === 0 ? ( + + + + ) : ( + data.map((row) => ( + onRowClick?.(row)} + className={`border-b border-gray-100 last:border-0 ${onRowClick ? 'cursor-pointer active:bg-gray-50 touch-manipulation' : ''}`} + > + {columns.map((col) => ( + + ))} + + )) + )} + +
col.sortable && onSort && onSort(col.key)} + > +
+ {col.label} + {col.sortable && sortConfig?.key === col.key && ( + {sortConfig.direction === 'asc' ? '↑' : '↓'} + )} +
+
+ Loading... +
+ {emptyMessage} +
+ {col.render ? col.render(row) : (row as any)[col.key]} +
+
+ ); +} diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..ca3c6d7 --- /dev/null +++ b/src/components/ui/Tabs.tsx @@ -0,0 +1,33 @@ + + +interface Tab { + id: string; + label: string; +} + +interface TabsProps { + tabs: Tab[]; + activeTab: string; + onChange: (id: string) => void; +} + +export default function Tabs({ tabs, activeTab, onChange }: TabsProps) { + return ( +
+ {tabs.map((tab) => { + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+ ); +} diff --git a/src/components/ui/TopbarAction.tsx b/src/components/ui/TopbarAction.tsx new file mode 100644 index 0000000..d1f966b --- /dev/null +++ b/src/components/ui/TopbarAction.tsx @@ -0,0 +1,14 @@ +import { useEffect, useState, ReactNode } from 'react'; +import { createPortal } from 'react-dom'; + +export default function TopbarAction({ children }: { children: ReactNode }) { + const [targetElement, setTargetElement] = useState(null); + + useEffect(() => { + setTargetElement(document.getElementById('topbar-actions')); + }, []); + + if (!targetElement) return null; + + return createPortal(children, targetElement); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..11be5f9 --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,14 @@ +export { default as Button } from './Button'; +export { default as Badge } from './Badge'; +export { default as TopbarAction } from './TopbarAction'; +export { default as Input } from './Input'; +export { default as Select } from './Select'; +export { default as Modal } from './Modal'; +export { default as Table } from './Table'; +export { default as Skeleton } from './Skeleton'; +export { default as Card } from './Card'; +export { default as StatCard } from './StatCard'; +export { default as Spinner } from './Spinner'; +export { default as SearchBar } from './SearchBar'; +export { default as Tabs } from './Tabs'; +export { default as Avatar } from './Avatar'; diff --git a/src/data/categories.ts b/src/data/categories.ts new file mode 100644 index 0000000..71159a6 --- /dev/null +++ b/src/data/categories.ts @@ -0,0 +1,10 @@ +import { Category } from '../types'; + +export const categories: Category[] = [ + { id: 'dairy', name: 'Dairy', emoji: '🥛' }, + { id: 'grocery', name: 'Grocery', emoji: '🛒' }, + { id: 'beverages', name: 'Beverages', emoji: '🥤' }, + { id: 'snacks', name: 'Snacks', emoji: '🍪' }, + { id: 'personal', name: 'Personal Care', emoji: '🧴' }, + { id: 'household', name: 'Household', emoji: '🏠' }, +]; diff --git a/src/data/customers.ts b/src/data/customers.ts new file mode 100644 index 0000000..2270b69 --- /dev/null +++ b/src/data/customers.ts @@ -0,0 +1,12 @@ +import { Customer } from '../types'; + +export const customers: Customer[] = [ + { id: 'c1', name: 'Rahul Sharma', phone: '9876543210', email: 'rahul@example.com', dob: '1990-05-15', loyaltyPoints: 450, tier: 'gold', totalSpent: 12500, storeCredit: 0, lastVisit: '2026-06-25T14:30:00Z', initials: 'RS' }, + { id: 'c2', name: 'Priya Patel', phone: '9876543211', email: 'priya@example.com', dob: '1985-08-20', loyaltyPoints: 1200, tier: 'platinum', totalSpent: 45000, storeCredit: 500, lastVisit: '2026-06-26T09:15:00Z', initials: 'PP' }, + { id: 'c3', name: 'Amit Kumar', phone: '9876543212', email: 'amit@example.com', dob: '1992-11-10', loyaltyPoints: 150, tier: 'silver', totalSpent: 3500, storeCredit: 0, lastVisit: '2026-06-20T11:45:00Z', initials: 'AK' }, + { id: 'c4', name: 'Sneha Gupta', phone: '9876543213', email: 'sneha@example.com', dob: '1988-02-25', loyaltyPoints: 800, tier: 'gold', totalSpent: 22000, storeCredit: 150, lastVisit: '2026-06-24T16:20:00Z', initials: 'SG' }, + { id: 'c5', name: 'Vikram Singh', phone: '9876543214', email: 'vikram@example.com', dob: '1975-07-08', loyaltyPoints: 2100, tier: 'platinum', totalSpent: 85000, storeCredit: 0, lastVisit: '2026-06-26T10:05:00Z', initials: 'VS' }, + { id: 'c6', name: 'Neha Reddy', phone: '9876543215', email: 'neha@example.com', dob: '1995-04-12', loyaltyPoints: 50, tier: 'silver', totalSpent: 1200, storeCredit: 0, lastVisit: '2026-06-15T18:30:00Z', initials: 'NR' }, + { id: 'c7', name: 'Sanjay Verma', phone: '9876543216', email: 'sanjay@example.com', dob: '1982-09-30', loyaltyPoints: 320, tier: 'silver', totalSpent: 8900, storeCredit: 0, lastVisit: '2026-06-22T13:10:00Z', initials: 'SV' }, + { id: 'c8', name: 'Pooja Iyer', phone: '9876543217', email: 'pooja@example.com', dob: '1991-12-05', loyaltyPoints: 650, tier: 'gold', totalSpent: 18500, storeCredit: 0, lastVisit: '2026-06-25T19:45:00Z', initials: 'PI' }, +]; diff --git a/src/data/dashboard.ts b/src/data/dashboard.ts new file mode 100644 index 0000000..4046efe --- /dev/null +++ b/src/data/dashboard.ts @@ -0,0 +1,33 @@ +import { DashboardStats } from '../types'; + +export const dashboardData: DashboardStats = { + todayStats: { + sales: 24870, + transactions: 148, + avgBasket: 168, + lowStockCount: 3, + }, + weeklySales: [ + { day: 'Mon', amount: 18500 }, + { day: 'Tue', amount: 21200 }, + { day: 'Wed', amount: 19800 }, + { day: 'Thu', amount: 20500 }, + { day: 'Fri', amount: 25600 }, + { day: 'Sat', amount: 32000 }, + { day: 'Sun', amount: 29400 }, + ], + topProducts: [ + { rank: 1, name: 'Amul Milk 1L', qty: 45, revenue: 2790, trend: 12 }, + { rank: 2, name: 'Maggi 2-min', qty: 38, revenue: 532, trend: 5 }, + { rank: 3, name: 'Coca-Cola 600ml', qty: 30, revenue: 1200, trend: -2 }, + { rank: 4, name: 'Parle-G 800g', qty: 25, revenue: 1000, trend: 8 }, + { rank: 5, name: 'Lay\'s Chips 26g', qty: 22, revenue: 440, trend: -1 }, + ], + recentActivity: [ + { id: 'a1', type: 'sale', text: 'Sale completed: ₹304.00 (UPI)', time: '2 mins ago', color: 'green' }, + { id: 'a2', type: 'alert', text: 'Low stock alert: Basmati Rice 1kg (4 left)', time: '15 mins ago', color: 'amber' }, + { id: 'a3', type: 'po', text: 'Purchase Order #PO-001 received', time: '1 hour ago', color: 'blue' }, + { id: 'a4', type: 'sale', text: 'Sale completed: ₹1,250.00 (Card)', time: '2 hours ago', color: 'green' }, + { id: 'a5', type: 'refund', text: 'Refund processed: ₹145.00 (Fortune Oil)', time: '3 hours ago', color: 'red' }, + ], +}; diff --git a/src/data/products.ts b/src/data/products.ts new file mode 100644 index 0000000..6f77f8d --- /dev/null +++ b/src/data/products.ts @@ -0,0 +1,34 @@ +import { Product } from '../types'; + +export const products: Product[] = [ + // Dairy + { id: 'p1', name: 'Amul Milk 1L', sku: 'DAI-001', barcode: '10001', price: 62, costPrice: 50, taxRate: 18, categoryId: 'dairy', categoryName: 'Dairy', unit: 'L', emoji: '🥛', stock: 50, reorderPoint: 10, status: 'in_stock' }, + { id: 'p2', name: 'Amul Butter 500g', sku: 'DAI-002', barcode: '10002', price: 245, costPrice: 200, taxRate: 18, categoryId: 'dairy', categoryName: 'Dairy', unit: 'g', emoji: '🧈', stock: 30, reorderPoint: 5, status: 'in_stock' }, + { id: 'p3', name: 'Curd 400g', sku: 'DAI-003', barcode: '10003', price: 48, costPrice: 38, taxRate: 18, categoryId: 'dairy', categoryName: 'Dairy', unit: 'g', emoji: '🥣', stock: 40, reorderPoint: 10, status: 'in_stock' }, + { id: 'p4', name: 'Paneer 200g', sku: 'DAI-004', barcode: '10004', price: 90, costPrice: 70, taxRate: 18, categoryId: 'dairy', categoryName: 'Dairy', unit: 'g', emoji: '🧀', stock: 20, reorderPoint: 5, status: 'in_stock' }, + // Grocery + { id: 'p5', name: 'Basmati Rice 1kg', sku: 'GRO-001', barcode: '20001', price: 180, costPrice: 150, taxRate: 18, categoryId: 'grocery', categoryName: 'Grocery', unit: 'kg', emoji: '🍚', stock: 4, reorderPoint: 10, status: 'low_stock' }, + { id: 'p6', name: 'Fortune Oil 1L', sku: 'GRO-002', barcode: '20002', price: 145, costPrice: 120, taxRate: 18, categoryId: 'grocery', categoryName: 'Grocery', unit: 'L', emoji: '🛢️', stock: 60, reorderPoint: 15, status: 'in_stock' }, + { id: 'p7', name: 'Toor Dal 500g', sku: 'GRO-003', barcode: '20003', price: 90, costPrice: 75, taxRate: 18, categoryId: 'grocery', categoryName: 'Grocery', unit: 'g', emoji: '🫘', stock: 45, reorderPoint: 10, status: 'in_stock' }, + { id: 'p8', name: 'Maggi 2-min', sku: 'GRO-004', barcode: '20004', price: 14, costPrice: 10, taxRate: 18, categoryId: 'grocery', categoryName: 'Grocery', unit: 'pc', emoji: '🍜', stock: 120, reorderPoint: 20, status: 'in_stock' }, + // Beverages + { id: 'p9', name: 'Coca-Cola 600ml', sku: 'BEV-001', barcode: '30001', price: 40, costPrice: 30, taxRate: 18, categoryId: 'beverages', categoryName: 'Beverages', unit: 'ml', emoji: '🥤', stock: 80, reorderPoint: 20, status: 'in_stock' }, + { id: 'p10', name: 'Frooti 250ml', sku: 'BEV-002', barcode: '30002', price: 15, costPrice: 10, taxRate: 18, categoryId: 'beverages', categoryName: 'Beverages', unit: 'ml', emoji: '🥭', stock: 100, reorderPoint: 25, status: 'in_stock' }, + { id: 'p11', name: 'Bisleri 1L', sku: 'BEV-003', barcode: '30003', price: 20, costPrice: 12, taxRate: 18, categoryId: 'beverages', categoryName: 'Beverages', unit: 'L', emoji: '💧', stock: 150, reorderPoint: 30, status: 'in_stock' }, + { id: 'p12', name: 'Red Bull 250ml', sku: 'BEV-004', barcode: '30004', price: 125, costPrice: 100, taxRate: 18, categoryId: 'beverages', categoryName: 'Beverages', unit: 'ml', emoji: '🔋', stock: 40, reorderPoint: 10, status: 'in_stock' }, + // Snacks + { id: 'p13', name: 'Parle-G 800g', sku: 'SNA-001', barcode: '40001', price: 40, costPrice: 30, taxRate: 18, categoryId: 'snacks', categoryName: 'Snacks', unit: 'g', emoji: '🍪', stock: 90, reorderPoint: 20, status: 'in_stock' }, + { id: 'p14', name: 'Lay\'s Chips 26g', sku: 'SNA-002', barcode: '40002', price: 20, costPrice: 15, taxRate: 18, categoryId: 'snacks', categoryName: 'Snacks', unit: 'g', emoji: '🥔', stock: 110, reorderPoint: 20, status: 'in_stock' }, + { id: 'p15', name: 'KitKat 50g', sku: 'SNA-003', barcode: '40003', price: 50, costPrice: 38, taxRate: 18, categoryId: 'snacks', categoryName: 'Snacks', unit: 'g', emoji: '🍫', stock: 75, reorderPoint: 15, status: 'in_stock' }, + { id: 'p16', name: 'Hide&Seek', sku: 'SNA-004', barcode: '40004', price: 30, costPrice: 22, taxRate: 18, categoryId: 'snacks', categoryName: 'Snacks', unit: 'pc', emoji: '🍪', stock: 65, reorderPoint: 15, status: 'in_stock' }, + // Personal Care + { id: 'p17', name: 'Colgate 200g', sku: 'PER-001', barcode: '50001', price: 99, costPrice: 80, taxRate: 18, categoryId: 'personal', categoryName: 'Personal Care', unit: 'g', emoji: '🪥', stock: 0, reorderPoint: 10, status: 'out_of_stock' }, + { id: 'p18', name: 'Dove Soap 75g', sku: 'PER-002', barcode: '50002', price: 60, costPrice: 45, taxRate: 18, categoryId: 'personal', categoryName: 'Personal Care', unit: 'g', emoji: '🧼', stock: 55, reorderPoint: 10, status: 'in_stock' }, + { id: 'p19', name: 'Head & Shoulders 180ml', sku: 'PER-003', barcode: '50003', price: 199, costPrice: 160, taxRate: 18, categoryId: 'personal', categoryName: 'Personal Care', unit: 'ml', emoji: '🧴', stock: 35, reorderPoint: 8, status: 'in_stock' }, + { id: 'p20', name: 'Dettol 200ml', sku: 'PER-004', barcode: '50004', price: 80, costPrice: 65, taxRate: 18, categoryId: 'personal', categoryName: 'Personal Care', unit: 'ml', emoji: '🧴', stock: 45, reorderPoint: 10, status: 'in_stock' }, + // Household + { id: 'p21', name: 'Surf Excel 1kg', sku: 'HOU-001', barcode: '60001', price: 185, costPrice: 150, taxRate: 18, categoryId: 'household', categoryName: 'Household', unit: 'kg', emoji: '👕', stock: 25, reorderPoint: 5, status: 'in_stock' }, + { id: 'p22', name: 'Vim Bar 200g', sku: 'HOU-002', barcode: '60002', price: 35, costPrice: 25, taxRate: 18, categoryId: 'household', categoryName: 'Household', unit: 'g', emoji: '🧽', stock: 85, reorderPoint: 15, status: 'in_stock' }, + { id: 'p23', name: 'Harpic 500ml', sku: 'HOU-003', barcode: '60003', price: 130, costPrice: 100, taxRate: 18, categoryId: 'household', categoryName: 'Household', unit: 'ml', emoji: '🚽', stock: 30, reorderPoint: 8, status: 'in_stock' }, + { id: 'p24', name: 'Lizol 500ml', sku: 'HOU-004', barcode: '60004', price: 120, costPrice: 95, taxRate: 18, categoryId: 'household', categoryName: 'Household', unit: 'ml', emoji: '🧹', stock: 40, reorderPoint: 10, status: 'in_stock' }, +]; diff --git a/src/data/promotions.ts b/src/data/promotions.ts new file mode 100644 index 0000000..6195e3c --- /dev/null +++ b/src/data/promotions.ts @@ -0,0 +1,9 @@ +import { Promotion } from '../types'; + +export const promotions: Promotion[] = [ + { id: 'promo1', name: 'Weekend Sale 10%', type: 'percentage', value: 10, description: 'Get 10% off on all items this weekend', applyTo: 'All', startDate: '2026-06-25', endDate: '2026-06-28', status: 'active', usageCount: 45, icon: '🏷️' }, + { id: 'promo2', name: 'Buy 2 Get 1 Free on Snacks', type: 'buy_x_get_y', value: 1, description: 'Buy 2 snacks, get 1 free', applyTo: 'Snacks', startDate: '2026-06-20', endDate: '2026-06-30', status: 'active', usageCount: 112, icon: '🎁' }, + { id: 'promo3', name: 'Birthday Special', type: 'fixed', value: 100, description: '₹100 off on your birthday month', applyTo: 'All', startDate: '2026-01-01', endDate: '2026-12-31', status: 'active', usageCount: 8, icon: '🎂' }, + { id: 'promo4', name: 'Diwali Dhamaka', type: 'percentage', value: 25, description: '25% off on all items for Diwali', applyTo: 'All', startDate: '2026-10-20', endDate: '2026-11-05', status: 'scheduled', usageCount: 0, icon: '🪔' }, + { id: 'promo5', name: 'Summer Drinks 15%', type: 'percentage', value: 15, description: '15% off on all beverages', applyTo: 'Beverages', startDate: '2026-04-01', endDate: '2026-05-31', status: 'expired', usageCount: 345, icon: '🥤' }, +]; diff --git a/src/data/sales.ts b/src/data/sales.ts new file mode 100644 index 0000000..cb7b13c --- /dev/null +++ b/src/data/sales.ts @@ -0,0 +1,24 @@ +import { Sale } from '../types'; + +export const sales: Sale[] = Array.from({ length: 30 }).map((_, i) => { + const isToday = i < 15; + const dateStr = isToday + ? `2026-06-26T${10 + Math.floor(i / 3)}:${(i * 15) % 60}:00Z` + : `2026-06-25T${10 + Math.floor(i / 3)}:${(i * 15) % 60}:00Z`; + + return { + id: `INV-2026-${1000 + i}`, + date: dateStr, + cashier: i % 3 === 0 ? 'Prabhakaran' : 'Abhishek', + customerId: i % 4 === 0 ? `c${(i % 8) + 1}` : undefined, + items: [ + { productId: 'p1', name: 'Amul Milk 1L', qty: 2, unitPrice: 62 }, + { productId: 'p5', name: 'Basmati Rice 1kg', qty: 1, unitPrice: 180 }, + ], + subtotal: 304, + taxAmount: 0, + discountAmount: 0, + total: 304, + paymentMethod: i % 3 === 0 ? 'cash' : i % 2 === 0 ? 'upi' : 'card', + }; +}); diff --git a/src/data/staff.ts b/src/data/staff.ts new file mode 100644 index 0000000..dd5239b --- /dev/null +++ b/src/data/staff.ts @@ -0,0 +1,16 @@ +import { User } from '@/types'; + +export const staff: User[] = [ + { + id: 'EMP-001', + name: 'Abhishek', + role: 'cashier', + pin: '1111' + }, + { + id: 'EMP-002', + name: 'Suriya', + role: 'manager', + pin: '9999' + } +]; diff --git a/src/data/suppliers.ts b/src/data/suppliers.ts new file mode 100644 index 0000000..a94357b --- /dev/null +++ b/src/data/suppliers.ts @@ -0,0 +1,45 @@ +import { Supplier, PurchaseOrder } from '../types'; + +export const suppliers: Supplier[] = [ + { id: 's1', name: 'Amul Distributors', category: 'Dairy', phone: '080-1234567', email: 'orders@amul.local', paymentTerms: 'Net 15' }, + { id: 's2', name: 'ITC Wholesale', category: 'Grocery', phone: '080-2345678', email: 'sales@itc.local', paymentTerms: 'Net 30' }, + { id: 's3', name: 'Coca-Cola India', category: 'Beverages', phone: '080-3456789', email: 'dist@cocacola.local', paymentTerms: 'Net 7' }, + { id: 's4', name: 'Parle Products', category: 'Snacks', phone: '080-4567890', email: 'supply@parle.local', paymentTerms: 'Net 30' }, + { id: 's5', name: 'HUL Supply', category: 'Personal Care', phone: '080-5678901', email: 'orders@hul.local', paymentTerms: 'Net 30' }, + { id: 's6', name: 'Reckitt Benckiser', category: 'Household', phone: '080-6789012', email: 'dist@rb.local', paymentTerms: 'Net 15' }, +]; + +export const purchaseOrders: PurchaseOrder[] = [ + { + id: 'PO-001', supplierId: 's1', supplierName: 'Amul Distributors', value: 12500, status: 'received', orderedAt: '2026-06-20T10:00:00Z', + items: [{ productId: 'p1', name: 'Amul Milk 1L', qtyOrdered: 100, qtyReceived: 100, unitCost: 50, total: 5000 }] + }, + { + id: 'PO-002', supplierId: 's2', supplierName: 'ITC Wholesale', value: 8400, status: 'in_transit', orderedAt: '2026-06-25T11:30:00Z', + items: [{ productId: 'p5', name: 'Basmati Rice 1kg', qtyOrdered: 50, qtyReceived: 0, unitCost: 150, total: 7500 }] + }, + { + id: 'PO-003', supplierId: 's5', supplierName: 'HUL Supply', value: 4500, status: 'pending', orderedAt: '2026-06-26T09:15:00Z', + items: [{ productId: 'p18', name: 'Dove Soap 75g', qtyOrdered: 100, qtyReceived: 0, unitCost: 45, total: 4500 }] + }, + { + id: 'PO-004', supplierId: 's1', supplierName: 'Amul Distributors', value: 3500, status: 'pending', orderedAt: '2026-06-26T14:00:00Z', + items: [{ productId: 'p4', name: 'Paneer 200g', qtyOrdered: 50, qtyReceived: 0, unitCost: 70, total: 3500 }] + }, + { + id: 'PO-005', supplierId: 's3', supplierName: 'Coca-Cola India', value: 6000, status: 'received', orderedAt: '2026-06-22T10:00:00Z', + items: [{ productId: 'p9', name: 'Coca-Cola 600ml', qtyOrdered: 200, qtyReceived: 200, unitCost: 30, total: 6000 }] + }, + { + id: 'PO-006', supplierId: 's4', supplierName: 'Parle Products', value: 2400, status: 'in_transit', orderedAt: '2026-06-24T16:45:00Z', + items: [{ productId: 'p13', name: 'Parle-G 800g', qtyOrdered: 80, qtyReceived: 0, unitCost: 30, total: 2400 }] + }, + { + id: 'PO-007', supplierId: 's6', supplierName: 'Reckitt Benckiser', value: 5000, status: 'received', orderedAt: '2026-06-18T09:00:00Z', + items: [{ productId: 'p23', name: 'Harpic 500ml', qtyOrdered: 50, qtyReceived: 50, unitCost: 100, total: 5000 }] + }, + { + id: 'PO-008', supplierId: 's5', supplierName: 'HUL Supply', value: 16000, status: 'in_transit', orderedAt: '2026-06-23T11:20:00Z', + items: [{ productId: 'p19', name: 'Head & Shoulders 180ml', qtyOrdered: 100, qtyReceived: 0, unitCost: 160, total: 16000 }] + }, +]; diff --git a/src/hooks/useBarcodeScanner.ts b/src/hooks/useBarcodeScanner.ts new file mode 100644 index 0000000..8180fa4 --- /dev/null +++ b/src/hooks/useBarcodeScanner.ts @@ -0,0 +1,61 @@ +import { useEffect, useRef } from 'react'; + +interface UseBarcodeScannerOptions { + onScan: (barcode: string) => void; + ignoreIfFocused?: boolean; + timeout?: number; +} + +export function useBarcodeScanner({ + onScan, + ignoreIfFocused = true, + timeout = 50 +}: UseBarcodeScannerOptions) { + const bufferRef = useRef(''); + const lastKeyTimeRef = useRef(Date.now()); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if user is manually typing in an input/textarea + if (ignoreIfFocused) { + const activeTag = document.activeElement?.tagName; + const activeType = (document.activeElement as HTMLInputElement)?.type; + + if (activeTag === 'INPUT' || activeTag === 'TEXTAREA') { + // Allow exceptions for certain input types if needed in future + if (activeType !== 'checkbox' && activeType !== 'radio') { + return; + } + } + } + + const currentTime = Date.now(); + const timeDiff = currentTime - lastKeyTimeRef.current; + + // If more than 'timeout' ms passed between keys, it's probably human typing. + // Reset the buffer. + if (timeDiff > timeout) { + bufferRef.current = ''; + } + + // Scanner usually concludes with an Enter key + if (e.key === 'Enter') { + if (bufferRef.current.length > 3) { + // Prevent form submissions if this was a global scan + e.preventDefault(); + onScan(bufferRef.current); + bufferRef.current = ''; + } + } + // Only capture single printable characters + else if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { + bufferRef.current += e.key; + } + + lastKeyTimeRef.current = currentTime; + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onScan, ignoreIfFocused, timeout]); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..158d99e --- /dev/null +++ b/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body, #root { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #f3f4f6; /* gray-100 */ +} + +/* Touch optimizations */ +.touch-manipulation { + touch-action: manipulation; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..21939de --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,40 @@ +export function formatCurrency(amount: number): string { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + }).format(amount); +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + }).format(date); +} + +export function getStatusBadge(status: string): { label: string; className: string } { + switch (status.toLowerCase()) { + case 'in_stock': + case 'received': + case 'active': + case 'ok': + return { label: status.replace('_', ' ').toUpperCase(), className: 'bg-green-100 text-green-700' }; + case 'low_stock': + case 'pending': + case 'scheduled': + case 'low': + return { label: status.replace('_', ' ').toUpperCase(), className: 'bg-amber-100 text-amber-700' }; + case 'out_of_stock': + case 'oos': + case 'cancelled': + return { label: status.replace('_', ' ').toUpperCase(), className: 'bg-red-100 text-red-700' }; + case 'in_transit': + case 'loyalty': + return { label: status.replace('_', ' ').toUpperCase(), className: 'bg-blue-100 text-blue-700' }; + case 'expired': + default: + return { label: status.replace('_', ' ').toUpperCase(), className: 'bg-gray-100 text-gray-700' }; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..2bd5e46 --- /dev/null +++ b/src/pages/auth/LoginPage.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuthStore } from '@/stores/authStore'; +import { Delete, Lock } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function LoginPage() { + const [pin, setPin] = useState(''); + const { login, currentUser } = useAuthStore(); + const navigate = useNavigate(); + + useEffect(() => { + if (currentUser) { + navigate('/pos', { replace: true }); + } + }, [currentUser, navigate]); + + const handleKeyPress = (key: string) => { + if (pin.length < 4) { + setPin(prev => prev + key); + } + }; + + const handleBackspace = () => { + setPin(prev => prev.slice(0, -1)); + }; + + const handleLogin = () => { + if (pin.length !== 4) return; + + const success = login(pin); + if (success) { + toast.success('Login successful'); + } else { + toast.error('Invalid PIN'); + setPin(''); + } + }; + + // Auto-submit when 4 digits are reached + useEffect(() => { + if (pin.length === 4) { + // Small timeout to let the user see the 4th dot fill up + const timer = setTimeout(() => { + handleLogin(); + }, 150); + return () => clearTimeout(timer); + } + }, [pin]); + + const keypad = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '⌫']; + + return ( +
+
+ +
+ +
+ +

Terminal Locked

+

+ Enter your 4-digit staff PIN to unlock the register. +

+ + {/* PIN Dots */} +
+ {[0, 1, 2, 3].map(i => ( +
+ ))} +
+ + {/* Keypad */} +
+ {keypad.map((key) => { + if (key === 'C') { + return ( + + ); + } + if (key === '⌫') { + return ( + + ); + } + return ( + + ); + })} +
+ +
+

Cashier PIN: 1111 | Manager PIN: 9999

+
+ +
+
+ ); +} diff --git a/src/pages/customers/AddCustomerModal.tsx b/src/pages/customers/AddCustomerModal.tsx new file mode 100644 index 0000000..bdf8096 --- /dev/null +++ b/src/pages/customers/AddCustomerModal.tsx @@ -0,0 +1,120 @@ +import { useState, useEffect } from 'react'; +import { Modal, Button, Input } from '@/components/ui'; +import { Customer } from '@/types'; +import toast from 'react-hot-toast'; + +interface AddCustomerModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (customer: Customer) => void; +} + +export default function AddCustomerModal({ isOpen, onClose, onSave }: AddCustomerModalProps) { + const [formData, setFormData] = useState({ + name: '', + phone: '', + email: '', + dob: '', + notes: '', + }); + + const [errors, setErrors] = useState>({}); + + useEffect(() => { + if (isOpen) { + setFormData({ + name: '', + phone: '', + email: '', + dob: '', + notes: '', + }); + setErrors({}); + } + }, [isOpen]); + + const handleChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })); + } + }; + + const handleSave = () => { + const newErrors: Record = {}; + if (!formData.name.trim()) newErrors.name = 'Name is required'; + if (!formData.phone.trim()) newErrors.phone = 'Phone is required'; + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + + const initials = formData.name.split(' ').map(n => n[0]).join('').toUpperCase().substring(0, 2); + + const newCustomer: Customer = { + id: `c${Date.now()}`, + name: formData.name, + phone: formData.phone, + email: formData.email, + dob: formData.dob, + loyaltyPoints: 0, + tier: 'silver', + totalSpent: 0, + storeCredit: 0, + lastVisit: new Date().toISOString(), + initials, + }; + + onSave(newCustomer); + toast.success('Customer saved successfully!'); + onClose(); + }; + + return ( + +
+ handleChange('name', e.target.value)} + error={errors.name} + /> +
+ handleChange('phone', e.target.value)} + error={errors.phone} + /> + handleChange('email', e.target.value)} + /> +
+ handleChange('dob', e.target.value)} + /> + handleChange('notes', e.target.value)} + /> +
+ +
+ + +
+
+ ); +} diff --git a/src/pages/customers/CustomersPage.tsx b/src/pages/customers/CustomersPage.tsx new file mode 100644 index 0000000..18e4489 --- /dev/null +++ b/src/pages/customers/CustomersPage.tsx @@ -0,0 +1,273 @@ +import { useState, useMemo } from 'react'; +import { SearchBar, Button, Card, Table, Badge } from '@/components/ui'; +import { customers as initialCustomers } from '@/data/customers'; +import { sales } from '@/data/sales'; +import { Customer } from '@/types'; +import { formatCurrency } from '@/lib/utils'; +import AddCustomerModal from './AddCustomerModal'; +import { Users, Phone, Mail, Calendar, Gift, Star, Award, CreditCard, UserPlus } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function CustomersPage() { + const [customersList, setCustomersList] = useState(initialCustomers); + const [searchQuery, setSearchQuery] = useState(''); + const [activeTier, setActiveTier] = useState('all'); + const [selectedCustomerId, setSelectedCustomerId] = useState(null); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + + const selectedCustomer = useMemo(() => + customersList.find(c => c.id === selectedCustomerId) || null + , [customersList, selectedCustomerId]); + + const customerSales = useMemo(() => + selectedCustomerId ? sales.filter(s => s.customerId === selectedCustomerId).slice(0, 5) : [] + , [selectedCustomerId]); + + const filteredCustomers = useMemo(() => { + let result = customersList; + if (searchQuery) { + const q = searchQuery.toLowerCase(); + result = result.filter(c => + c.name.toLowerCase().includes(q) || + c.phone.includes(q) || + c.email?.toLowerCase().includes(q) + ); + } + if (activeTier !== 'all') { + result = result.filter(c => c.tier === activeTier); + } + return result; + }, [customersList, searchQuery, activeTier]); + + const handleAddCustomer = (newCustomer: Customer) => { + setCustomersList([newCustomer, ...customersList]); + setSelectedCustomerId(newCustomer.id); + }; + + const getTierColor = (tier: string) => { + if (tier === 'platinum') return 'bg-blue-100 text-blue-800 border-blue-200'; + if (tier === 'gold') return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + return 'bg-gray-100 text-gray-800 border-gray-200'; // silver + }; + + const getTierIcon = (tier: string) => { + if (tier === 'platinum') return '🥇'; + if (tier === 'gold') return '🥈'; + return '🥉'; + }; + + const calculateProgress = (points: number, tier: string) => { + let nextThreshold = 500; // silver -> gold + let currentBase = 0; + + if (tier === 'gold') { + nextThreshold = 2000; + currentBase = 500; + } else if (tier === 'platinum') { + return 100; // maxed out + } + + const progress = ((points - currentBase) / (nextThreshold - currentBase)) * 100; + return Math.min(Math.max(progress, 0), 100); + }; + + return ( +
+ + {/* LEFT PANEL - Customer List */} +
+
+
+

Customers

+ +
+ + setSearchQuery(e.target.value)} + /> + +
+ {['all', 'silver', 'gold', 'platinum'].map(tier => ( + + ))} +
+
+ +
+ {filteredCustomers.map(customer => { + const isSelected = selectedCustomerId === customer.id; + return ( +
setSelectedCustomerId(customer.id)} + className={`p-4 rounded-xl border cursor-pointer transition-all ${ + isSelected + ? 'border-primary bg-primary/5 shadow-sm' + : 'border-gray-100 ' + }`} + > +
+
+ {customer.initials} +
+
+
+

{customer.name}

+ + {customer.tier} + +
+

{customer.phone} {customer.email && `· ${customer.email}`}

+ +
+
+ Total spent: {formatCurrency(customer.totalSpent)} +
+
+ {customer.loyaltyPoints} pts +
+
+
+
+
+ ) + })} + {filteredCustomers.length === 0 && ( +
+

No customers found.

+
+ )} +
+
+ + {/* RIGHT PANEL - Profile Detail */} +
+ {selectedCustomer ? ( +
+
+ + {/* Header Profile */} +
+
+
+ {selectedCustomer.initials} +
+
+
+

{selectedCustomer.name}

+ + {getTierIcon(selectedCustomer.tier)} {selectedCustomer.tier} Member + +
+
+ {selectedCustomer.phone} + {selectedCustomer.email && {selectedCustomer.email}} + {selectedCustomer.dob && {selectedCustomer.dob}} +
+
+
+ +
+ + {/* Stats & Loyalty */} +
+ +
+ Total Spent +
+
{formatCurrency(selectedCustomer.totalSpent)}
+
+ +
+ Loyalty Points +
+
{selectedCustomer.loyaltyPoints} pts
+
+ +
+ Store Credit +
+
{formatCurrency(selectedCustomer.storeCredit)}
+
+
+ + {/* Loyalty Progress */} + 0 && ( + + ) + }> +
+
+ {selectedCustomer.tier.toUpperCase()} TIER + {selectedCustomer.tier === 'platinum' ? 'MAX TIER' : `Next Tier`} +
+
+
+
+

+ {selectedCustomer.tier === 'platinum' + ? 'Customer has reached the highest loyalty tier.' + : `Earn ${selectedCustomer.tier === 'silver' ? 500 - selectedCustomer.loyaltyPoints : 2000 - selectedCustomer.loyaltyPoints} more points to reach ${selectedCustomer.tier === 'silver' ? 'Gold' : 'Platinum'}!`} +

+
+ + + {/* Purchase History */} + + {customerSales.length > 0 ? ( + new Date(s.date).toLocaleDateString() }, + { key: 'items', label: 'Items', render: s => s.items.map(i => i.name).join(', ') }, + { key: 'amount', label: 'Amount', render: s => {formatCurrency(s.total)} }, + { key: 'payment', label: 'Payment', render: s => {s.paymentMethod} }, + { key: 'action', label: '', render: () => }, + ]} + /> + ) : ( +
No purchase history found for this customer.
+ )} + + + + + ) : ( +
+
+ +
+

Customer Profiles

+

Select a customer from the list on the left to view their detailed profile, loyalty progress, and purchase history.

+
+ )} + + + setIsAddModalOpen(false)} + onSave={handleAddCustomer} + /> + + ); +} diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..fce65f6 --- /dev/null +++ b/src/pages/dashboard/DashboardPage.tsx @@ -0,0 +1,165 @@ +import { useNavigate } from 'react-router-dom'; +import { StatCard, Card, Table, Badge } from '@/components/ui'; +import { dashboardData } from '@/data/dashboard'; +import { formatCurrency } from '@/lib/utils'; +import { DollarSign, Receipt, ShoppingBag, AlertCircle, Download, ShoppingCart, Archive, FileText, Truck } from 'lucide-react'; + +export default function DashboardPage() { + const navigate = useNavigate(); + const { todayStats, weeklySales, topProducts, recentActivity } = dashboardData; + + const maxSale = Math.max(...weeklySales.map(d => d.amount)); + + return ( +
+ + {/* KPI ROW */} +
+ } + iconBg="bg-green-100 text-green-700" + /> + } + iconBg="bg-blue-100 text-blue-700" + /> + } + iconBg="bg-amber-100 text-amber-700" + /> +
navigate('/inventory')} className="cursor-pointer active:scale-[0.98] transition-transform select-none touch-manipulation"> + } + iconBg="bg-red-100 text-red-700" + /> +
+
+ + {/* 2-COLUMN GRID */} +
+ + {/* LEFT COLUMN (2fr) */} +
+ + + Export + + } + > +
+ {weeklySales.map((day, idx) => { + const isToday = idx === 3; // Mocking Thursday as today + const heightPct = (day.amount / maxSale) * 100; + return ( +
+
+ {formatCurrency(day.amount)} +
+
+
+ {day.day} +
+
+ ); + })} +
+ + + +
({ ...p, id: p.rank }))} + columns={[ + { key: 'rank', label: 'Rank', render: (item) => #{item.rank} }, + { key: 'name', label: 'Product', render: (item) => {item.name} }, + { key: 'qty', label: 'Qty Sold' }, + { key: 'revenue', label: 'Revenue', render: (item) => {formatCurrency(item.revenue)} }, + { + key: 'trend', + label: 'Trend', + render: (item) => ( + 0 ? 'green' : 'red'}> + {item.trend > 0 ? '▲' : '▼'} {Math.abs(item.trend)}% + + ) + }, + ]} + /> + + + + {/* RIGHT COLUMN (1fr) */} +
+ +
+ + + + +
+
+ + +
+ {recentActivity.map((activity, index) => { + const colorMap: Record = { + green: 'bg-green-500 ring-green-100', + amber: 'bg-amber-500 ring-amber-100', + blue: 'bg-blue-500 ring-blue-100', + red: 'bg-red-500 ring-red-100', + }; + return ( +
+ {/* Timeline line */} + {index !== recentActivity.length - 1 && ( +
+ )} +
+
+

{activity.text}

+ {activity.time} +
+
+ ); + })} +
+ +
+ +
+
+ ); +} diff --git a/src/pages/inventory/AdjustStockModal.tsx b/src/pages/inventory/AdjustStockModal.tsx new file mode 100644 index 0000000..4368e65 --- /dev/null +++ b/src/pages/inventory/AdjustStockModal.tsx @@ -0,0 +1,88 @@ +import { useState, useEffect } from 'react'; +import { Modal, Button, Input, Select } from '@/components/ui'; +import { Product } from '@/types'; +import toast from 'react-hot-toast'; + +interface AdjustStockModalProps { + isOpen: boolean; + onClose: () => void; + product: Product | null; + onConfirm: (productId: string, qty: number, type: string) => void; +} + +export default function AdjustStockModal({ isOpen, onClose, product, onConfirm }: AdjustStockModalProps) { + const [type, setType] = useState('add'); + const [qty, setQty] = useState(''); + const [reason, setReason] = useState(''); + + useEffect(() => { + if (isOpen) { + setType('add'); + setQty(''); + setReason(''); + } + }, [isOpen]); + + if (!product) return null; + + const handleConfirm = () => { + if (!qty || Number(qty) <= 0) { + toast.error('Please enter a valid quantity'); + return; + } + + // Calculate new total just to check validity + const numQty = Number(qty); + if ((type === 'remove' || type === 'damage' || type === 'write_off') && numQty > product.stock) { + toast.error(`Cannot remove ${numQty}. Only ${product.stock} in stock.`); + return; + } + + onConfirm(product.id, numQty, type); + toast.success(`Stock adjusted successfully`); + onClose(); + }; + + return ( + +
+
+ Current Stock On Hand + {product.stock} {product.unit} +
+ + setQty(e.target.value ? Number(e.target.value) : '')} + /> + + setReason(e.target.value)} + /> +
+ +
+ + +
+
+ ); +} diff --git a/src/pages/inventory/InventoryPage.tsx b/src/pages/inventory/InventoryPage.tsx new file mode 100644 index 0000000..d663161 --- /dev/null +++ b/src/pages/inventory/InventoryPage.tsx @@ -0,0 +1,304 @@ +import { useState, useMemo } from 'react'; +import { StatCard, Card, Tabs, Table, Badge, Button, SearchBar, Select, Input } from '@/components/ui'; +import { products as initialProducts } from '@/data/products'; +import { categories } from '@/data/categories'; +import { formatCurrency } from '@/lib/utils'; +import { Product } from '@/types'; +import AdjustStockModal from './AdjustStockModal'; +import { Archive, AlertTriangle, XCircle, DollarSign, PackageCheck, ClipboardList } from 'lucide-react'; +import toast from 'react-hot-toast'; + +// Mock movement history +const mockHistory = [ + { id: '1', date: '2026-06-26 10:30', product: 'Amul Milk 1L', type: 'Add Stock', qtyChange: 50, reason: 'PO-2034', user: 'Admin', balance: 100 }, + { id: '2', date: '2026-06-26 09:15', product: 'Lay\'s Chips 26g', type: 'Damage', qtyChange: -2, reason: 'Crushed packet', user: 'Cashier 1', balance: 108 }, + { id: '3', date: '2026-06-25 18:45', product: 'Colgate 200g', type: 'Remove Stock', qtyChange: -1, reason: 'Expired', user: 'Admin', balance: 0 }, + { id: '4', date: '2026-06-25 14:20', product: 'Red Bull 250ml', type: 'Add Stock', qtyChange: 24, reason: 'PO-2033', user: 'Admin', balance: 40 }, + { id: '5', date: '2026-06-25 11:10', product: 'Frooti 250ml', type: 'Sale', qtyChange: -2, reason: 'POS Sale', user: 'Cashier 2', balance: 100 }, + { id: '6', date: '2026-06-24 16:30', product: 'Toor Dal 500g', type: 'Sale', qtyChange: -5, reason: 'POS Sale', user: 'Cashier 1', balance: 45 }, + { id: '7', date: '2026-06-24 09:00', product: 'Amul Butter 500g', type: 'Add Stock', qtyChange: 10, reason: 'PO-2032', user: 'Admin', balance: 30 }, + { id: '8', date: '2026-06-23 15:45', product: 'Vim Bar 200g', type: 'Sale', qtyChange: -1, reason: 'POS Sale', user: 'Cashier 2', balance: 85 }, + { id: '9', date: '2026-06-23 10:20', product: 'Dove Soap 75g', type: 'Sale', qtyChange: -3, reason: 'POS Sale', user: 'Cashier 1', balance: 55 }, + { id: '10', date: '2026-06-22 12:00', product: 'Maggi 2-min', type: 'Write-off', qtyChange: -10, reason: 'Pest damage', user: 'Admin', balance: 120 }, +]; + +export default function InventoryPage() { + const [activeTab, setActiveTab] = useState('levels'); + const [productsList, setProductsList] = useState(initialProducts); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + + const [isAdjustModalOpen, setIsAdjustModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + + // Tab 3 state + const [adjProduct, setAdjProduct] = useState(''); + const [adjType, setAdjType] = useState('add'); + const [adjQty, setAdjQty] = useState(''); + const [adjReason, setAdjReason] = useState(''); + + // Stats calculation + const totalSKUs = productsList.length; + const stockValue = productsList.reduce((acc, p) => acc + (p.stock * p.costPrice), 0); + const lowStockCount = productsList.filter(p => p.stock <= p.reorderPoint && p.stock > 0).length; + const outOfStockCount = productsList.filter(p => p.stock === 0).length; + + // Filtered products for Tab 1 + const filteredProducts = useMemo(() => { + let result = productsList; + if (searchQuery) { + const q = searchQuery.toLowerCase(); + result = result.filter(p => p.name.toLowerCase().includes(q) || p.sku.toLowerCase().includes(q)); + } + if (statusFilter !== 'all') { + if (statusFilter === 'ok') result = result.filter(p => p.stock > p.reorderPoint); + if (statusFilter === 'low') result = result.filter(p => p.stock <= p.reorderPoint && p.stock > 0); + if (statusFilter === 'oos') result = result.filter(p => p.stock === 0); + } + return result; + }, [productsList, searchQuery, statusFilter]); + + const handleAdjustConfirm = (productId: string, qty: number, type: string) => { + setProductsList(prev => prev.map(p => { + if (p.id === productId) { + let newStock = p.stock; + if (type === 'add') newStock += qty; + else newStock -= qty; // remove, damage, write-off + return { ...p, stock: newStock }; + } + return p; + })); + }; + + const handleManualAdjustmentSubmit = () => { + if (!adjProduct) return toast.error('Select a product'); + if (!adjQty || Number(adjQty) <= 0) return toast.error('Enter valid quantity'); + + handleAdjustConfirm(adjProduct, Number(adjQty), adjType); + setAdjProduct(''); + setAdjQty(''); + setAdjReason(''); + }; + + const renderStockLevels = () => ( +
+
+ setSearchQuery(e.target.value)} + /> +
{p.name} }, + { key: 'category', label: 'Category', render: p => {categories.find(c => c.id === p.categoryId)?.name || '-'} }, + { + key: 'stock', + label: 'On Hand', + render: p => { + let color = "text-gray-900"; + if (p.stock === 0) color = "text-red-600 font-bold"; + else if (p.stock <= p.reorderPoint) color = "text-amber-600 font-bold"; + return {p.stock} {p.unit}; + } + }, + { key: 'reorder', label: 'Reorder Point', render: p => {p.reorderPoint} {p.unit} }, + { + key: 'status', + label: 'Status', + render: p => { + if (p.stock === 0) return OOS; + if (p.stock <= p.reorderPoint) return Low; + return OK; + } + }, + { + key: 'action', + label: 'Action', + render: p => ( +
+ + {(p.stock <= p.reorderPoint) && ( + + )} +
+ ) + } + ]} + /> + + + ); + + const renderStocktake = () => ( +
+ +
+
+ +
+

Quickly scan and count specific shelves or categories without locking down the entire store.

+ +
+
+ +
+
+ +
+

Perform a full store audit. Freezes inventory movements until the count is reconciled and approved.

+ +
+
+
+ ); + + const renderAdjustments = () => ( +
+ +
+ setAdjType(e.target.value)} + options={[ + { value: 'add', label: 'Add Stock (+)' }, + { value: 'remove', label: 'Remove Stock (-)' }, + { value: 'damage', label: 'Damaged Goods (-)' }, + { value: 'write_off', label: 'Write-off (-)' }, + ]} + /> + setAdjQty(e.target.value ? Number(e.target.value) : '')} + /> + setAdjReason(e.target.value)} + /> + +
+
+
+ ); + + const renderHistory = () => ( +
+
+ + to + +
+
+
{h.date} }, + { key: 'product', label: 'Product', render: h => {h.product} }, + { key: 'type', label: 'Type', render: h => {h.type} }, + { + key: 'qtyChange', + label: 'Qty Change', + render: h => ( + 0 ? 'text-green-600' : 'text-red-600'}`}> + {h.qtyChange > 0 ? '+' : ''}{h.qtyChange} + + ) + }, + { key: 'reason', label: 'Reason', render: h => {h.reason} }, + { key: 'user', label: 'User' }, + { key: 'balance', label: 'Balance After', render: h => {h.balance} }, + ]} + /> + + + ); + + return ( +
+ {/* STATS ROW */} +
+ } + iconBg="bg-blue-100 text-blue-700" + /> + } + iconBg="bg-green-100 text-green-700" + /> + } + iconBg="bg-amber-100 text-amber-700" + /> + } + iconBg="bg-red-100 text-red-700" + /> +
+ + {/* TABS & CONTENT */} +
+ + +
+ {activeTab === 'levels' && renderStockLevels()} + {activeTab === 'stocktake' && renderStocktake()} + {activeTab === 'adjustments' && renderAdjustments()} + {activeTab === 'history' && renderHistory()} +
+
+ + setIsAdjustModalOpen(false)} + product={selectedProduct} + onConfirm={handleAdjustConfirm} + /> +
+ ); +} diff --git a/src/pages/pos/CustomerIdentifyPanel.tsx b/src/pages/pos/CustomerIdentifyPanel.tsx new file mode 100644 index 0000000..b0a827d --- /dev/null +++ b/src/pages/pos/CustomerIdentifyPanel.tsx @@ -0,0 +1,237 @@ +import { useState, KeyboardEvent, ClipboardEvent } from 'react'; +import { Customer } from '@/types'; +import { customers } from '@/data/customers'; +import { useCartStore, WalkInCustomer } from '@/stores/cartStore'; +import { User, Search, UserPlus, Phone, Calendar, Mail, ArrowRight } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function CustomerIdentifyPanel() { + const [phoneInput, setPhoneInput] = useState(''); + const [hasSearched, setHasSearched] = useState(false); + const [foundCustomer, setFoundCustomer] = useState(null); + + // New customer form state + const [newName, setNewName] = useState(''); + const [newEmail, setNewEmail] = useState(''); + const [newDob, setNewDob] = useState(''); + + const { setCustomer } = useCartStore(); + + const handleSearch = () => { + if (phoneInput.length !== 10) { + return toast.error('Please enter a valid 10-digit mobile number'); + } + const customer = customers.find(c => c.phone === phoneInput); + if (customer) { + setFoundCustomer(customer); + } else { + setFoundCustomer(null); + } + setHasSearched(true); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') handleSearch(); + }; + + const handlePaste = (e: ClipboardEvent) => { + const paste = e.clipboardData.getData('text').replace(/\D/g, ''); + if (paste.length >= 10) { + const tenDigits = paste.slice(0, 10); + setPhoneInput(tenDigits); + setTimeout(() => { + const customer = customers.find(c => c.phone === tenDigits); + if (customer) setFoundCustomer(customer); + else setFoundCustomer(null); + setHasSearched(true); + }, 50); + } + }; + + const handleRegisterAndStart = () => { + if (!newName.trim()) { + return toast.error('Name is required to register'); + } + + const initials = newName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase() || 'C'; + + const newCustomer: Customer = { + id: `c${Date.now()}`, + name: newName, + phone: phoneInput, + email: newEmail, + dob: newDob, + loyaltyPoints: 0, + tier: 'silver', + totalSpent: 0, + storeCredit: 0, + lastVisit: new Date().toISOString(), + initials + }; + + // Add to local state (mutates the imported array so it persists during session) + customers.push(newCustomer); + + setCustomer(newCustomer); + toast.success('Customer registered successfully!'); + }; + + const startBill = () => { + if (foundCustomer) { + setCustomer(foundCustomer); + } + }; + + + + const resetSearch = () => { + setHasSearched(false); + setFoundCustomer(null); + setPhoneInput(''); + setNewName(''); + setNewEmail(''); + setNewDob(''); + }; + + return ( +
+
+ +
+
+ +
+

Who is this sale for?

+

Enter customer mobile number to start billing

+
+ + {!hasSearched ? ( +
+
+ + setPhoneInput(e.target.value.replace(/\D/g, ''))} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + autoFocus + /> +
+ +
+ ) : foundCustomer ? ( + /* CASE A: Customer FOUND */ +
+
+
+ +
+ {foundCustomer.initials} +
+ +

{foundCustomer.name}

+
+ {foundCustomer.tier} + 🎯 {foundCustomer.loyaltyPoints} pts +
+ +
Welcome back, {foundCustomer.name.split(' ')[0]}!
+
+ +
+ + +
+
+ ) : ( + /* CASE B: Customer NOT FOUND */ +
+
+
+ +
+ +

New customer

+
+ +
+ setNewName(e.target.value)} + className="w-full h-[44px] px-4 bg-white border border-primary/20 rounded-lg focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 font-medium text-sm" + autoFocus + /> + +
+
+ + setNewEmail(e.target.value)} + className="w-full h-[44px] pl-9 pr-3 bg-white border border-primary/20 rounded-lg focus:outline-none focus:border-primary font-medium text-sm" + /> +
+
+ + setNewDob(e.target.value)} + className="w-full h-[44px] pl-9 pr-3 bg-white border border-primary/20 rounded-lg focus:outline-none focus:border-primary font-medium text-sm text-gray-600" + /> +
+
+
+
+ +
+ + +
+
+ )} + +
+
+ ); +} diff --git a/src/pages/pos/POSPage.tsx b/src/pages/pos/POSPage.tsx new file mode 100644 index 0000000..b720910 --- /dev/null +++ b/src/pages/pos/POSPage.tsx @@ -0,0 +1,389 @@ +import { useState, useMemo } from 'react'; +import { SearchBar, Badge, TopbarAction } from '@/components/ui'; +import { products } from '@/data/products'; +import { categories } from '@/data/categories'; +import { useCartStore } from '@/stores/cartStore'; +import { formatCurrency } from '@/lib/utils'; +import PaymentModal from './PaymentModal'; +import ReceiptModal from './ReceiptModal'; +import CustomerIdentifyPanel from './CustomerIdentifyPanel'; +import RefundModal from './RefundModal'; +import { ShoppingCart, X, Plus, Minus, RefreshCcw } from 'lucide-react'; +import toast from 'react-hot-toast'; +import { Sale } from '@/types'; +import { useBarcodeScanner } from '@/hooks/useBarcodeScanner'; +import { useAuthStore } from '@/stores/authStore'; + +export default function POSPage() { + const [activeCategory, setActiveCategory] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false); + const [isRefundOpen, setIsRefundOpen] = useState(false); + const [shakeCart, setShakeCart] = useState(false); + const [completedSale, setCompletedSale] = useState(null); + + const { currentUser } = useAuthStore(); + + const { + activeCustomer, clearCustomer, clearCart, + items, addItem, removeItem, updateQty, + getSubtotal, getTaxAmount, getTotal, + parkedSales, parkSale + } = useCartStore(); + + const handlePaymentSuccess = (sale: Sale) => { + setIsPaymentModalOpen(false); + setCompletedSale(sale); + }; + + const handleCloseReceipt = () => { + setCompletedSale(null); + clearCart(); + clearCustomer(); + }; + + useBarcodeScanner({ + onScan: (barcode) => { + // Only allow scanning if a customer is identified and no modals are open + if (!activeCustomer || isPaymentModalOpen || isRefundOpen || completedSale) return; + + const product = products.find(p => p.barcode === barcode); + if (product) { + addItem(product); + toast.success(`Added ${product.name}`, { duration: 1500, id: 'barcode-success' }); + } else { + toast.error(`Barcode ${barcode} not found`, { duration: 2000, id: 'barcode-error' }); + } + } + }); + + const filteredProducts = useMemo(() => { + return products.filter((p) => { + const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) || + p.sku.toLowerCase().includes(searchQuery.toLowerCase()) || + p.barcode.includes(searchQuery); + const matchesCategory = activeCategory === 'all' || p.categoryId === activeCategory; + return matchesSearch && matchesCategory; + }); + }, [searchQuery, activeCategory]); + + const handleChangeCustomer = () => { + if (items.length > 0) { + if (window.confirm("Clear cart and change customer?")) { + clearCart(); + } + } else { + clearCustomer(); + } + }; + + const handleChargeClick = () => { + if (items.length === 0) { + setShakeCart(true); + setTimeout(() => setShakeCart(false), 500); + return; + } + setIsPaymentModalOpen(true); + }; + + const handlePark = () => { + if (items.length === 0) return; + const parkId = `P${parkedSales.length + 1}`; + parkSale(); + toast.success(`Bill parked as ${parkId}`); + }; + + if (!activeCustomer) { + return ( + <> + +
+

🛒 POS Terminal

+ + + Live + + {parkedSales.length > 0 && ( + + 🅿 {parkedSales.length} parked + + )} + +
+
+ + setIsRefundOpen(false)} + /> + + ); + } + + const isWalkIn = activeCustomer.id === 'walkin'; + const total = getTotal(); + const pointsToEarn = Math.floor(total / 10); + + return ( + <> + +
+

🛒 POS Terminal

+ + + Live + + {parkedSales.length > 0 && ( + + 🅿 {parkedSales.length} parked + + )} + {currentUser?.role === 'manager' && ( + + )} +
+
+ +
+ + {/* LEFT PANEL - Product Browser */} +
+ + {/* Customer Header */} +
+
+ {!isWalkIn && 'initials' in activeCustomer ? ( +
+ {activeCustomer.initials} +
+ ) : ( +
+ +
+ )} +
+ {activeCustomer.name} + {!isWalkIn && ( + + {activeCustomer.tier} + + )} +
+
+ +
+ + {/* Search & Categories */} +
+ setSearchQuery(e.target.value)} + placeholder="🔍 Search product, scan barcode or enter SKU…" + className="h-[48px] text-base" + /> + +
+ + {categories.map((cat) => ( + + ))} +
+
+ + {/* Product Grid */} +
+
+ {filteredProducts.map((p) => { + const isOOS = p.stock === 0; + return ( +
!isOOS && addItem(p)} + className={`bg-white border ${isOOS ? 'border-red-200 opacity-50 pointer-events-none' : 'border-gray-200'} rounded-xl min-h-[130px] p-3 flex flex-col select-none touch-manipulation transition-all ${isOOS ? '' : 'active:scale-[0.96] active:border-primary'}`} + > +
{p.emoji}
+
{p.name}
+
+
{formatCurrency(p.price)}
+
{p.stock} in stock
+
+
+ ) + })} + {filteredProducts.length === 0 && ( +
+ No products found. +
+ )} +
+
+
+ + {/* RIGHT PANEL - Cart */} +
+
+
+
+

Cart

+ {items.length > 0 && ( + {items.reduce((acc, item) => acc + item.qty, 0)} + )} +
+
+ +
+ {items.length > 0 && ( + + )} + {items.length > 0 && ( + + )} +
+
+ +
+ {items.length === 0 ? ( +
+ +

Tap a product to add it

+
+ ) : ( +
+ {items.map((item) => ( +
+
{item.product.emoji}
+
+
{item.product.name}
+
{formatCurrency(item.product.price * item.qty)}
+
+
+ +
+ + {item.qty} + +
+
+
+ ))} +
+ )} +
+ +
+ {/* Points Earned Preview */} + {!isWalkIn && items.length > 0 && ( +
+ 🎯 This sale earns +{pointsToEarn} pts +
+ )} + +
+
+ Subtotal + {formatCurrency(getSubtotal())} +
+
+ GST (18%) + {formatCurrency(getTaxAmount())} +
+
+ Discount + -₹0.00 +
+ +
+ Total + {formatCurrency(total)} +
+ + +
+
+
+ + setIsPaymentModalOpen(false)} + onPaymentSuccess={handlePaymentSuccess} + /> + + + + setIsRefundOpen(false)} + /> +
+ + ); +} diff --git a/src/pages/pos/PaymentModal.tsx b/src/pages/pos/PaymentModal.tsx new file mode 100644 index 0000000..188c9ee --- /dev/null +++ b/src/pages/pos/PaymentModal.tsx @@ -0,0 +1,225 @@ +import { useState, useEffect } from 'react'; +import { useCartStore, PaymentMethod } from '@/stores/cartStore'; +import { customers } from '@/data/customers'; +import { sales } from '@/data/sales'; +import { Sale } from '@/types'; +import { useAuthStore } from '@/stores/authStore'; +import { formatCurrency } from '@/lib/utils'; +import { X, Delete, Banknote, CreditCard, Smartphone, SplitSquareHorizontal } from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface PaymentModalProps { + isOpen: boolean; + onClose: () => void; + onPaymentSuccess?: (sale: Sale) => void; +} + +export default function PaymentModal({ isOpen, onClose, onPaymentSuccess }: PaymentModalProps) { + const { activeCustomer, items, getSubtotal, getTaxAmount, getTotal, paymentMethod, setPaymentMethod, clearCart } = useCartStore(); + const { currentUser } = useAuthStore(); + const [cashReceived, setCashReceived] = useState(''); + + const total = getTotal(); + + useEffect(() => { + if (isOpen) { + setCashReceived(''); + } + }, [isOpen]); + + if (!isOpen || !activeCustomer) return null; + + const isWalkIn = activeCustomer.id === 'walkin'; + const pointsToEarn = Math.floor(total / 10); + + const cashAmount = parseFloat(cashReceived) || 0; + const changeDue = Math.max(0, cashAmount - total); + + const paymentMethods: { id: PaymentMethod, label: string, icon: React.ReactNode }[] = [ + { id: 'cash', label: 'Cash', icon: }, + { id: 'card', label: 'Card', icon: }, + { id: 'upi', label: 'UPI', icon: }, + { id: 'split', label: 'Split', icon: }, + ]; + + const handleNumpad = (val: string) => { + if (val === 'Exact') { + setCashReceived(total.toString()); + return; + } + if (val === 'backspace') { + setCashReceived(prev => prev.slice(0, -1)); + return; + } + + // Validate max amount + if (cashReceived.length < 6) { + // Prevent multiple leading zeros + if (cashReceived === '0' && val === '0') return; + if (cashReceived === '0' && val !== '0') { + setCashReceived(val); + return; + } + setCashReceived(prev => prev + val); + } + }; + + const handleConfirm = () => { + if (paymentMethod === 'cash' && cashAmount < total) { + return toast.error('Received amount is less than the total due.'); + } + + // Assign loyalty points if not a walk-in + if (!isWalkIn) { + const customerToUpdate = customers.find(c => c.id === activeCustomer.id); + if (customerToUpdate) { + customerToUpdate.loyaltyPoints += pointsToEarn; + } + } + + // Record the sale + const newSale: Sale = { + id: `INV-2026-${1000 + sales.length + 1}`, + type: 'sale', + date: new Date().toISOString(), + cashier: currentUser?.name || 'Unknown', + customerId: isWalkIn ? 'walkin' : activeCustomer.id, + items: items.map(i => ({ + productId: i.product.id, + name: i.product.name, + qty: i.qty, + unitPrice: i.product.price + })), + subtotal: getSubtotal(), + taxAmount: getTaxAmount(), + discountAmount: 0, + total: total, + paymentMethod: paymentMethod + }; + + sales.unshift(newSale); + + const toastMsg = isWalkIn + ? '✅ Payment successful!' + : `✅ Payment successful! +${pointsToEarn} pts added to ${activeCustomer.name.split(' ')[0]}`; + + toast.success(toastMsg, { duration: 4000 }); + + // Call success callback and close modal (cart is cleared by Receipt modal or parent) + if (onPaymentSuccess) { + onPaymentSuccess(newSale); + } else { + clearCart(); + } + onClose(); + }; + + const numpadKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'Exact', '0', 'backspace']; + + return ( +
+
+ +
+ + {/* Header */} +
+
+ Billing + {activeCustomer.name} +
+ +
+ +
+ + {/* Amount Due Box */} +
+ Amount Due + {formatCurrency(total)} +
+ + {/* Payment Method Selection */} +
+ {paymentMethods.map((method) => { + const isActive = paymentMethod === method.id; + return ( + + ); + })} +
+ + {/* Cash Input & Change */} + {paymentMethod === 'cash' && ( +
+
+ Received: + + ₹{cashReceived || '0'} + +
+ {cashAmount >= total && ( +
+ Change Due: + {formatCurrency(changeDue)} +
+ )} +
+ )} + + {/* Points preview */} + {!isWalkIn && pointsToEarn > 0 && ( +
+

+ 🎯 +{pointsToEarn} pts will be added to {activeCustomer.name.split(' ')[0]}'s account +

+
+ )} + + {/* Numpad */} + {paymentMethod === 'cash' && ( +
+ {numpadKeys.map((key) => ( + + ))} +
+ )} + + +
+ +
+
+ ); +} diff --git a/src/pages/pos/ReceiptModal.tsx b/src/pages/pos/ReceiptModal.tsx new file mode 100644 index 0000000..bcc4393 --- /dev/null +++ b/src/pages/pos/ReceiptModal.tsx @@ -0,0 +1,159 @@ +import { Sale } from '@/types'; +import { X, Printer, Plus } from 'lucide-react'; +import { formatCurrency } from '@/lib/utils'; +import { customers } from '@/data/customers'; + +interface ReceiptModalProps { + isOpen: boolean; + onClose: () => void; + sale: Sale | null; +} + +export default function ReceiptModal({ isOpen, onClose, sale }: ReceiptModalProps) { + if (!isOpen || !sale) return null; + + const customer = sale.customerId && sale.customerId !== 'walkin' + ? customers.find(c => c.id === sale.customerId) + : null; + + const handlePrint = () => { + window.print(); + }; + + return ( +
+
+ + {/* Top Actions */} +
+

Transaction Complete

+ +
+ + {/* Receipt Scroll Area */} +
+ + {/* Actual Receipt Paper */} +
+ + {/* Header */} +
+

Nearle Daily

+

123 Main Street, Market Area

+

GSTIN: 29ABCDE1234F1Z5

+
+ +
+ + {/* Meta */} +
+
+ Date: {new Date(sale.date).toLocaleDateString()} + Time: {new Date(sale.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ Bill: {sale.id} + Cashier: {sale.cashier} +
+
+ Customer: + {customer ? customer.name : 'Walk-in'} +
+
+ +
+ + {/* Items */} +
+ + + + + + + + + + {sale.items.map((item, idx) => ( + + + + + + + ))} + +
ItemQtyPriceTotal
{item.name}{item.qty}{formatCurrency(item.unitPrice)}{formatCurrency(item.unitPrice * item.qty)}
+ +
+ + {/* Totals */} +
+
+ Subtotal + {formatCurrency(sale.subtotal)} +
+
+ GST + {formatCurrency(sale.taxAmount)} +
+
+ Total + {formatCurrency(sale.total)} +
+
+ +
+ + {/* Payment Info */} +
+
+ Payment Method: + {sale.paymentMethod} +
+ {customer && ( +
+ Points Earned: + +{Math.floor(sale.total / 10)} pts +
+ )} +
+ + {/* Footer */} +
+

Thank you for shopping!

+

Please visit again.

+
+ {/* Mock Barcode */} +
+
+
+ +
+
+ + {/* Bottom Actions */} +
+ + +
+ +
+
+ ); +} diff --git a/src/pages/pos/RefundModal.tsx b/src/pages/pos/RefundModal.tsx new file mode 100644 index 0000000..603418f --- /dev/null +++ b/src/pages/pos/RefundModal.tsx @@ -0,0 +1,588 @@ +import { useState } from 'react'; +import { X, Search, Phone, Receipt, RefreshCcw, CheckSquare, Square, Store, Smartphone, Banknote } from 'lucide-react'; +import { customers } from '@/data/customers'; +import { sales } from '@/data/sales'; +import { products } from '@/data/products'; +import { Sale, SaleItem } from '@/types'; +import { formatCurrency } from '@/lib/utils'; +import toast from 'react-hot-toast'; +import { useAuthStore } from '@/stores/authStore'; + +interface RefundModalProps { + isOpen: boolean; + onClose: () => void; +} + +type Step = 1 | 2 | 3 | 4; +type SearchType = 'phone' | 'bill'; + +export default function RefundModal({ isOpen, onClose }: RefundModalProps) { + const { currentUser } = useAuthStore(); + const [step, setStep] = useState(1); + const [searchType, setSearchType] = useState('phone'); + + // Step 1 State + const [phoneInput, setPhoneInput] = useState(''); + const [billInput, setBillInput] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [hasSearched, setHasSearched] = useState(false); + const [selectedSale, setSelectedSale] = useState(null); + + // Step 2 State + const [selectedItemIds, setSelectedItemIds] = useState([]); + const [refundMethod, setRefundMethod] = useState<'cash' | 'card' | 'upi' | 'store_credit'>('cash'); + + // Step 3 State + const [managerPin, setManagerPin] = useState(''); + const [pinError, setPinError] = useState(false); + + // Reset state when closing + const handleClose = () => { + setStep(1); + setSearchType('phone'); + setPhoneInput(''); + setBillInput(''); + setSearchResults([]); + setHasSearched(false); + setSelectedSale(null); + setSelectedItemIds([]); + setRefundMethod('cash'); + setManagerPin(''); + setPinError(false); + onClose(); + }; + + if (!isOpen) return null; + + // Search Handlers + const handleSearch = () => { + setHasSearched(true); + if (searchType === 'phone') { + const customer = customers.find(c => c.phone === phoneInput); + if (customer) { + // Find all completed sales for this customer that are not refunds + const customerSales = sales.filter(s => s.customerId === customer.id && s.type !== 'refund'); + setSearchResults(customerSales.reverse()); + } else { + setSearchResults([]); + } + } else { + const sale = sales.find(s => s.id === billInput.trim() && s.type !== 'refund'); + setSearchResults(sale ? [sale] : []); + } + }; + + const handleSelectSale = (sale: Sale) => { + // Check if fully refunded + const relatedRefunds = sales.filter(s => s.type === 'refund' && s.originalBillId === sale.id); + const totalRefundedItems = relatedRefunds.reduce((acc, r) => acc + r.items.length, 0); + + if (totalRefundedItems >= sale.items.length) { + toast.error('This bill has already been fully refunded.'); + return; + } + + setSelectedSale(sale); + + // Auto-select items that haven't been refunded yet + const alreadyRefundedItemIds = new Set(relatedRefunds.flatMap(r => r.items.map(i => i.productId))); + const availableItems = sale.items.filter(i => !alreadyRefundedItemIds.has(i.productId)); + + setSelectedItemIds(availableItems.map(i => i.productId)); + + // Default refund method to original if applicable + if (sale.paymentMethod !== 'split') { + setRefundMethod(sale.paymentMethod as any); + } else { + setRefundMethod('cash'); + } + + setStep(2); + }; + + // Step 2 Logic + const toggleItem = (productId: string) => { + setSelectedItemIds(prev => + prev.includes(productId) ? prev.filter(id => id !== productId) : [...prev, productId] + ); + }; + + const toggleAllItems = (availableItems: SaleItem[]) => { + if (selectedItemIds.length === availableItems.length) { + setSelectedItemIds([]); + } else { + setSelectedItemIds(availableItems.map(i => i.productId)); + } + }; + + const getRefundTotals = () => { + if (!selectedSale) return { amount: 0, points: 0 }; + const refundItems = selectedSale.items.filter(i => selectedItemIds.includes(i.productId)); + const amount = refundItems.reduce((sum, item) => sum + (item.unitPrice * item.qty), 0); + const points = Math.floor(amount / 10); + return { amount, points }; + }; + + // Step 3 Logic + const handleProcessRefund = () => { + const { amount, points } = getRefundTotals(); + const PIN_THRESHOLD = 200; + + if (amount >= PIN_THRESHOLD && managerPin !== '1234') { + setPinError(true); + return; + } + + if (!selectedSale) return; + + const refundItems = selectedSale.items.filter(i => selectedItemIds.includes(i.productId)); + + // 1. Add qty back to stock + refundItems.forEach(item => { + const prod = products.find(p => p.id === item.productId); + if (prod) { + prod.stock += item.qty; + } + }); + + // 2. Deduct loyalty points (if applicable) + let customerName = 'Walk-in'; + if (selectedSale.customerId && selectedSale.customerId !== 'walkin') { + const customer = customers.find(c => c.id === selectedSale.customerId); + if (customer) { + customer.loyaltyPoints = Math.max(0, customer.loyaltyPoints - points); + customerName = customer.name; + + if (refundMethod === 'store_credit') { + customer.storeCredit = (customer.storeCredit || 0) + amount; + } + } + } + + // 3. Record refund sale + const newRefund: Sale = { + id: `REF-${selectedSale.id}-${Date.now().toString().slice(-4)}`, + type: 'refund', + originalBillId: selectedSale.id, + date: new Date().toISOString(), + cashier: currentUser?.name || 'Unknown', + customerId: selectedSale.customerId, + items: refundItems, + subtotal: -amount, + taxAmount: 0, + discountAmount: 0, + total: -amount, + paymentMethod: refundMethod + }; + + sales.unshift(newRefund); + + toast.success(`✅ Refund of ${formatCurrency(amount)} processed for ${customerName}`); + setStep(4); + }; + + // --------------------------------------------------------- + // RENDER HELPERS + // --------------------------------------------------------- + + const renderStep1 = () => ( +
+
+

🔄 Process Refund

+

Search by customer phone or bill number

+
+ +
+ + +
+ +
+ {searchType === 'phone' ? ( + setPhoneInput(e.target.value.replace(/\D/g, ''))} + className="flex-1 h-[56px] px-4 bg-gray-50 border-2 border-gray-200 rounded-xl text-[20px] font-mono focus:outline-none focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all" + autoFocus + /> + ) : ( + setBillInput(e.target.value)} + className="flex-1 h-[56px] px-4 bg-gray-50 border-2 border-gray-200 rounded-xl text-[20px] font-mono focus:outline-none focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all" + autoFocus + /> + )} + +
+ +
+ {!hasSearched ? ( +
+ Enter search criteria to find sales +
+ ) : searchResults.length === 0 ? ( +
+ {searchType === 'phone' ? 'No completed sales found for this customer.' : 'Bill not found.'} +
+ ) : ( +
+

Search Results

+ {searchResults.map(sale => { + const customer = sale.customerId ? customers.find(c => c.id === sale.customerId) : null; + const isRefunded = sales.filter(s => s.type === 'refund' && s.originalBillId === sale.id).reduce((acc, r) => acc + r.items.length, 0) >= sale.items.length; + + return ( +
+
+
+ {sale.id} + + {new Date(sale.date).toLocaleDateString()} + + {sale.paymentMethod} + +
+
+ {customer?.name || 'Walk-in'} + + {sale.items.reduce((acc, i) => acc + i.qty, 0)} items +
+
+ +
+
{formatCurrency(sale.total)}
+ +
+
+ ); + })} +
+ )} +
+
+ ); + + const renderStep2 = () => { + if (!selectedSale) return null; + const customer = selectedSale.customerId ? customers.find(c => c.id === selectedSale.customerId) : null; + + // Check which items are already refunded + const relatedRefunds = sales.filter(s => s.type === 'refund' && s.originalBillId === selectedSale.id); + const alreadyRefundedItemIds = new Set(relatedRefunds.flatMap(r => r.items.map(i => i.productId))); + const availableItems = selectedSale.items.filter(i => !alreadyRefundedItemIds.has(i.productId)); + const { amount, points } = getRefundTotals(); + + const refundMethods = [ + { id: 'cash', label: 'Cash', icon: }, + { id: 'upi', label: 'UPI', icon: }, + { id: 'store_credit', label: 'Store Credit', icon: }, + ]; + + return ( +
+
+ +
+

Select Items

+

{selectedSale.id} • {customer?.name || 'Walk-in'}

+
+
+ +
+ Items in bill: + +
+ +
+ {selectedSale.items.map((item) => { + const isRefunded = alreadyRefundedItemIds.has(item.productId); + const isSelected = selectedItemIds.includes(item.productId); + const prod = products.find(p => p.id === item.productId); + + return ( +
+
!isRefunded && toggleItem(item.productId)} + className={`w-11 h-11 flex items-center justify-center rounded-lg flex-shrink-0 cursor-pointer select-none touch-manipulation ${isRefunded ? '' : 'active:scale-95 transition-transform'}`} + > + {isRefunded ? ( +
+ ) : isSelected ? ( + + ) : ( + + )} +
+ +
+
+ {prod?.emoji} + {item.name} +
+
+ {item.qty} × {formatCurrency(item.unitPrice)} + {isRefunded && Refunded} +
+
+ +
+ {formatCurrency(item.qty * item.unitPrice)} +
+
+ ); + })} +
+ +
+
+ Items to return: + {selectedItemIds.length} of {availableItems.length} +
+ {points > 0 && customer && ( +
+ Loyalty points: + -{points} pts +
+ )} +
+ Refund amount: + {formatCurrency(amount)} +
+ +
+ Refund via: +
+ {refundMethods.map(method => ( + + ))} +
+ {selectedSale.paymentMethod === 'card' && refundMethod !== 'store_credit' && ( +

+ ℹ️ Card refunds may take 3-5 days. You can refund as cash/store credit instead. +

+ )} +
+
+ + +
+ ); + }; + + const renderStep3 = () => { + if (!selectedSale) return null; + const customer = selectedSale.customerId ? customers.find(c => c.id === selectedSale.customerId) : null; + const { amount, points } = getRefundTotals(); + const PIN_THRESHOLD = 200; + const requiresPin = amount >= PIN_THRESHOLD; + + return ( +
+
+ +
+

Confirm Refund

+
+
+ +
+
+ Customer: + {customer?.name || 'Walk-in'} +
+
+ Bill #: + {selectedSale.id} +
+
+ Items returned: + {selectedItemIds.length} items +
+
+ Refund via: + {refundMethod.replace('_', ' ')} +
+ {points > 0 && customer && ( +
+ Points deducted: + -{points} pts +
+ )} +
+ Refund amount: + {formatCurrency(amount)} +
+
+ +
+ {requiresPin ? ( +
+ + { + setManagerPin(e.target.value.replace(/\D/g, '')); + setPinError(false); + }} + className={`w-full h-[56px] text-center text-[28px] font-mono tracking-[0.5em] border-2 rounded-xl focus:outline-none transition-all bg-gray-50 ${ + pinError ? 'border-red-500 bg-red-50 text-red-600' : 'border-gray-300 focus:border-primary focus:bg-white' + }`} + autoFocus + /> + {pinError && ( +

+ Incorrect PIN. Try again. +

+ )} +
+ ) : ( +
+
+ 👍 +
+

Refund amount is under {formatCurrency(PIN_THRESHOLD)}.

+

No Manager PIN required.

+
+ )} +
+ +
+ + +
+
+ ); + }; + + const renderStep4 = () => ( +
+
+ +
+

Refund Successful!

+

+ The refund has been processed and recorded in the system. +

+ +
+ ); + + return ( +
+
+ + + +
+ {step === 1 && renderStep1()} + {step === 2 && renderStep2()} + {step === 3 && renderStep3()} + {step === 4 && renderStep4()} +
+ +
+
+ ); +} diff --git a/src/pages/products/ProductDrawer.tsx b/src/pages/products/ProductDrawer.tsx new file mode 100644 index 0000000..38661ae --- /dev/null +++ b/src/pages/products/ProductDrawer.tsx @@ -0,0 +1,176 @@ +import { useState, useEffect } from 'react'; +import { Button, Input, Select } from '@/components/ui'; +import { Product } from '@/types'; +import { categories } from '@/data/categories'; +import { X } from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface ProductDrawerProps { + isOpen: boolean; + onClose: () => void; + product?: Product | null; + onSave: (product: Partial) => void; +} + +export default function ProductDrawer({ isOpen, onClose, product, onSave }: ProductDrawerProps) { + const [formData, setFormData] = useState>({}); + const [errors, setErrors] = useState>({}); + + useEffect(() => { + if (isOpen) { + if (product) { + setFormData({ ...product }); + } else { + setFormData({ + name: '', + sku: '', + barcode: '', + categoryId: categories[0]?.id || '', + price: 0, + costPrice: 0, + taxRate: 0, + stock: 0, + reorderPoint: 0, + unit: 'pc', + emoji: '📦', + }); + } + setErrors({}); + } + }, [isOpen, product]); + + if (!isOpen) return null; + + const handleChange = (field: keyof Product, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: '' })); + } + }; + + const handleSave = () => { + const newErrors: Record = {}; + if (!formData.name) newErrors.name = 'Name is required'; + if (!formData.sku) newErrors.sku = 'SKU is required'; + if (formData.price === undefined || formData.price < 0) newErrors.price = 'Valid price is required'; + if (formData.costPrice === undefined || formData.costPrice < 0) newErrors.costPrice = 'Valid cost price is required'; + if (formData.stock === undefined || formData.stock < 0) newErrors.stock = 'Valid stock is required'; + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + + onSave(formData); + toast.success(product ? 'Product updated!' : 'Product created!'); + onClose(); + }; + + return ( +
+
+ +
+
+

{product ? 'Edit Product' : 'Add Product'}

+ +
+ +
+ handleChange('name', e.target.value)} + error={errors.name} + /> +
+ handleChange('sku', e.target.value)} + error={errors.sku} + /> + handleChange('barcode', e.target.value)} + /> +
+ + handleChange('price', parseFloat(e.target.value))} + error={errors.price} + /> + handleChange('costPrice', parseFloat(e.target.value))} + error={errors.costPrice} + /> +
+ +
+ handleChange('unit', e.target.value)} + options={['pc', 'kg', 'g', 'L', 'ml'].map(u => ({ value: u, label: u }))} + /> +
+ +
+ handleChange('stock', parseInt(e.target.value, 10))} + error={errors.stock} + /> + handleChange('reorderPoint', parseInt(e.target.value, 10))} + /> +
+ + handleChange('emoji', e.target.value)} + /> + +
+ +
+ +
+
+
+ ); +} diff --git a/src/pages/products/ProductsPage.tsx b/src/pages/products/ProductsPage.tsx new file mode 100644 index 0000000..31dc055 --- /dev/null +++ b/src/pages/products/ProductsPage.tsx @@ -0,0 +1,222 @@ +import { useState, useMemo, useEffect } from 'react'; +import { SearchBar, Select, Button, Table, Badge } from '@/components/ui'; +import { products as initialProducts } from '@/data/products'; +import { categories } from '@/data/categories'; +import { Product } from '@/types'; +import { formatCurrency } from '@/lib/utils'; +import ProductDrawer from './ProductDrawer'; +import { Download, Plus } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function ProductsPage() { + const [productsList, setProductsList] = useState(initialProducts); + const [searchQuery, setSearchQuery] = useState(''); + const [activeCategory, setActiveCategory] = useState('all'); + const [isLoading, setIsLoading] = useState(true); + const [stockStatus, setStockStatus] = useState('all'); + const [sortConfig, setSortConfig] = useState<{ key: keyof Product, direction: 'asc'|'desc' } | null>(null); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [editingProduct, setEditingProduct] = useState(null); + + useEffect(() => { + const timer = setTimeout(() => setIsLoading(false), 600); + return () => clearTimeout(timer); + }, []); + + const filteredProducts = useMemo(() => { + let result = productsList; + + // Search filter + if (searchQuery) { + const q = searchQuery.toLowerCase(); + result = result.filter(p => + p.name.toLowerCase().includes(q) || + p.sku.toLowerCase().includes(q) || + p.barcode.includes(q) + ); + } + + // Category filter + if (activeCategory !== 'all') { + result = result.filter(p => p.categoryId === activeCategory); + } + + // Stock status filter + if (stockStatus !== 'all') { + if (stockStatus === 'in_stock') result = result.filter(p => p.stock > p.reorderPoint); + if (stockStatus === 'low_stock') result = result.filter(p => p.stock <= p.reorderPoint && p.stock > 0); + if (stockStatus === 'out_of_stock') result = result.filter(p => p.stock === 0); + } + + // Sorting + if (sortConfig) { + result.sort((a, b) => { + const aVal = a[sortConfig.key]; + const bVal = b[sortConfig.key]; + if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1; + if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + } + + return result; + }, [productsList, searchQuery, activeCategory, stockStatus, sortConfig]); + + const handleSort = (key: string) => { + const typedKey = key as keyof Product; + let direction: 'asc' | 'desc' = 'asc'; + if (sortConfig && sortConfig.key === typedKey && sortConfig.direction === 'asc') { + direction = 'desc'; + } + setSortConfig({ key: typedKey, direction }); + }; + + const handleEdit = (product: Product) => { + setEditingProduct(product); + setIsDrawerOpen(true); + }; + + const handleAdd = () => { + setEditingProduct(null); + setIsDrawerOpen(true); + }; + + const handleSaveProduct = (prodData: Partial) => { + if (editingProduct) { + setProductsList(prev => prev.map(p => p.id === editingProduct.id ? { ...p, ...prodData } as Product : p)); + } else { + const newProduct = { ...prodData, id: `p${Date.now()}` } as Product; + setProductsList(prev => [newProduct, ...prev]); + } + }; + + return ( +
+
+
+ setSearchQuery(e.target.value)} + /> + setStockStatus(e.target.value)} + options={[ + { value: 'all', label: 'All Stock Status' }, + { value: 'in_stock', label: 'In Stock' }, + { value: 'low_stock', label: 'Low Stock' }, + { value: 'out_of_stock', label: 'Out of Stock' }, + ]} + /> +
+ +
+ + +
+
+ +
+
+

Product Catalog

+ Showing {filteredProducts.length} of {productsList.length} products +
+ +
+ {isLoading ? ( +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ ) : ( + ( +
+ {p.emoji} +
+ {p.name} + {p.barcode} +
+
+ ) + }, + { key: 'sku', label: 'SKU', render: (p) => {p.sku} }, + { key: 'categoryId', label: 'Category', render: (p) => {categories.find(c => c.id === p.categoryId)?.name || '-'} }, + { + key: 'price', + label: 'Price', + sortable: true, + render: (p) => {formatCurrency(p.price)} + }, + { key: 'costPrice', label: 'Cost', render: (p) => {formatCurrency(p.costPrice)} }, + { + key: 'stock', + label: 'Stock', + sortable: true, + render: (p) => { + let styling = "text-gray-900 font-medium"; + if (p.stock === 0) styling = "text-red-600 font-bold"; + else if (p.stock <= p.reorderPoint) styling = "text-amber-600 font-bold"; + return {p.stock} {p.unit}; + } + }, + { + key: 'status', + label: 'Status', + render: (p) => { + if (p.stock === 0) return Out of Stock; + if (p.stock <= p.reorderPoint) return Low Stock; + return In Stock; + } + }, + { + key: 'actions', + label: 'Actions', + render: (p) => ( + + ) + } + ]} + /> + )} + + + + setIsDrawerOpen(false)} + product={editingProduct} + onSave={handleSaveProduct} + /> + + ); +} diff --git a/src/pages/promotions/CreatePromotionDrawer.tsx b/src/pages/promotions/CreatePromotionDrawer.tsx new file mode 100644 index 0000000..f922428 --- /dev/null +++ b/src/pages/promotions/CreatePromotionDrawer.tsx @@ -0,0 +1,180 @@ +import { useState, useEffect } from 'react'; +import { Button, Input, Select } from '@/components/ui'; +import { Promotion } from '@/types'; +import { X, Tag, Gift, Cake, CalendarClock } from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface CreatePromotionDrawerProps { + isOpen: boolean; + onClose: () => void; + onSave: (promo: Promotion) => void; +} + +export default function CreatePromotionDrawer({ isOpen, onClose, onSave }: CreatePromotionDrawerProps) { + const [step, setStep] = useState(1); + const [formData, setFormData] = useState>({}); + + useEffect(() => { + if (isOpen) { + setStep(1); + setFormData({ + name: '', + type: 'percentage', + value: 0, + applyTo: 'All', + status: 'active', + icon: '🏷️', + }); + } + }, [isOpen]); + + if (!isOpen) return null; + + const handleSave = () => { + if (!formData.name) return toast.error('Name is required'); + if (!formData.startDate || !formData.endDate) return toast.error('Dates are required'); + + const newPromo: Promotion = { + id: `pr${Date.now()}`, + name: formData.name, + type: formData.type as 'percentage' | 'fixed' | 'buy_x_get_y', + value: formData.value || 0, + description: `${formData.type === 'percentage' ? formData.value + '% off' : 'Special promo'}`, + applyTo: formData.applyTo || 'All', + startDate: formData.startDate, + endDate: formData.endDate, + status: formData.status as 'active' | 'scheduled' | 'expired', + usageCount: 0, + icon: formData.icon || '🏷️', + }; + + onSave(newPromo); + toast.success('Promotion created successfully!'); + onClose(); + }; + + const promoTypes = [ + { id: 'percentage', label: 'Percentage Off', icon: , desc: 'e.g. 10% off all items', emoji: '🏷️' }, + { id: 'buy_x_get_y', label: 'Buy X Get Y', icon: , desc: 'e.g. Buy 2 Get 1 Free', emoji: '🎁' }, + { id: 'fixed', label: 'Birthday Special', icon: , desc: 'e.g. ₹100 off on birthdays', emoji: '🎂' }, + { id: 'scheduled', label: 'Scheduled/Seasonal', icon: , desc: 'e.g. Happy Hour discounts', emoji: '⏰' }, + ]; + + return ( +
+
+ +
+
+

Create Promotion

+ +
+ +
+ + {/* STEP 1 */} + {step === 1 && ( +
+

Step 1: Basics

+ setFormData({ ...formData, name: e.target.value })} + /> + +
+ +
+ {promoTypes.map(t => ( +
setFormData({ ...formData, type: t.id, icon: t.emoji })} + className={`p-4 rounded-xl border cursor-pointer transition-all flex flex-col items-start gap-2 ${ + formData.type === t.id + ? 'border-primary bg-primary/5 shadow-sm' + : 'border-gray-200 ' + }`} + > + {t.icon} +
{t.label}
+
{t.desc}
+
+ ))} +
+
+
+ )} + + {/* STEP 2 */} + {step === 2 && ( +
+

Step 2: Rules

+ + {formData.type === 'percentage' && ( + <> + setFormData({ ...formData, value: Number(e.target.value) })} /> + + +
+ )} + + {(formData.type === 'fixed' || formData.type === 'scheduled') && ( + <> + setFormData({ ...formData, value: Number(e.target.value) })} /> + + + )} +
+ )} + + {/* STEP 3 */} + {step === 3 && ( +
+

Step 3: Schedule & Limits

+ +
+ setFormData({ ...formData, startDate: e.target.value })} /> + setFormData({ ...formData, endDate: e.target.value })} /> +
+ + + + setStatusFilter(e.target.value)} + options={[ + { value: 'all', label: 'All Statuses' }, + { value: 'active', label: 'Active' }, + { value: 'scheduled', label: 'Scheduled' }, + { value: 'expired', label: 'Expired' }, + ]} + /> +
+ +
+ +
+
+ {filteredPromos.map(p => { + const opacity = p.status === 'expired' ? 'opacity-60 grayscale' : 'opacity-100'; + return ( +
+
+
+
+ {p.icon} +
+
+

{p.name}

+
{getTypeBadge(p.type)}
+
+
+ {getStatusBadge(p.status)} +
+ +

{p.description}

+ +
+
+
Valid Until
+
{new Date(p.endDate).toLocaleDateString()}
+
+
+
Times Used
+
{p.usageCount}
+
+
+ +
+ + +
+
+ ); + })} + + {filteredPromos.length === 0 && ( +
+ +

No promotions found

+

Try adjusting your search or filters.

+
+ )} +
+
+ +
+ + setIsDrawerOpen(false)} + onSave={handleSavePromo} + /> +
+ ); +} diff --git a/src/pages/reports/ReportsPage.tsx b/src/pages/reports/ReportsPage.tsx new file mode 100644 index 0000000..40b1495 --- /dev/null +++ b/src/pages/reports/ReportsPage.tsx @@ -0,0 +1,255 @@ +import { useState, useMemo } from 'react'; +import { Card, StatCard, Button, Table, Badge } from '@/components/ui'; +import { sales } from '@/data/sales'; +import { dashboardData } from '@/data/dashboard'; +import { formatCurrency } from '@/lib/utils'; +import { BarChart3, PackageOpen, FileText, Landmark, TrendingUp, Users, Download, Printer } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function ReportsPage() { + const [activeReport, setActiveReport] = useState('sales'); + const [dateRange, setDateRange] = useState('today'); + + const reportTypes = [ + { id: 'sales', label: 'Sales Report', icon: , active: true }, + { id: 'inventory', label: 'Inventory Report', icon: , active: false }, + { id: 'zreport', label: 'EOD Z-Report', icon: , active: true, highlight: true }, + { id: 'gst', label: 'GST Tax Report', icon: , active: false }, + { id: 'profit', label: 'Profit & Margin', icon: , active: false }, + { id: 'staff', label: 'Staff Performance', icon: , active: false }, + ]; + + // Sales calculations + const totalRevenue = dashboardData.todayStats.sales; + const totalTransactions = dashboardData.todayStats.transactions; + const gstCollected = totalRevenue * 0.18; // mockup + const totalRefunds = 450; // mockup + + const cashSales = 8500; + const cardSales = 12000; + const upiSales = totalRevenue - cashSales - cardSales; + + const renderSalesChart = () => { + const maxVal = Math.max(...dashboardData.weeklySales.map(s => s.amount)); + return ( +
+ {dashboardData.weeklySales.map((day, idx) => { + const heightPct = (day.amount / maxVal) * 100; + return ( +
+
+ {/* Tooltip */} +
+ {day.day}: {formatCurrency(day.amount)} +
+ {/* Bar */} +
+
+ {day.day} +
+ ); + })} +
+ ); + }; + + const renderSalesReport = () => ( +
+
+
+ {['Today', 'Yesterday', 'This Week', 'This Month'].map(range => ( + + ))} +
+
+ + +
+
+ +
+ } iconBg="bg-blue-100 text-blue-600" /> + } iconBg="bg-green-100 text-green-600" /> + } iconBg="bg-amber-100 text-amber-600" /> + } iconBg="bg-red-100 text-red-600" /> +
+ +
+ +

Revenue Trend

+ {renderSalesChart()} +
+ + +
+ {[ + { label: 'UPI', amount: upiSales, color: 'bg-orange-500' }, + { label: 'Card', amount: cardSales, color: 'bg-blue-500' }, + { label: 'Cash', amount: cashSales, color: 'bg-green-500' }, + ].map(method => { + const pct = (method.amount / totalRevenue) * 100; + return ( +
+
+ {method.label} + {formatCurrency(method.amount)} ({pct.toFixed(1)}%) +
+
+
+
+
+ ); + })} +
+ +
+ + +
{d.date} }, + { key: 'trans', label: 'Transactions' }, + { key: 'cash', label: 'Cash', render: d => {formatCurrency(d.cash)} }, + { key: 'card', label: 'Card', render: d => {formatCurrency(d.card)} }, + { key: 'upi', label: 'UPI', render: d => {formatCurrency(d.upi)} }, + { key: 'gst', label: 'GST (18%)', render: d => {formatCurrency(d.gst)} }, + { key: 'total', label: 'Total Revenue', render: d => {formatCurrency(d.total)} }, + ]} + /> + + + ); + + const renderZReport = () => ( +
+
+
+

EOD Z-Report

+

Nearle Daily POS • 26 Jun 2026, 10:30 PM

+
+ +
+
+

Register Summary

+
+ Opening Float + ₹2,000.00 +
+
+ Cash Sales + {formatCurrency(cashSales)} +
+
+ Card Sales + {formatCurrency(cardSales)} +
+
+ UPI Sales + {formatCurrency(upiSales)} +
+
+ +
+

Revenue Breakdown

+
+ Gross Sales + {formatCurrency(totalRevenue)} +
+
+ GST Collected + {formatCurrency(gstCollected)} +
+
+ Discounts Given + -₹120.00 +
+
+ Refunds + -₹{totalRefunds.toFixed(2)} +
+
+ +
+ Total Net Revenue + {formatCurrency(totalRevenue - totalRefunds)} +
+ +
+ Expected Cash in Drawer + {formatCurrency(2000 + cashSales)} +
+
+ +
+ +
+
+
+ ); + + return ( +
+ + {/* REPORTS MENU GRID */} +
+ {reportTypes.map(rt => { + const isSelected = activeReport === rt.id; + return ( +
setActiveReport(rt.id)} + className={`p-4 rounded-xl border flex flex-col items-center justify-center gap-3 text-center cursor-pointer transition-all active:scale-[0.97] select-none touch-manipulation ${ + isSelected + ? 'border-primary bg-primary/5 shadow-sm text-primary' + : 'border-gray-200 bg-white text-gray-600 ' + } ${rt.highlight && !isSelected ? 'border-blue-300 bg-blue-50/50' : ''}`} + > +
+ {rt.icon} +
+
+
{rt.label}
+ {!rt.active &&
Coming Soon
} +
+
+ ) + })} +
+ + {/* REPORT CONTENT */} +
+ {activeReport === 'sales' && renderSalesReport()} + {activeReport === 'zreport' && renderZReport()} + + {reportTypes.find(rt => rt.id === activeReport && !rt.active) && ( +
+
+ +
+

Under Construction

+

The {reportTypes.find(rt => rt.id === activeReport)?.label} module is currently being built and will be available in the next major update.

+
+ )} +
+ +
+ ); +} diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx new file mode 100644 index 0000000..f314eb3 --- /dev/null +++ b/src/pages/settings/SettingsPage.tsx @@ -0,0 +1,254 @@ +import { useState } from 'react'; +import { Card, Input, Button, Select, Badge, Table, Modal } from '@/components/ui'; +import { Store, Users, Printer, FileText, CreditCard, Landmark, Bell, UploadCloud } from 'lucide-react'; +import toast from 'react-hot-toast'; + +export default function SettingsPage() { + const [activeTab, setActiveTab] = useState('store'); + const [isEditUserOpen, setIsEditUserOpen] = useState(false); + + const tabs = [ + { id: 'store', label: 'Store Profile', icon: }, + { id: 'users', label: 'Users & Roles', icon: }, + { id: 'hardware', label: 'Hardware', icon: }, + { id: 'receipt', label: 'Receipt Template', icon: }, + { id: 'payment', label: 'Payment Methods', icon: }, + { id: 'tax', label: 'Tax Settings', icon: }, + { id: 'notifications', label: 'Notifications', icon: }, + ]; + + const handleSave = () => toast.success('Changes saved successfully'); + + const renderStoreProfile = () => ( +
+

Store Profile

+ +
+
+ + Upload Logo +
+
+ + +
+ +