diff --git a/.env b/.env
new file mode 100644
index 0000000..e07617f
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_API_BASE_URL=https://parche.montech.ir/server/api
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e07617f
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+VITE_API_BASE_URL=https://parche.montech.ir/server/api
diff --git a/env.d.ts b/env.d.ts
index 11f02fe..a1b07af 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -1 +1,2 @@
///
+///
diff --git a/package-lock.json b/package-lock.json
index a3b0773..6367dd5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,13 +8,18 @@
"name": "fabric-trade",
"version": "0.0.0",
"dependencies": {
+ "@unocss/vite": "^66.7.5",
+ "@vueuse/core": "^14.4.0",
"jalaali-js": "^2.0.0",
+ "lodash-es": "^4.18.1",
"pinia": "^4.0.2",
+ "unocss": "^66.7.5",
"vue": "^3.5.40",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
+ "@types/lodash-es": "^4.17.12",
"@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue-jsx": "^5.1.6",
@@ -38,6 +43,19 @@
"node": "^22.18.0 || >=24.12.0"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -687,6 +705,23 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.4.tgz",
+ "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -736,7 +771,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz",
"integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -792,6 +826,383 @@
"node": ">= 8"
}
},
+ "node_modules/@oxc-parser/binding-android-arm-eabi": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.131.0.tgz",
+ "integrity": "sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-android-arm64": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.131.0.tgz",
+ "integrity": "sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-darwin-arm64": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.131.0.tgz",
+ "integrity": "sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-darwin-x64": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.131.0.tgz",
+ "integrity": "sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-freebsd-x64": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.131.0.tgz",
+ "integrity": "sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.131.0.tgz",
+ "integrity": "sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.131.0.tgz",
+ "integrity": "sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-arm64-gnu": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.131.0.tgz",
+ "integrity": "sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-arm64-musl": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.131.0.tgz",
+ "integrity": "sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.131.0.tgz",
+ "integrity": "sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.131.0.tgz",
+ "integrity": "sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-riscv64-musl": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.131.0.tgz",
+ "integrity": "sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-s390x-gnu": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.131.0.tgz",
+ "integrity": "sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-x64-gnu": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.131.0.tgz",
+ "integrity": "sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-x64-musl": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.131.0.tgz",
+ "integrity": "sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-openharmony-arm64": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.131.0.tgz",
+ "integrity": "sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.131.0.tgz",
+ "integrity": "sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-win32-arm64-msvc": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.131.0.tgz",
+ "integrity": "sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-win32-ia32-msvc": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.131.0.tgz",
+ "integrity": "sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-win32-x64-msvc": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.131.0.tgz",
+ "integrity": "sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.142.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
@@ -1129,7 +1540,34 @@
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
- "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@quansync/fs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/@quansync/fs/-/fs-1.0.0.tgz",
+ "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==",
+ "license": "MIT",
+ "dependencies": {
+ "quansync": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/@quansync/fs/node_modules/quansync": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/quansync/-/quansync-1.0.0.tgz",
+ "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
@@ -1404,7 +1842,6 @@
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1422,7 +1859,6 @@
"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/jsesc": {
@@ -1438,6 +1874,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz",
+ "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
@@ -1448,6 +1901,12 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
@@ -1704,6 +2163,301 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@unocss/cli": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/cli/-/cli-66.7.5.tgz",
+ "integrity": "sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "@unocss/config": "66.7.5",
+ "@unocss/core": "66.7.5",
+ "@unocss/preset-wind3": "66.7.5",
+ "@unocss/preset-wind4": "66.7.5",
+ "@unocss/transformer-directives": "66.7.5",
+ "cac": "^7.0.0",
+ "chokidar": "^5.0.0",
+ "colorette": "^2.0.20",
+ "consola": "^3.4.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^2.1.0",
+ "tinyglobby": "^0.2.17",
+ "unplugin-utils": "^0.3.1"
+ },
+ "bin": {
+ "unocss": "bin/unocss.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/config": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-66.7.5.tgz",
+ "integrity": "sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "colorette": "^2.0.20",
+ "consola": "^3.4.2",
+ "unconfig": "^7.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/core": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-66.7.5.tgz",
+ "integrity": "sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/extractor-arbitrary-variants": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.7.5.tgz",
+ "integrity": "sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/inspector": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/inspector/-/inspector-66.7.5.tgz",
+ "integrity": "sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/rule-utils": "66.7.5",
+ "colorette": "^2.0.20",
+ "gzip-size": "^6.0.0",
+ "sirv": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-attributify": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-attributify/-/preset-attributify-66.7.5.tgz",
+ "integrity": "sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-icons": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-icons/-/preset-icons-66.7.5.tgz",
+ "integrity": "sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/utils": "^3.1.3",
+ "@unocss/core": "66.7.5",
+ "ofetch": "^1.5.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-mini": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-mini/-/preset-mini-66.7.5.tgz",
+ "integrity": "sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/extractor-arbitrary-variants": "66.7.5",
+ "@unocss/rule-utils": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-tagify": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-tagify/-/preset-tagify-66.7.5.tgz",
+ "integrity": "sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-typography": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-typography/-/preset-typography-66.7.5.tgz",
+ "integrity": "sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/rule-utils": "66.7.5"
+ }
+ },
+ "node_modules/@unocss/preset-uno": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-uno/-/preset-uno-66.7.5.tgz",
+ "integrity": "sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/preset-wind3": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-web-fonts": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-web-fonts/-/preset-web-fonts-66.7.5.tgz",
+ "integrity": "sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "ofetch": "^1.5.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-wind/-/preset-wind-66.7.5.tgz",
+ "integrity": "sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/preset-wind3": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind3": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-wind3/-/preset-wind3-66.7.5.tgz",
+ "integrity": "sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/preset-mini": "66.7.5",
+ "@unocss/rule-utils": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind4": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-wind4/-/preset-wind4-66.7.5.tgz",
+ "integrity": "sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/extractor-arbitrary-variants": "66.7.5",
+ "@unocss/rule-utils": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/rule-utils": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-66.7.5.tgz",
+ "integrity": "sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-attributify-jsx": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.7.5.tgz",
+ "integrity": "sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "oxc-parser": "^0.131.0",
+ "oxc-walker": "^0.7.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-compile-class": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-compile-class/-/transformer-compile-class-66.7.5.tgz",
+ "integrity": "sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-directives": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-directives/-/transformer-directives-66.7.5.tgz",
+ "integrity": "sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5",
+ "@unocss/rule-utils": "66.7.5",
+ "css-tree": "^3.2.1"
+ }
+ },
+ "node_modules/@unocss/transformer-variant-group": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-variant-group/-/transformer-variant-group-66.7.5.tgz",
+ "integrity": "sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/vite": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/vite/-/vite-66.7.5.tgz",
+ "integrity": "sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "@unocss/config": "66.7.5",
+ "@unocss/core": "66.7.5",
+ "@unocss/inspector": "66.7.5",
+ "chokidar": "^5.0.0",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3",
+ "tinyglobby": "^0.2.17",
+ "unplugin-utils": "^0.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0"
+ }
+ },
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
@@ -2067,6 +2821,44 @@
}
}
},
+ "node_modules/@vueuse/core": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.4.0.tgz",
+ "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.4.0",
+ "@vueuse/shared": "14.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.4.0.tgz",
+ "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.4.0.tgz",
+ "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
@@ -2284,6 +3076,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/cac": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/cac/-/cac-7.0.0.tgz",
+ "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001806",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
@@ -2320,12 +3121,27 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "license": "MIT"
+ },
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"license": "MIT"
},
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2348,6 +3164,19 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2435,6 +3264,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2445,6 +3286,12 @@
"node": ">=8"
}
},
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.399",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz",
@@ -2944,6 +3791,21 @@
"node": ">=10.13.0"
}
},
+ "node_modules/gzip-size": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz",
+ "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -2960,6 +3822,16 @@
"node": ">= 4"
}
},
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -3087,7 +3959,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -3487,6 +4358,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3497,6 +4374,57 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-regexp": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/magic-regexp/-/magic-regexp-0.10.0.tgz",
+ "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==",
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12",
+ "mlly": "^1.7.2",
+ "regexp-tree": "^0.1.27",
+ "type-level-regexp": "~0.1.17",
+ "ufo": "^1.5.4",
+ "unplugin": "^2.0.0"
+ }
+ },
+ "node_modules/magic-regexp/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/magic-regexp/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/magic-regexp/node_modules/unplugin": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.11.tgz",
+ "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "acorn": "^8.15.0",
+ "picomatch": "^4.0.3",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3521,6 +4449,12 @@
"url": "https://github.com/sponsors/sxzz"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "license": "CC0-1.0"
+ },
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@@ -3603,7 +4537,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3647,6 +4580,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "license": "MIT"
+ },
"node_modules/node-releases": {
"version": "2.0.51",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
@@ -3766,6 +4705,17 @@
"node": ">=12.20.0"
}
},
+ "node_modules/ofetch": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz",
+ "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
+ "license": "MIT",
+ "dependencies": {
+ "destr": "^2.0.5",
+ "node-fetch-native": "^1.6.7",
+ "ufo": "^1.6.1"
+ }
+ },
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
@@ -3812,6 +4762,64 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/oxc-parser": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/oxc-parser/-/oxc-parser-0.131.0.tgz",
+ "integrity": "sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "^0.131.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxc-parser/binding-android-arm-eabi": "0.131.0",
+ "@oxc-parser/binding-android-arm64": "0.131.0",
+ "@oxc-parser/binding-darwin-arm64": "0.131.0",
+ "@oxc-parser/binding-darwin-x64": "0.131.0",
+ "@oxc-parser/binding-freebsd-x64": "0.131.0",
+ "@oxc-parser/binding-linux-arm-gnueabihf": "0.131.0",
+ "@oxc-parser/binding-linux-arm-musleabihf": "0.131.0",
+ "@oxc-parser/binding-linux-arm64-gnu": "0.131.0",
+ "@oxc-parser/binding-linux-arm64-musl": "0.131.0",
+ "@oxc-parser/binding-linux-ppc64-gnu": "0.131.0",
+ "@oxc-parser/binding-linux-riscv64-gnu": "0.131.0",
+ "@oxc-parser/binding-linux-riscv64-musl": "0.131.0",
+ "@oxc-parser/binding-linux-s390x-gnu": "0.131.0",
+ "@oxc-parser/binding-linux-x64-gnu": "0.131.0",
+ "@oxc-parser/binding-linux-x64-musl": "0.131.0",
+ "@oxc-parser/binding-openharmony-arm64": "0.131.0",
+ "@oxc-parser/binding-wasm32-wasi": "0.131.0",
+ "@oxc-parser/binding-win32-arm64-msvc": "0.131.0",
+ "@oxc-parser/binding-win32-ia32-msvc": "0.131.0",
+ "@oxc-parser/binding-win32-x64-msvc": "0.131.0"
+ }
+ },
+ "node_modules/oxc-parser/node_modules/@oxc-project/types": {
+ "version": "0.131.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.131.0.tgz",
+ "integrity": "sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/oxc-walker": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmmirror.com/oxc-walker/-/oxc-walker-0.7.0.tgz",
+ "integrity": "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==",
+ "license": "MIT",
+ "dependencies": {
+ "magic-regexp": "^0.10.0"
+ },
+ "peerDependencies": {
+ "oxc-parser": ">=0.98.0"
+ }
+ },
"node_modules/oxlint": {
"version": "1.74.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.74.0.tgz",
@@ -3893,6 +4901,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-manager-detector": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
+ "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
+ "license": "MIT"
+ },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -4155,6 +5169,15 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/regexp-tree": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmmirror.com/regexp-tree/-/regexp-tree-0.1.27.tgz",
+ "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+ "license": "MIT",
+ "bin": {
+ "regexp-tree": "bin/regexp-tree"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -4293,7 +5316,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
@@ -4313,6 +5335,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -4358,7 +5389,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4381,7 +5411,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -4398,6 +5427,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-level-regexp": {
+ "version": "0.1.17",
+ "resolved": "https://registry.npmmirror.com/type-level-regexp/-/type-level-regexp-0.1.17.tgz",
+ "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==",
+ "license": "MIT"
+ },
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
@@ -4442,6 +5477,67 @@
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"license": "MIT"
},
+ "node_modules/unconfig": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-7.5.0.tgz",
+ "integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@quansync/fs": "^1.0.0",
+ "defu": "^6.1.4",
+ "jiti": "^2.6.1",
+ "quansync": "^1.0.0",
+ "unconfig-core": "7.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unconfig-core": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmmirror.com/unconfig-core/-/unconfig-core-7.5.0.tgz",
+ "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@quansync/fs": "^1.0.0",
+ "quansync": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unconfig-core/node_modules/quansync": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/quansync/-/quansync-1.0.0.tgz",
+ "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/unconfig/node_modules/quansync": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/quansync/-/quansync-1.0.0.tgz",
+ "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -4449,6 +5545,50 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unocss": {
+ "version": "66.7.5",
+ "resolved": "https://registry.npmmirror.com/unocss/-/unocss-66.7.5.tgz",
+ "integrity": "sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/cli": "66.7.5",
+ "@unocss/core": "66.7.5",
+ "@unocss/preset-attributify": "66.7.5",
+ "@unocss/preset-icons": "66.7.5",
+ "@unocss/preset-mini": "66.7.5",
+ "@unocss/preset-tagify": "66.7.5",
+ "@unocss/preset-typography": "66.7.5",
+ "@unocss/preset-uno": "66.7.5",
+ "@unocss/preset-web-fonts": "66.7.5",
+ "@unocss/preset-wind": "66.7.5",
+ "@unocss/preset-wind3": "66.7.5",
+ "@unocss/preset-wind4": "66.7.5",
+ "@unocss/transformer-attributify-jsx": "66.7.5",
+ "@unocss/transformer-compile-class": "66.7.5",
+ "@unocss/transformer-directives": "66.7.5",
+ "@unocss/transformer-variant-group": "66.7.5",
+ "@unocss/vite": "66.7.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@unocss/astro": "66.7.5",
+ "@unocss/postcss": "66.7.5",
+ "@unocss/webpack": "66.7.5"
+ },
+ "peerDependenciesMeta": {
+ "@unocss/astro": {
+ "optional": true
+ },
+ "@unocss/postcss": {
+ "optional": true
+ },
+ "@unocss/webpack": {
+ "optional": true
+ }
+ }
+ },
"node_modules/unplugin": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
diff --git a/package.json b/package.json
index a91e206..72ae649 100644
--- a/package.json
+++ b/package.json
@@ -15,13 +15,18 @@
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
+ "@unocss/vite": "^66.7.5",
+ "@vueuse/core": "^14.4.0",
"jalaali-js": "^2.0.0",
+ "lodash-es": "^4.18.1",
"pinia": "^4.0.2",
+ "unocss": "^66.7.5",
"vue": "^3.5.40",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
+ "@types/lodash-es": "^4.17.12",
"@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue-jsx": "^5.1.6",
diff --git a/src/App.vue b/src/App.vue
index fff0c69..8556166 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,26 +1,59 @@
-
+
+
+
+
+
+
+
diff --git a/src/assets/styles/app-shell.css b/src/assets/styles/app-shell.css
index 2c69a5a..b9a235a 100644
--- a/src/assets/styles/app-shell.css
+++ b/src/assets/styles/app-shell.css
@@ -20,6 +20,7 @@
width: 100%;
height: 100%;
background: var(--color-bg);
+ color: var(--color-text);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -52,6 +53,16 @@
max-width: var(--content-max-width);
margin-inline: auto;
padding-inline: var(--page-padding);
+ position: relative;
+}
+
+.page-transition {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ overflow: hidden;
}
@media (min-width: 768px) {
diff --git a/src/assets/styles/main.css b/src/assets/styles/main.css
index 58dcffa..1caef9b 100644
--- a/src/assets/styles/main.css
+++ b/src/assets/styles/main.css
@@ -27,6 +27,10 @@
--color-tab-inactive-bg: #f1f5f9;
--color-tab-inactive-text: #64748b;
--color-summary-bg: #f1f5f9;
+ --color-elevated: #ffffff;
+ --color-brand-warm: #f3ece3;
+ --color-brand-warm-text: #1a202c;
+ --color-brand-warm-muted: #718096;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
@@ -52,6 +56,45 @@
--desktop-breakpoint: 1200px;
}
+html.dark {
+ --color-primary: #7b9cff;
+ --color-primary-hover: #9bb4ff;
+ --color-primary-soft: #1b2740;
+ --color-primary-dark: #9bb4ff;
+ --color-primary-light: #1b2740;
+ --color-success: #3dd68c;
+ --color-success-light: #123528;
+ --color-success-soft: #123528;
+ --color-warning: #f5c14a;
+ --color-warning-light: #3a2f14;
+ --color-danger: #f47979;
+ --color-danger-light: #3f1d22;
+ --color-purple: #b39afa;
+ --color-purple-light: #2a2144;
+ --color-info: #7b9cff;
+ --color-info-light: #1b2740;
+ --color-bg: #0b0f14;
+ --color-page-background: #0b0f14;
+ --color-shell-bg: #080b10;
+ --color-surface: #121821;
+ --color-elevated: #1a2230;
+ --color-border: #2a3444;
+ --color-text: #eef2f8;
+ --color-text-primary: #eef2f8;
+ --color-text-muted: #9aa8bc;
+ --color-text-secondary: #9aa8bc;
+ --color-tab-inactive-bg: #1a2230;
+ --color-tab-inactive-text: #9aa8bc;
+ --color-summary-bg: #1a2230;
+ --color-brand-warm: #2a241c;
+ --color-brand-warm-text: #f3ebe0;
+ --color-brand-warm-muted: #b9a88d;
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
+ --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.35);
+ --shadow-frame: 0 12px 40px rgba(0, 0, 0, 0.5);
+}
+
*,
*::before,
*::after {
@@ -121,7 +164,8 @@ input[type='number'] {
min-height: 0;
overflow: hidden;
padding: 12px var(--page-padding) 0;
- background: #ffffff;
+ background: var(--color-bg);
+ color: var(--color-text);
}
.page__scroll {
@@ -133,7 +177,7 @@ input[type='number'] {
}
.page__scroll--with-nav {
- padding-bottom: 12px;
+ padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
}
.page-title {
@@ -152,11 +196,12 @@ input[type='number'] {
}
.card {
- background: var(--color-surface);
+ background: var(--color-elevated);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
padding: 16px;
box-shadow: var(--shadow-sm);
+ color: var(--color-text);
}
.card + .card {
@@ -296,11 +341,6 @@ input[type='number'] {
}
/* Responsive visibility */
-.mobile-only,
-.mobile-block {
- display: block;
-}
-
.desktop-only {
display: none !important;
}
@@ -348,3 +388,51 @@ input[type='number'] {
padding-bottom: 0;
}
}
+
+/* Page route transitions */
+.page-fade-enter-active,
+.page-fade-leave-active,
+.page-forward-enter-active,
+.page-forward-leave-active,
+.page-back-enter-active,
+.page-back-leave-active {
+ transition:
+ opacity 0.22s ease,
+ transform 0.22s ease;
+}
+
+.page-fade-enter-from,
+.page-fade-leave-to {
+ opacity: 0;
+}
+
+.page-forward-enter-from {
+ opacity: 0;
+ transform: translateX(-18px);
+}
+
+.page-forward-leave-to {
+ opacity: 0;
+ transform: translateX(12px);
+}
+
+.page-back-enter-from {
+ opacity: 0;
+ transform: translateX(18px);
+}
+
+.page-back-leave-to {
+ opacity: 0;
+ transform: translateX(-12px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .page-fade-enter-active,
+ .page-fade-leave-active,
+ .page-forward-enter-active,
+ .page-forward-leave-active,
+ .page-back-enter-active,
+ .page-back-leave-active {
+ transition: none;
+ }
+}
diff --git a/src/assets/styles/responsive.css b/src/assets/styles/responsive.css
index 943f541..f9d77af 100644
--- a/src/assets/styles/responsive.css
+++ b/src/assets/styles/responsive.css
@@ -52,8 +52,8 @@
display: flex;
flex-direction: column;
align-items: center;
- padding-top: 48px;
- padding-bottom: 48px;
+ justify-content: center;
+ padding-block: 48px;
}
.auth-header,
diff --git a/src/components/BasicPacks/BasicIcon.vue b/src/components/BasicPacks/BasicIcon.vue
new file mode 100644
index 0000000..2984530
--- /dev/null
+++ b/src/components/BasicPacks/BasicIcon.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/src/components/BasicPacks/tools/icons/icons.json b/src/components/BasicPacks/tools/icons/icons.json
new file mode 100644
index 0000000..40c5978
--- /dev/null
+++ b/src/components/BasicPacks/tools/icons/icons.json
@@ -0,0 +1,67 @@
+{
+ "vuesax-linear:add": "\n",
+ "vuesax-linear:arrow-down": "\n",
+ "vuesax-linear:arrow-left-1": "\n",
+ "vuesax-linear:arrow-right-1": "\n",
+ "vuesax-linear:bag-2": "\n",
+ "vuesax-linear:bag-cross": "\n",
+ "vuesax-linear:box-1": "\n",
+ "vuesax-linear:box-remove": "\n",
+ "vuesax-linear:box-search": "\n",
+ "vuesax-linear:building": "\n",
+ "vuesax-linear:buildings": "\n",
+ "vuesax-linear:calendar": "\n",
+ "vuesax-linear:call": "\n",
+ "vuesax-linear:card": "\n",
+ "vuesax-linear:card-receive": "\n",
+ "vuesax-linear:category": "\n",
+ "vuesax-linear:chart-2": "\n",
+ "vuesax-linear:clipboard-close": "\n",
+ "vuesax-linear:clock": "\n",
+ "vuesax-linear:close-circle": "\n",
+ "vuesax-linear:cpu": "\n",
+ "vuesax-linear:diamonds": "\n",
+ "vuesax-linear:document-text": "\n",
+ "vuesax-linear:edit-2": "\n",
+ "vuesax-linear:element-3": "\n",
+ "vuesax-linear:eye": "\n",
+ "vuesax-linear:eye-slash": "\n",
+ "vuesax-linear:filter": "\n",
+ "vuesax-linear:flash": "\n",
+ "vuesax-linear:headphone": "\n",
+ "vuesax-linear:home": "\n",
+ "vuesax-linear:info-circle": "\n",
+ "vuesax-linear:lamp-on": "\n",
+ "vuesax-linear:location": "\n",
+ "vuesax-linear:lock": "\n",
+ "vuesax-linear:logout": "\n",
+ "vuesax-linear:menu": "\n",
+ "vuesax-linear:message": "\n",
+ "vuesax-linear:mobile": "\n",
+ "vuesax-linear:money-4": "\n",
+ "vuesax-linear:monitor": "\n",
+ "vuesax-linear:moon": "\n",
+ "vuesax-linear:more": "\n",
+ "vuesax-linear:note-remove": "\n",
+ "vuesax-linear:notification": "\n",
+ "vuesax-linear:pen-add": "\n",
+ "vuesax-linear:people": "\n",
+ "vuesax-linear:personalcard": "\n",
+ "vuesax-linear:printer": "\n",
+ "vuesax-linear:profile-2user": "\n",
+ "vuesax-linear:profile-circle": "\n",
+ "vuesax-linear:receipt-search": "\n",
+ "vuesax-linear:search-normal": "\n",
+ "vuesax-linear:search-status": "\n",
+ "vuesax-linear:setting-2": "\n",
+ "vuesax-linear:shopping-bag": "\n",
+ "vuesax-linear:sms": "\n",
+ "vuesax-linear:sun-1": "\n",
+ "vuesax-linear:tick-circle": "\n",
+ "vuesax-linear:timer-1": "\n",
+ "vuesax-linear:trash": "\n",
+ "vuesax-linear:truck-time": "\n",
+ "vuesax-linear:verify": "\n",
+ "vuesax-linear:wallet": "\n",
+ "vuesax-linear:warning-2": "\n"
+}
diff --git a/src/components/icons/IconBell.vue b/src/components/icons/IconBell.vue
index ddd7c1d..005cc37 100644
--- a/src/components/icons/IconBell.vue
+++ b/src/components/icons/IconBell.vue
@@ -1,20 +1,21 @@
-
+
diff --git a/src/components/icons/IconBolt.vue b/src/components/icons/IconBolt.vue
index c445861..3d5287e 100644
--- a/src/components/icons/IconBolt.vue
+++ b/src/components/icons/IconBolt.vue
@@ -1,14 +1,21 @@
-
+
diff --git a/src/components/icons/IconBuilding.vue b/src/components/icons/IconBuilding.vue
index 4108e66..c360d54 100644
--- a/src/components/icons/IconBuilding.vue
+++ b/src/components/icons/IconBuilding.vue
@@ -1,21 +1,21 @@
-
+
diff --git a/src/components/icons/IconCart.vue b/src/components/icons/IconCart.vue
index 933ac0d..fd5c71f 100644
--- a/src/components/icons/IconCart.vue
+++ b/src/components/icons/IconCart.vue
@@ -1,30 +1,21 @@
-
+
diff --git a/src/components/icons/IconCash.vue b/src/components/icons/IconCash.vue
index 92b8c12..ad70898 100644
--- a/src/components/icons/IconCash.vue
+++ b/src/components/icons/IconCash.vue
@@ -1,26 +1,21 @@
-
+
diff --git a/src/components/icons/IconCashSettlement.vue b/src/components/icons/IconCashSettlement.vue
index 5dc53cf..82e63c8 100644
--- a/src/components/icons/IconCashSettlement.vue
+++ b/src/components/icons/IconCashSettlement.vue
@@ -1,20 +1,21 @@
-
+
diff --git a/src/components/icons/IconCatalog.vue b/src/components/icons/IconCatalog.vue
index 5bc74e6..d3130f9 100644
--- a/src/components/icons/IconCatalog.vue
+++ b/src/components/icons/IconCatalog.vue
@@ -1,19 +1,21 @@
-
+
diff --git a/src/components/icons/IconCheckBook.vue b/src/components/icons/IconCheckBook.vue
index 3dfd43d..7ac5959 100644
--- a/src/components/icons/IconCheckBook.vue
+++ b/src/components/icons/IconCheckBook.vue
@@ -1,19 +1,21 @@
-
+
diff --git a/src/components/icons/IconCheckCircleOutline.vue b/src/components/icons/IconCheckCircleOutline.vue
index a9bad91..a63202d 100644
--- a/src/components/icons/IconCheckCircleOutline.vue
+++ b/src/components/icons/IconCheckCircleOutline.vue
@@ -1,29 +1,21 @@
-
+
diff --git a/src/components/icons/IconChevronBack.vue b/src/components/icons/IconChevronBack.vue
index 45c145a..046b239 100644
--- a/src/components/icons/IconChevronBack.vue
+++ b/src/components/icons/IconChevronBack.vue
@@ -1,28 +1,21 @@
-
+
diff --git a/src/components/icons/IconChevronLeft.vue b/src/components/icons/IconChevronLeft.vue
index 521e9c3..4e17f71 100644
--- a/src/components/icons/IconChevronLeft.vue
+++ b/src/components/icons/IconChevronLeft.vue
@@ -1,28 +1,21 @@
-
+
diff --git a/src/components/icons/IconClipboardEmpty.vue b/src/components/icons/IconClipboardEmpty.vue
index b605030..7eec367 100644
--- a/src/components/icons/IconClipboardEmpty.vue
+++ b/src/components/icons/IconClipboardEmpty.vue
@@ -1,75 +1,21 @@
-
+
-
-
diff --git a/src/components/icons/IconDiamond.vue b/src/components/icons/IconDiamond.vue
index 5ccd3db..59ce446 100644
--- a/src/components/icons/IconDiamond.vue
+++ b/src/components/icons/IconDiamond.vue
@@ -1,19 +1,21 @@
-
+
diff --git a/src/components/icons/IconEditPen.vue b/src/components/icons/IconEditPen.vue
index bda5184..3ced2b6 100644
--- a/src/components/icons/IconEditPen.vue
+++ b/src/components/icons/IconEditPen.vue
@@ -1,28 +1,21 @@
-
+
diff --git a/src/components/icons/IconFamBaftLogo.vue b/src/components/icons/IconFamBaftLogo.vue
index 60b0f1a..8277c73 100644
--- a/src/components/icons/IconFamBaftLogo.vue
+++ b/src/components/icons/IconFamBaftLogo.vue
@@ -4,6 +4,7 @@ withDefaults(defineProps<{ size?: number }>(), { size: 72 })
+
+
diff --git a/src/components/icons/IconFeather.vue b/src/components/icons/IconFeather.vue
index 9e2b591..d59c3b0 100644
--- a/src/components/icons/IconFeather.vue
+++ b/src/components/icons/IconFeather.vue
@@ -1,26 +1,21 @@
-
+
diff --git a/src/components/icons/IconFilter.vue b/src/components/icons/IconFilter.vue
index 6f76880..7c75d7c 100644
--- a/src/components/icons/IconFilter.vue
+++ b/src/components/icons/IconFilter.vue
@@ -1,27 +1,21 @@
-
+
diff --git a/src/components/icons/IconHeadset.vue b/src/components/icons/IconHeadset.vue
index 50d320b..d890af0 100644
--- a/src/components/icons/IconHeadset.vue
+++ b/src/components/icons/IconHeadset.vue
@@ -1,16 +1,21 @@
-
+
diff --git a/src/components/icons/IconHome.vue b/src/components/icons/IconHome.vue
index 563ddb4..e7a9261 100644
--- a/src/components/icons/IconHome.vue
+++ b/src/components/icons/IconHome.vue
@@ -1,27 +1,21 @@
-
+
diff --git a/src/components/icons/IconHourglass.vue b/src/components/icons/IconHourglass.vue
index c74e8bf..73d367b 100644
--- a/src/components/icons/IconHourglass.vue
+++ b/src/components/icons/IconHourglass.vue
@@ -1,28 +1,21 @@
-
+
diff --git a/src/components/icons/IconIdCard.vue b/src/components/icons/IconIdCard.vue
index 4cd9ac0..5721dbb 100644
--- a/src/components/icons/IconIdCard.vue
+++ b/src/components/icons/IconIdCard.vue
@@ -1,11 +1,21 @@
-
+
diff --git a/src/components/icons/IconLayoutGrid.vue b/src/components/icons/IconLayoutGrid.vue
index 5ef52e1..d1cf288 100644
--- a/src/components/icons/IconLayoutGrid.vue
+++ b/src/components/icons/IconLayoutGrid.vue
@@ -1,25 +1,21 @@
-
+
diff --git a/src/components/icons/IconLightbulb.vue b/src/components/icons/IconLightbulb.vue
index dcf03d8..6bb5b91 100644
--- a/src/components/icons/IconLightbulb.vue
+++ b/src/components/icons/IconLightbulb.vue
@@ -1,26 +1,21 @@
-
+
diff --git a/src/components/icons/IconLock.vue b/src/components/icons/IconLock.vue
index 08330e1..3a80b09 100644
--- a/src/components/icons/IconLock.vue
+++ b/src/components/icons/IconLock.vue
@@ -1,15 +1,21 @@
-
+
diff --git a/src/components/icons/IconMail.vue b/src/components/icons/IconMail.vue
index 6a51a63..1893780 100644
--- a/src/components/icons/IconMail.vue
+++ b/src/components/icons/IconMail.vue
@@ -1,10 +1,21 @@
-
+
diff --git a/src/components/icons/IconMapPin.vue b/src/components/icons/IconMapPin.vue
index ed3266b..0ef53e7 100644
--- a/src/components/icons/IconMapPin.vue
+++ b/src/components/icons/IconMapPin.vue
@@ -1,15 +1,21 @@
-
+
diff --git a/src/components/icons/IconMoreVertical.vue b/src/components/icons/IconMoreVertical.vue
index 1342aea..23c5702 100644
--- a/src/components/icons/IconMoreVertical.vue
+++ b/src/components/icons/IconMoreVertical.vue
@@ -1,23 +1,21 @@
-
+
diff --git a/src/components/icons/IconOrders.vue b/src/components/icons/IconOrders.vue
index ae85291..6feeb16 100644
--- a/src/components/icons/IconOrders.vue
+++ b/src/components/icons/IconOrders.vue
@@ -1,41 +1,21 @@
-
+
diff --git a/src/components/icons/IconParLogo.vue b/src/components/icons/IconParLogo.vue
index 652d95f..862ede2 100644
--- a/src/components/icons/IconParLogo.vue
+++ b/src/components/icons/IconParLogo.vue
@@ -4,6 +4,7 @@ withDefaults(defineProps<{ size?: number }>(), { size: 72 })
+
+
diff --git a/src/components/icons/IconPhone.vue b/src/components/icons/IconPhone.vue
index 6106975..e116846 100644
--- a/src/components/icons/IconPhone.vue
+++ b/src/components/icons/IconPhone.vue
@@ -1,14 +1,21 @@
-
+
diff --git a/src/components/icons/IconProfile.vue b/src/components/icons/IconProfile.vue
index 53e6688..426223d 100644
--- a/src/components/icons/IconProfile.vue
+++ b/src/components/icons/IconProfile.vue
@@ -1,28 +1,21 @@
-
+
diff --git a/src/components/icons/IconProgressClock.vue b/src/components/icons/IconProgressClock.vue
index b826761..886d014 100644
--- a/src/components/icons/IconProgressClock.vue
+++ b/src/components/icons/IconProgressClock.vue
@@ -1,29 +1,21 @@
-
+
diff --git a/src/components/icons/IconSearch.vue b/src/components/icons/IconSearch.vue
index a24a679..7341e9b 100644
--- a/src/components/icons/IconSearch.vue
+++ b/src/components/icons/IconSearch.vue
@@ -1,20 +1,21 @@
-
+
diff --git a/src/components/icons/IconSettings.vue b/src/components/icons/IconSettings.vue
index 406b0a3..c07c2e3 100644
--- a/src/components/icons/IconSettings.vue
+++ b/src/components/icons/IconSettings.vue
@@ -1,15 +1,21 @@
-
+
diff --git a/src/components/icons/IconShoppingBag.vue b/src/components/icons/IconShoppingBag.vue
index 1950982..d0d4a99 100644
--- a/src/components/icons/IconShoppingBag.vue
+++ b/src/components/icons/IconShoppingBag.vue
@@ -1,20 +1,21 @@
-
+
diff --git a/src/components/icons/IconSuccessCircle.vue b/src/components/icons/IconSuccessCircle.vue
index b7af4c7..a63202d 100644
--- a/src/components/icons/IconSuccessCircle.vue
+++ b/src/components/icons/IconSuccessCircle.vue
@@ -1,17 +1,21 @@
-
+
diff --git a/src/components/icons/IconTrash.vue b/src/components/icons/IconTrash.vue
index a91b068..5b537e0 100644
--- a/src/components/icons/IconTrash.vue
+++ b/src/components/icons/IconTrash.vue
@@ -1,48 +1,21 @@
-
+
diff --git a/src/components/icons/IconVerified.vue b/src/components/icons/IconVerified.vue
index 504ade9..3b8f918 100644
--- a/src/components/icons/IconVerified.vue
+++ b/src/components/icons/IconVerified.vue
@@ -1,16 +1,21 @@
-
+
diff --git a/src/components/layout/BottomNav.vue b/src/components/layout/BottomNav.vue
index 36181bd..36c6689 100644
--- a/src/components/layout/BottomNav.vue
+++ b/src/components/layout/BottomNav.vue
@@ -10,7 +10,9 @@ const cartStore = useCartStore()
const cartCount = computed(() => cartStore.itemCount)
-function navigate(path: string) {
+function navigate(path: string, event: Event) {
+ const target = event.currentTarget
+ if (target instanceof HTMLElement) target.blur()
router.push(path)
}
@@ -24,13 +26,13 @@ function navigate(path: string) {
class="nav-item"
:class="{ active: isNavItemActive(item.name, item.path, route.path) }"
:aria-current="isNavItemActive(item.name, item.path, route.path) ? 'page' : undefined"
- @click="navigate(item.path)"
+ @click="navigate(item.path, $event)"
>
-
+
+ {{ cartCount }}
{{ item.label }}
- {{ cartCount }}
@@ -38,14 +40,14 @@ function navigate(path: string) {
diff --git a/src/components/layout/PageHeader.vue b/src/components/layout/PageHeader.vue
index aa3dca6..26685da 100644
--- a/src/components/layout/PageHeader.vue
+++ b/src/components/layout/PageHeader.vue
@@ -121,8 +121,8 @@ defineEmits<{
}
.back-btn:hover {
- border-color: #cbd5e1;
- background: #f8fafc;
+ border-color: var(--color-border);
+ background: var(--color-summary-bg);
}
.header-text {
diff --git a/src/components/orders/OrderStatusHero.vue b/src/components/orders/OrderStatusHero.vue
index 79d4ac0..03bfd34 100644
--- a/src/components/orders/OrderStatusHero.vue
+++ b/src/components/orders/OrderStatusHero.vue
@@ -39,7 +39,7 @@ defineProps<{
}
.status-hero--waiting .status-hero__icon {
- background: #eef4ff;
+ background: var(--color-primary-soft);
color: var(--color-primary);
}
diff --git a/src/components/orders/OrderTrackingSummary.vue b/src/components/orders/OrderTrackingSummary.vue
index d258271..eee0b14 100644
--- a/src/components/orders/OrderTrackingSummary.vue
+++ b/src/components/orders/OrderTrackingSummary.vue
@@ -55,7 +55,7 @@ function settlementLabel(type: 'cash' | 'check') {
.tracking-summary {
padding: 16px;
border-radius: 16px;
- background: #ffffff;
+ background: var(--color-elevated);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
}
@@ -67,7 +67,7 @@ function settlementLabel(type: 'cash' | 'check') {
gap: 12px;
padding: 10px 0;
font-size: 0.875rem;
- border-bottom: 1px solid #f1f5f9;
+ border-bottom: 1px solid var(--color-border);
}
.tracking-summary__row:last-child {
diff --git a/src/components/orders/OrderTrackingTimeline.vue b/src/components/orders/OrderTrackingTimeline.vue
index 96f3f81..faf798c 100644
--- a/src/components/orders/OrderTrackingTimeline.vue
+++ b/src/components/orders/OrderTrackingTimeline.vue
@@ -80,7 +80,8 @@ function timestampFor(step: OrderStatus) {
margin: 4px 0;
padding: 12px;
border-radius: 14px;
- background: #eef4ff;
+ background: var(--color-primary-soft);
+ border: 1px solid var(--color-border);
}
.tracking-timeline__marker-wrap {
@@ -94,7 +95,7 @@ function timestampFor(step: OrderStatus) {
top: 22px;
bottom: -10px;
width: 2px;
- background: #e2e8f0;
+ background: var(--color-border);
}
.tracking-timeline__item:last-child .tracking-timeline__line {
@@ -109,8 +110,8 @@ function timestampFor(step: OrderStatus) {
width: 22px;
height: 22px;
border-radius: 50%;
- background: #e2e8f0;
- border: 2px solid #e2e8f0;
+ background: var(--color-border);
+ border: 2px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
@@ -126,12 +127,12 @@ function timestampFor(step: OrderStatus) {
.tracking-timeline__item--current .tracking-timeline__marker {
background: var(--color-primary);
border-color: var(--color-primary);
- box-shadow: 0 0 0 4px rgba(29, 53, 87, 0.12);
+ box-shadow: 0 0 0 4px var(--color-primary-soft);
}
.tracking-timeline__item--pending .tracking-timeline__marker {
- background: #fff;
- border-color: #cbd5e1;
+ background: var(--color-elevated);
+ border-color: var(--color-border);
}
.tracking-timeline__check {
diff --git a/src/components/ui/BaseBadge.vue b/src/components/ui/BaseBadge.vue
index e1e8b49..eff59a0 100644
--- a/src/components/ui/BaseBadge.vue
+++ b/src/components/ui/BaseBadge.vue
@@ -46,7 +46,7 @@ defineProps<{
}
.badge-neutral {
- background: #f1f5f9;
+ background: var(--color-summary-bg);
color: var(--color-text-muted);
}
diff --git a/src/components/ui/BaseInput.vue b/src/components/ui/BaseInput.vue
index fea2b89..334c6f9 100644
--- a/src/components/ui/BaseInput.vue
+++ b/src/components/ui/BaseInput.vue
@@ -1,19 +1,30 @@
@@ -22,7 +33,7 @@ defineEmits<{
{{ error }}
{{ hint }}
@@ -50,6 +66,19 @@ defineEmits<{
transition: border-color 0.15s;
}
+.input--align-start {
+ text-align: start;
+}
+
+.input--align-center {
+ text-align: center;
+ letter-spacing: 0.04em;
+}
+
+.input--align-end {
+ text-align: end;
+}
+
.input:focus {
outline: none;
border-color: var(--color-primary);
diff --git a/src/components/ui/EmptyState.vue b/src/components/ui/EmptyState.vue
index 06583db..dee482b 100644
--- a/src/components/ui/EmptyState.vue
+++ b/src/components/ui/EmptyState.vue
@@ -1,20 +1,64 @@
-
+
-
+
-
- {{ message }}
+
+ {{ resolvedTitle }}
+
+
+ {{ resolvedMessage }}
@@ -29,20 +73,36 @@ defineProps<{
align-items: center;
justify-content: center;
flex: 1;
+ width: 100%;
padding: 48px 24px;
- min-height: 320px;
+ min-height: 280px;
text-align: center;
}
.empty-state__icon {
margin-bottom: 16px;
+ color: var(--color-text-muted);
+ opacity: 0.75;
+}
+
+.empty-state__title {
+ margin: 0 0 8px;
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--color-text);
+ line-height: 1.4;
}
.empty-state__message {
margin: 0 0 24px;
- font-size: 0.9375rem;
+ font-size: 0.875rem;
color: var(--color-text-muted);
line-height: 1.5;
+ max-width: 280px;
+}
+
+.empty-state__message:last-child {
+ margin-bottom: 0;
}
.empty-state__action {
diff --git a/src/components/ui/FilterTabs.vue b/src/components/ui/FilterTabs.vue
index 861e38a..b17da03 100644
--- a/src/components/ui/FilterTabs.vue
+++ b/src/components/ui/FilterTabs.vue
@@ -50,10 +50,10 @@ defineEmits<{
flex-shrink: 0;
height: 36px;
padding: 0 18px;
- border: none;
+ border: 1px solid transparent;
border-radius: 999px;
- background: #f5f5f5;
- color: #64748b;
+ background: var(--color-tab-inactive-bg);
+ color: var(--color-tab-inactive-text);
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
@@ -61,7 +61,8 @@ defineEmits<{
cursor: pointer;
transition:
background 0.15s,
- color 0.15s;
+ color 0.15s,
+ border-color 0.15s;
}
.filter-tabs--equal .filter-tab {
@@ -76,6 +77,7 @@ defineEmits<{
.filter-tab.active {
background: var(--color-primary);
+ border-color: var(--color-primary);
color: #fff;
font-weight: 600;
}
@@ -85,6 +87,7 @@ defineEmits<{
}
.filter-tab:not(.active):hover {
- background: #eceff1;
+ border-color: var(--color-border);
+ color: var(--color-text);
}
diff --git a/src/components/ui/PersianDateInput.vue b/src/components/ui/PersianDateInput.vue
index d999e12..b9743e8 100644
--- a/src/components/ui/PersianDateInput.vue
+++ b/src/components/ui/PersianDateInput.vue
@@ -269,7 +269,7 @@ onBeforeUnmount(() => {
height: 32px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
- background: #fff;
+ background: var(--color-elevated);
color: var(--color-text);
cursor: pointer;
font-size: 1.125rem;
diff --git a/src/components/ui/SearchField.vue b/src/components/ui/SearchField.vue
index 9cddb1e..45496a8 100644
--- a/src/components/ui/SearchField.vue
+++ b/src/components/ui/SearchField.vue
@@ -37,7 +37,7 @@ defineEmits<{
}
.search-field__input::placeholder {
- color: #94a3b8;
+ color: var(--color-text-muted);
}
.search-field__input:focus {
diff --git a/src/composables/useCartPage.ts b/src/composables/useCartPage.ts
index ceca3ba..cafb1ad 100644
--- a/src/composables/useCartPage.ts
+++ b/src/composables/useCartPage.ts
@@ -41,8 +41,8 @@ export function useCartPage() {
router.push('/checkout/settlement')
}
- function createNewInvoice() {
- cartStore.createNewCartForSameCompany()
+ async function createNewInvoice() {
+ await cartStore.createNewCartForSameCompany()
showFullDialog.value = false
const companyId = cartStore.selectedCompanyId || cartStore.activeCart?.companyId
if (companyId) {
diff --git a/src/composables/useOrderList.ts b/src/composables/useOrderList.ts
index ddcbdcd..88f9a46 100644
--- a/src/composables/useOrderList.ts
+++ b/src/composables/useOrderList.ts
@@ -1,8 +1,8 @@
-import { computed, ref, watch } from 'vue'
+import { computed, ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useOrderStore } from '@/stores/order'
import { useAuthStore } from '@/stores/auth'
-import { getCompanyById } from '@/data/mockData'
+import { useCatalogStore } from '@/stores/catalog'
import type { Order } from '@/types'
import {
DELIVERED_STATUSES,
@@ -14,12 +14,19 @@ import {
} from '@/utils/orderStatusGroups'
const DESKTOP_PAGE_SIZE = 5
+const MOBILE_PAGE_SIZE = 5
export function useOrderList() {
const router = useRouter()
const orderStore = useOrderStore()
const authStore = useAuthStore()
+ const catalogStore = useCatalogStore()
const currentPage = ref(1)
+ const mobilePage = ref(1)
+
+ onMounted(async () => {
+ await Promise.all([orderStore.hydrate(), catalogStore.loadCompanies()])
+ })
const tab = computed({
get: () => (router.currentRoute.value.query.tab as string) ?? 'all',
@@ -103,12 +110,50 @@ export function useOrderList() {
return pages
})
+ const mobileTotalPages = computed(() =>
+ Math.max(1, Math.ceil(filteredOrdersMobile.value.length / MOBILE_PAGE_SIZE)),
+ )
+
+ const paginatedOrdersMobile = computed(() => {
+ const start = (mobilePage.value - 1) * MOBILE_PAGE_SIZE
+ return filteredOrdersMobile.value.slice(start, start + MOBILE_PAGE_SIZE)
+ })
+
+ const mobilePaginationRange = computed(() => {
+ if (filteredOrdersMobile.value.length === 0) return { start: 0, end: 0 }
+ const start = (mobilePage.value - 1) * MOBILE_PAGE_SIZE + 1
+ const end = Math.min(mobilePage.value * MOBILE_PAGE_SIZE, filteredOrdersMobile.value.length)
+ return { start, end }
+ })
+
+ const mobileVisiblePages = computed(() => {
+ const total = mobileTotalPages.value
+ const current = mobilePage.value
+ if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)
+
+ const pages: (number | 'ellipsis')[] = [1]
+ if (current > 3) pages.push('ellipsis')
+
+ const rangeStart = Math.max(2, current - 1)
+ const rangeEnd = Math.min(total - 1, current + 1)
+ for (let p = rangeStart; p <= rangeEnd; p += 1) pages.push(p)
+
+ if (current < total - 2) pages.push('ellipsis')
+ if (total > 1) pages.push(total)
+
+ return pages
+ })
+
watch([filteredOrdersDesktop, desktopFilter, searchQuery], () => {
currentPage.value = 1
})
+ watch([filteredOrdersMobile, tab, searchQuery], () => {
+ mobilePage.value = 1
+ })
+
function companySlug(order: Order) {
- return getCompanyById(order.companyId)?.slug ?? 'par'
+ return catalogStore.getCompany(order.companyId)?.slug ?? 'par'
}
function iconVariant(order: Order) {
@@ -125,6 +170,11 @@ export function useOrderList() {
currentPage.value = page
}
+ function setMobilePage(page: number) {
+ if (page < 1 || page > mobileTotalPages.value) return
+ mobilePage.value = page
+ }
+
function setDesktopFilter(filter: DesktopOrderFilterId) {
desktopFilter.value = desktopFilter.value === filter && filter !== 'all' ? 'all' : filter
}
@@ -141,14 +191,20 @@ export function useOrderList() {
filteredOrdersMobile,
filteredOrdersDesktop,
paginatedOrders,
+ paginatedOrdersMobile,
totalPages,
+ mobileTotalPages,
paginationRange,
+ mobilePaginationRange,
visiblePages,
+ mobileVisiblePages,
currentPage,
+ mobilePage,
companySlug,
iconVariant,
goToOrder,
setPage,
+ setMobilePage,
setDesktopFilter,
viewStatFilter,
}
diff --git a/src/composables/useProductOrdering.ts b/src/composables/useProductOrdering.ts
index f436e23..51a888d 100644
--- a/src/composables/useProductOrdering.ts
+++ b/src/composables/useProductOrdering.ts
@@ -1,4 +1,4 @@
-import { ref, computed } from 'vue'
+import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCatalogStore } from '@/stores/catalog'
import { useCartStore } from '@/stores/cart'
@@ -8,6 +8,7 @@ export function useProductOrdering() {
const router = useRouter()
const catalogStore = useCatalogStore()
const cartStore = useCartStore()
+ const loading = ref(true)
const companyId = computed(() => route.params.companyId as string)
const catalogId = computed(() => route.params.catalogId as string)
@@ -29,6 +30,27 @@ export function useProductOrdering() {
const errors = ref
>({})
const toast = ref('')
+ async function load() {
+ loading.value = true
+ if (!catalogStore.getCompany(companyId.value)) {
+ await catalogStore.loadCompanies()
+ }
+ if (!catalogStore.getCatalog(catalogId.value)) {
+ await catalogStore.loadCatalogs(companyId.value)
+ }
+ await catalogStore.loadProducts(catalogId.value)
+ loading.value = false
+ }
+
+ onMounted(load)
+ watch([companyId, catalogId], load)
+ watch(colorFilter, async (color) => {
+ await catalogStore.loadProducts(
+ catalogId.value,
+ color === 'all' ? undefined : color,
+ )
+ })
+
function getMeterage(productId: string) {
return meterages.value[productId] ?? 0
}
@@ -38,7 +60,7 @@ export function useProductOrdering() {
meterages.value[productId] = Math.max(0, current + delta * step)
}
- function addProduct(productId: string) {
+ async function addProduct(productId: string) {
const product = catalogStore.getProduct(productId)
if (!product || !catalog.value) return
@@ -48,7 +70,7 @@ export function useProductOrdering() {
return
}
- const result = cartStore.addItem(product, catalog.value.code, meterage)
+ const result = await cartStore.addItem(product, catalog.value.code, meterage)
if (result.success) {
errors.value[productId] = ''
toast.value = `${product.name} به سبد اضافه شد.`
@@ -84,6 +106,7 @@ export function useProductOrdering() {
meterages,
errors,
toast,
+ loading,
cartStore,
adjustMeterage,
setMeterage,
diff --git a/src/main.ts b/src/main.ts
index adea8df..0a34cd0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,6 +3,8 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
+import { useThemeStore } from '@/stores/theme'
+import 'virtual:uno.css'
import './assets/styles/fonts.css'
import './assets/styles/main.css'
import './assets/styles/app-shell.css'
@@ -11,8 +13,11 @@ import './assets/styles/desktop.css'
import './assets/styles/desktop-pages.css'
const app = createApp(App)
+const pinia = createPinia()
-app.use(createPinia())
+app.use(pinia)
app.use(router)
+useThemeStore(pinia)
+
app.mount('#app')
diff --git a/src/router/index.ts b/src/router/index.ts
index 8e1663b..8be3223 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'
-import { getCurrentUser } from '@/services/authService'
+import { isAuthenticated } from '@/services/authService'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -137,13 +137,13 @@ const router = createRouter({
})
router.beforeEach((to) => {
- const isAuthenticated = !!getCurrentUser()
+ const authed = isAuthenticated()
- if (to.meta.requiresAuth && !isAuthenticated) {
+ if (to.meta.requiresAuth && !authed) {
return { name: 'login', query: { redirect: to.fullPath } }
}
- if (to.meta.guest && isAuthenticated) {
+ if (to.meta.guest && authed) {
return { name: 'home' }
}
diff --git a/src/services/authService.ts b/src/services/authService.ts
index 1b59aa6..0236766 100644
--- a/src/services/authService.ts
+++ b/src/services/authService.ts
@@ -1,37 +1,31 @@
-import { MOCK_OTP_CODE } from '@/utils/constants'
-import { generateId, normalizePhone } from '@/utils/format'
-import { getItem, setItem, STORAGE_KEYS } from './storage'
+import { normalizePhone } from '@/utils/format'
+import { apiRequest, ApiError, getAccessToken, setAccessToken } from './http'
import type { User } from '@/types'
-interface PendingOtp {
- phone: string
- firstName?: string
- lastName?: string
- isRegister: boolean
- code: string
- expiresAt: string
+const USER_KEY = 'ft_user'
+
+function cacheUser(user: User | null): void {
+ if (user) localStorage.setItem(USER_KEY, JSON.stringify(user))
+ else localStorage.removeItem(USER_KEY)
}
-export function getUsers(): User[] {
- return getItem(STORAGE_KEYS.users, [])
-}
-
-function saveUsers(users: User[]): void {
- setItem(STORAGE_KEYS.users, users)
-}
-
-export function getSessionUserId(): string | null {
- return getItem(STORAGE_KEYS.session, null)
-}
-
-export function setSessionUserId(userId: string | null): void {
- setItem(STORAGE_KEYS.session, userId)
+export function getCachedUser(): User | null {
+ try {
+ const raw = localStorage.getItem(USER_KEY)
+ if (!raw) return null
+ return JSON.parse(raw) as User
+ } catch {
+ return null
+ }
}
export function getCurrentUser(): User | null {
- const id = getSessionUserId()
- if (!id) return null
- return getUsers().find((u) => u.id === id) ?? null
+ if (!getAccessToken()) return null
+ return getCachedUser()
+}
+
+export function isAuthenticated(): boolean {
+ return !!getAccessToken()
}
export async function sendOtp(
@@ -43,34 +37,31 @@ export async function sendOtp(
return { success: false, message: 'شماره موبایل معتبر نیست.' }
}
- if (options.isRegister) {
- if (!options.firstName?.trim() || !options.lastName?.trim()) {
- return { success: false, message: 'نام و نام خانوادگی الزامی است.' }
- }
- const exists = getUsers().some((u) => u.phone === normalized)
- if (exists) {
- return { success: false, message: 'این شماره قبلاً ثبت شده است. وارد شوید.' }
- }
- } else {
- const exists = getUsers().some((u) => u.phone === normalized)
- if (!exists) {
- return { success: false, message: 'کاربری با این شماره یافت نشد. ثبتنام کنید.' }
- }
+ if (options.isRegister && (!options.firstName?.trim() || !options.lastName?.trim())) {
+ return { success: false, message: 'نام و نام خانوادگی الزامی است.' }
}
- const pending: PendingOtp = {
- phone: normalized,
- firstName: options.firstName,
- lastName: options.lastName,
- isRegister: options.isRegister,
- code: MOCK_OTP_CODE,
- expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
- }
- setItem(STORAGE_KEYS.pendingOtp, pending)
-
- return {
- success: true,
- message: `کد تأیید ارسال شد. (نسخه آزمایشی: ${MOCK_OTP_CODE})`,
+ try {
+ const res = await apiRequest<{ success: boolean; message: string; debugCode?: string }>(
+ '/auth/otp/request',
+ {
+ method: 'POST',
+ auth: false,
+ body: {
+ phone: normalized,
+ firstName: options.firstName,
+ lastName: options.lastName,
+ isRegister: options.isRegister,
+ },
+ },
+ )
+ const hint = res.debugCode ? ` (کد آزمایشی: ${res.debugCode})` : ''
+ return { success: true, message: `${res.message}${hint}` }
+ } catch (e) {
+ return {
+ success: false,
+ message: e instanceof ApiError ? e.message : 'ارسال کد ناموفق بود.',
+ }
}
}
@@ -78,53 +69,55 @@ export async function verifyOtp(
phone: string,
code: string,
): Promise<{ success: boolean; message: string; user?: User }> {
- const pending = getItem(STORAGE_KEYS.pendingOtp, null)
const normalized = normalizePhone(phone)
-
- if (!pending || pending.phone !== normalized) {
- return { success: false, message: 'ابتدا درخواست کد دهید.' }
- }
- if (new Date(pending.expiresAt) < new Date()) {
- return { success: false, message: 'کد منقضی شده است. دوباره درخواست دهید.' }
- }
- if (code.trim() !== pending.code) {
- return { success: false, message: 'کد تأیید نادرست است.' }
- }
-
- let user: User
- const users = getUsers()
-
- if (pending.isRegister) {
- user = {
- id: generateId(),
- firstName: pending.firstName!,
- lastName: pending.lastName!,
- phone: normalized,
- createdAt: new Date().toISOString(),
+ try {
+ const res = await apiRequest<{ accessToken: string; user: User }>('/auth/otp/verify', {
+ method: 'POST',
+ auth: false,
+ body: { phone: normalized, code },
+ })
+ setAccessToken(res.accessToken)
+ cacheUser(res.user)
+ return { success: true, message: 'ورود موفق.', user: res.user }
+ } catch (e) {
+ return {
+ success: false,
+ message: e instanceof ApiError ? e.message : 'تأیید کد ناموفق بود.',
}
- users.push(user)
- saveUsers(users)
- } else {
- user = users.find((u) => u.phone === normalized)!
}
-
- setSessionUserId(user.id)
- setItem(STORAGE_KEYS.pendingOtp, null)
- return { success: true, message: 'ورود موفق.', user }
}
export function logout(): void {
- setSessionUserId(null)
+ setAccessToken(null)
+ cacheUser(null)
}
-export function updateProfile(
- userId: string,
+export async function fetchMe(): Promise {
+ if (!getAccessToken()) {
+ cacheUser(null)
+ return null
+ }
+ try {
+ const user = await apiRequest('/auth/me')
+ cacheUser(user)
+ return user
+ } catch {
+ logout()
+ return null
+ }
+}
+
+export async function updateProfile(
data: Partial>,
-): User | null {
- const users = getUsers()
- const index = users.findIndex((u) => u.id === userId)
- if (index === -1) return null
- users[index] = { ...users[index]!, ...data }
- saveUsers(users)
- return users[index]!
+): Promise {
+ try {
+ const user = await apiRequest('/auth/profile', {
+ method: 'PATCH',
+ body: data,
+ })
+ cacheUser(user)
+ return user
+ } catch {
+ return null
+ }
}
diff --git a/src/services/http.ts b/src/services/http.ts
new file mode 100644
index 0000000..fd6820b
--- /dev/null
+++ b/src/services/http.ts
@@ -0,0 +1,83 @@
+const TOKEN_KEY = 'ft_access_token'
+
+export function getApiBaseUrl(): string {
+ return (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, '')
+ || 'https://parche.montech.ir/server/api'
+}
+
+export function getAccessToken(): string | null {
+ return localStorage.getItem(TOKEN_KEY)
+}
+
+export function setAccessToken(token: string | null): void {
+ if (token) localStorage.setItem(TOKEN_KEY, token)
+ else localStorage.removeItem(TOKEN_KEY)
+}
+
+export class ApiError extends Error {
+ status: number
+ constructor(status: number, message: string) {
+ super(message)
+ this.status = status
+ }
+}
+
+type RequestOptions = {
+ method?: string
+ body?: unknown
+ auth?: boolean
+ query?: Record
+}
+
+function buildUrl(path: string, query?: RequestOptions['query']): string {
+ const base = getApiBaseUrl()
+ const url = new URL(path.startsWith('http') ? path : `${base}${path.startsWith('/') ? '' : '/'}${path}`)
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value === undefined || value === null || value === '') continue
+ url.searchParams.set(key, String(value))
+ }
+ }
+ return url.toString()
+}
+
+export async function apiRequest(path: string, options: RequestOptions = {}): Promise {
+ const headers: Record = {
+ Accept: 'application/json',
+ }
+ if (options.body !== undefined) {
+ headers['Content-Type'] = 'application/json'
+ }
+ if (options.auth !== false) {
+ const token = getAccessToken()
+ if (token) headers.Authorization = `Bearer ${token}`
+ }
+
+ const res = await fetch(buildUrl(path, options.query), {
+ method: options.method ?? (options.body !== undefined ? 'POST' : 'GET'),
+ headers,
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
+ })
+
+ const text = await res.text()
+ let data: unknown = null
+ if (text) {
+ try {
+ data = JSON.parse(text)
+ } catch {
+ data = text
+ }
+ }
+
+ if (!res.ok) {
+ const message =
+ typeof data === 'object' && data && 'message' in data
+ ? Array.isArray((data as { message: unknown }).message)
+ ? ((data as { message: string[] }).message).join('، ')
+ : String((data as { message: unknown }).message)
+ : `خطای سرور (${res.status})`
+ throw new ApiError(res.status, message)
+ }
+
+ return data as T
+}
diff --git a/src/stores/auth.ts b/src/stores/auth.ts
index b97a43a..ed700f9 100644
--- a/src/stores/auth.ts
+++ b/src/stores/auth.ts
@@ -7,7 +7,11 @@ import {
sendOtp,
verifyOtp,
updateProfile as authUpdateProfile,
+ fetchMe,
} from '@/services/authService'
+import { useCartStore } from './cart'
+import { useOrderStore } from './order'
+import { useCatalogStore } from './catalog'
export const useAuthStore = defineStore('auth', () => {
const user = ref(getCurrentUser())
@@ -42,6 +46,11 @@ export const useAuthStore = defineStore('auth', () => {
loading.value = false
if (result.success && result.user) {
user.value = result.user
+ await Promise.all([
+ useCatalogStore().loadCompanies(),
+ useCartStore().hydrate(),
+ useOrderStore().hydrate(),
+ ])
} else {
error.value = result.message
}
@@ -51,17 +60,22 @@ export const useAuthStore = defineStore('auth', () => {
function logout() {
authLogout()
user.value = null
+ useCartStore().resetLocal()
+ useOrderStore().resetLocal()
}
- function updateProfile(data: Partial>) {
+ async function updateProfile(data: Partial>) {
if (!user.value) return null
- const updated = authUpdateProfile(user.value.id, data)
+ const updated = await authUpdateProfile(data)
if (updated) user.value = updated
return updated
}
- function hydrate() {
+ async function hydrate() {
user.value = getCurrentUser()
+ if (!user.value) return
+ const me = await fetchMe()
+ user.value = me
}
return {
diff --git a/src/stores/cart.ts b/src/stores/cart.ts
index d78a8da..622075d 100644
--- a/src/stores/cart.ts
+++ b/src/stores/cart.ts
@@ -1,31 +1,22 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
-import type { Cart, CartItem, Company, Product } from '@/types'
-import { generateId } from '@/utils/format'
+import type { Cart, Company, Product } from '@/types'
import { validateMeterage } from '@/utils/validation'
-import { getItem, setItem, STORAGE_KEYS } from '@/services/storage'
-import { getCompanyById } from '@/data/mockData'
+import { apiRequest, ApiError } from '@/services/http'
+import { getAccessToken } from '@/services/http'
+import { useCatalogStore } from './catalog'
const DEFAULT_MAX_METERAGE = 5000
-function loadCarts(): Cart[] {
- return getItem(STORAGE_KEYS.carts, [])
+type ApiCart = Cart & { totalMeterage?: number; itemCount?: number }
+
+function emptyCart(): Cart {
+ return { id: 'local', companyId: '', companyName: '', items: [] }
}
-function saveCarts(carts: Cart[]) {
- setItem(STORAGE_KEYS.carts, carts)
-}
-
-function loadActiveCartId(): string | null {
- return getItem(STORAGE_KEYS.activeCartId, null)
-}
-
-function saveActiveCartId(id: string | null) {
- setItem(STORAGE_KEYS.activeCartId, id)
-}
-
-function getCartTotalMeterage(cart: Cart): number {
- return cart.items.reduce((sum, item) => sum + item.meterage, 0)
+function uniqueCompanyNames(items: Cart['items']): string {
+ const names = [...new Set(items.map((item) => item.companyName).filter(Boolean))]
+ return names.join('، ')
}
function companyMeterage(cart: Cart, companyId: string): number {
@@ -34,35 +25,13 @@ function companyMeterage(cart: Cart, companyId: string): number {
.reduce((sum, item) => sum + item.meterage, 0)
}
-function uniqueCompanyNames(items: CartItem[]): string {
- const names = [...new Set(items.map((item) => item.companyName).filter(Boolean))]
- return names.join('، ')
-}
-
-function syncCartCompanyMeta(cart: Cart) {
- if (cart.items.length === 0) return
- const first = cart.items[0]!
- cart.companyId = first.companyId
- cart.companyName = uniqueCompanyNames(cart.items) || first.companyName
-}
-
-function migrateCartItems(carts: Cart[]): Cart[] {
- return carts.map((cart) => ({
- ...cart,
- items: cart.items.map((item) => ({
- ...item,
- companyId: item.companyId || cart.companyId,
- companyName: item.companyName || cart.companyName,
- })),
- }))
-}
-
export const useCartStore = defineStore('cart', () => {
- const carts = ref(migrateCartItems(loadCarts()))
- const activeCartId = ref(loadActiveCartId())
+ const carts = ref([emptyCart()])
+ const activeCartId = ref('local')
const selectedCompanyId = ref(null)
+ const syncing = ref(false)
- const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value) ?? null)
+ const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value) ?? carts.value[0] ?? null)
const totalMeterage = computed(
() => activeCart.value?.items.reduce((sum, i) => sum + i.meterage, 0) ?? 0,
@@ -75,107 +44,101 @@ export const useCartStore = defineStore('cart', () => {
return uniqueCompanyNames(activeCart.value.items)
})
+ const catalogStore = useCatalogStore()
+
const maxMeterage = computed(() => {
if (!activeCart.value?.items.length) {
const selected = selectedCompanyId.value
- ? getCompanyById(selectedCompanyId.value)
+ ? catalogStore.getCompany(selectedCompanyId.value)
: null
return selected?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
}
const companyIds = [...new Set(activeCart.value.items.map((item) => item.companyId))]
return companyIds.reduce((sum, id) => {
- const company = getCompanyById(id)
+ const company = catalogStore.getCompany(id)
return sum + (company?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE)
}, 0)
})
const remainingCapacity = computed(() => Math.max(0, maxMeterage.value - totalMeterage.value))
-
const isFull = computed(() => totalMeterage.value >= maxMeterage.value)
- function persist() {
- saveCarts(carts.value)
- saveActiveCartId(activeCartId.value)
- }
-
- function createEmptyCart(company?: Company): Cart {
+ function applyServerCart(serverCart: ApiCart) {
const cart: Cart = {
- id: generateId(),
- companyId: company?.id ?? '',
- companyName: company?.name ?? '',
- items: [],
+ id: serverCart.id,
+ companyId: serverCart.companyId || '',
+ companyName: serverCart.companyName || uniqueCompanyNames(serverCart.items),
+ items: serverCart.items ?? [],
+ notes: serverCart.notes,
}
- carts.value.push(cart)
+ carts.value = [cart]
activeCartId.value = cart.id
- if (company) selectedCompanyId.value = company.id
- persist()
- return cart
+ if (cart.companyId) selectedCompanyId.value = cart.companyId
}
- function getOrCreateCart(company?: Company): Cart {
- if (activeCart.value) return activeCart.value
-
- const withItems = carts.value.find((cart) => cart.items.length > 0)
- if (withItems) {
- activeCartId.value = withItems.id
- selectedCompanyId.value = selectedCompanyId.value ?? withItems.companyId
- persist()
- return withItems
+ async function hydrate() {
+ if (!getAccessToken()) {
+ resetLocal()
+ return
}
-
- const open = carts.value.find(
- (cart) => getCartTotalMeterage(cart) < DEFAULT_MAX_METERAGE,
- )
- if (open) {
- activeCartId.value = open.id
- if (company) selectedCompanyId.value = company.id
- persist()
- return open
+ syncing.value = true
+ try {
+ const serverCart = await apiRequest('/carts')
+ applyServerCart(serverCart)
+ } catch {
+ resetLocal()
+ } finally {
+ syncing.value = false
}
-
- return createEmptyCart(company)
}
- /** @deprecated Prefer getOrCreateCart / selectCompany — kept for callers. */
- function initCartForCompany(company: Company): Cart {
- selectedCompanyId.value = company.id
- const cart = getOrCreateCart(company)
- if (!cart.companyId) {
- cart.companyId = company.id
- cart.companyName = company.name
- persist()
- }
- return cart
+ function resetLocal() {
+ carts.value = [emptyCart()]
+ activeCartId.value = carts.value[0]!.id
+ selectedCompanyId.value = null
}
function selectCompany(company: Company): void {
selectedCompanyId.value = company.id
- getOrCreateCart(company)
+ if (activeCart.value && !activeCart.value.items.length) {
+ activeCart.value.companyId = company.id
+ activeCart.value.companyName = company.name
+ }
}
- function createNewCartForSameCompany(): Cart | null {
- const company = selectedCompanyId.value
- ? getCompanyById(selectedCompanyId.value)
- : activeCart.value?.companyId
- ? getCompanyById(activeCart.value.companyId)
- : undefined
- return createEmptyCart(company)
+ function initCartForCompany(company: Company): Cart {
+ selectCompany(company)
+ return activeCart.value ?? emptyCart()
}
- function addItem(
+ function getOrCreateCart(company?: Company): Cart {
+ if (company) selectCompany(company)
+ return activeCart.value ?? emptyCart()
+ }
+
+ async function createNewCartForSameCompany(): Promise {
+ try {
+ const serverCart = await apiRequest('/carts', { method: 'DELETE' })
+ applyServerCart(serverCart)
+ return activeCart.value
+ } catch (e) {
+ console.error(e)
+ return null
+ }
+ }
+
+ async function addItem(
product: Product,
catalogCode: string,
meterage: number,
- ): { success: boolean; message?: string } {
- const company = getCompanyById(product.companyId)
+ ): Promise<{ success: boolean; message?: string }> {
+ const company = catalogStore.getCompany(product.companyId)
if (!company) {
return { success: false, message: 'شرکت محصول یافت نشد.' }
}
const cart = getOrCreateCart(company)
- selectedCompanyId.value = company.id
-
const companyMax = company.maxMeteragePerInvoice
const currentCompanyMeterage = companyMeterage(cart, company.id)
const existing = cart.items.find((i) => i.productId === product.id)
@@ -192,41 +155,40 @@ export const useCartStore = defineStore('cart', () => {
return { success: false, message: validation.message }
}
- if (existing) {
- existing.meterage = meterage
- existing.catalogCode = catalogCode
- existing.companyId = company.id
- existing.companyName = company.name
- } else {
- cart.items.push({
- id: generateId(),
- productId: product.id,
- catalogCode,
- productName: product.name,
- productCode: product.code,
- color: product.color,
- meterage,
- unitPrice: product.basePrice,
- companyId: company.id,
- companyName: company.name,
- })
+ try {
+ let serverCart: ApiCart
+ if (existing) {
+ serverCart = await apiRequest(`/carts/items/${existing.id}`, {
+ method: 'PATCH',
+ body: { meterage },
+ })
+ } else {
+ serverCart = await apiRequest('/carts/items', {
+ method: 'POST',
+ body: { productId: product.id, meterage },
+ })
+ }
+ applyServerCart(serverCart)
+ selectedCompanyId.value = company.id
+ return { success: true }
+ } catch (e) {
+ return {
+ success: false,
+ message: e instanceof ApiError ? e.message : 'افزودن به سبد ناموفق بود.',
+ }
}
-
- syncCartCompanyMeta(cart)
- persist()
- return { success: true }
}
- function updateItemMeterage(
+ async function updateItemMeterage(
itemId: string,
meterage: number,
product: Product,
- ): { success: boolean; message?: string } {
+ ): Promise<{ success: boolean; message?: string }> {
if (!activeCart.value) return { success: false, message: 'سبد خرید یافت نشد.' }
const item = activeCart.value.items.find((i) => i.id === itemId)
if (!item) return { success: false, message: 'آیتم یافت نشد.' }
- const company = getCompanyById(item.companyId || product.companyId)
+ const company = catalogStore.getCompany(item.companyId || product.companyId)
const companyMax = company?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
const currentCompanyMeterage = companyMeterage(activeCart.value, item.companyId)
@@ -241,56 +203,62 @@ export const useCartStore = defineStore('cart', () => {
return { success: false, message: validation.message }
}
- item.meterage = meterage
- persist()
- return { success: true }
+ try {
+ const serverCart = await apiRequest(`/carts/items/${itemId}`, {
+ method: 'PATCH',
+ body: { meterage },
+ })
+ applyServerCart(serverCart)
+ return { success: true }
+ } catch (e) {
+ return {
+ success: false,
+ message: e instanceof ApiError ? e.message : 'بهروزرسانی متراژ ناموفق بود.',
+ }
+ }
}
- function removeItem(itemId: string) {
- if (!activeCart.value) return
- activeCart.value.items = activeCart.value.items.filter((i) => i.id !== itemId)
- syncCartCompanyMeta(activeCart.value)
- persist()
+ async function removeItem(itemId: string) {
+ try {
+ const serverCart = await apiRequest(`/carts/items/${itemId}`, {
+ method: 'DELETE',
+ })
+ applyServerCart(serverCart)
+ } catch (e) {
+ console.error(e)
+ }
}
- function setNotes(notes: string) {
- if (!activeCart.value) return
- activeCart.value.notes = notes
- persist()
+ async function setNotes(notes: string) {
+ try {
+ const serverCart = await apiRequest('/carts/notes', {
+ method: 'PATCH',
+ body: { notes },
+ })
+ applyServerCart(serverCart)
+ } catch (e) {
+ console.error(e)
+ }
}
- function clearActiveCart() {
- if (!activeCart.value) return
- activeCart.value.items = []
- activeCart.value.notes = undefined
- persist()
+ async function clearActiveCart() {
+ try {
+ const serverCart = await apiRequest('/carts', { method: 'DELETE' })
+ applyServerCart(serverCart)
+ } catch {
+ resetLocal()
+ }
}
function ensureActiveCartForCartPage() {
- if (activeCart.value?.items.length) return
-
- const cartWithItems = carts.value.find((cart) => cart.items.length > 0)
- if (!cartWithItems) return
-
- activeCartId.value = cartWithItems.id
- selectedCompanyId.value = selectedCompanyId.value || cartWithItems.companyId
- persist()
- }
-
- function hydrate() {
- carts.value = migrateCartItems(loadCarts())
- activeCartId.value = loadActiveCartId()
- if (activeCart.value) {
- syncCartCompanyMeta(activeCart.value)
- selectedCompanyId.value = selectedCompanyId.value || activeCart.value.companyId
- persist()
- }
+ // server keeps a single cart
}
return {
carts,
activeCartId,
selectedCompanyId,
+ syncing,
activeCart,
totalMeterage,
itemCount,
@@ -308,5 +276,6 @@ export const useCartStore = defineStore('cart', () => {
clearActiveCart,
ensureActiveCartForCartPage,
hydrate,
+ resetLocal,
}
})
diff --git a/src/stores/catalog.ts b/src/stores/catalog.ts
index 8a732fe..d7040e1 100644
--- a/src/stores/catalog.ts
+++ b/src/stores/catalog.ts
@@ -1,38 +1,86 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
-import {
- companies as mockCompanies,
- getCatalogsByCompany,
- getProductsByCatalog,
- getCatalogById,
- getProductById,
- getCompanyById,
-} from '@/data/mockData'
-import type { Catalog, Company, Product } from '@/types'
+import { apiRequest } from '@/services/http'
+import type { BankAccount, Catalog, Company, Product } from '@/types'
export const useCatalogStore = defineStore('catalog', () => {
- const companies = ref(mockCompanies.filter((c) => c.isActive))
+ const companies = ref([])
+ const catalogs = ref([])
+ const products = ref([])
+ const bankAccounts = ref([])
const searchQuery = ref('')
+ const loading = ref(false)
+ const loaded = ref(false)
+
+ async function loadCompanies(force = false) {
+ if (loaded.value && !force) return companies.value
+ loading.value = true
+ try {
+ const list = await apiRequest('/companies', {
+ query: { activeOnly: true },
+ })
+ companies.value = list
+ .filter((c) => c.isActive)
+ .sort((a, b) => a.displayOrder - b.displayOrder)
+ loaded.value = true
+ } finally {
+ loading.value = false
+ }
+ return companies.value
+ }
function getActiveCompanies(): Company[] {
return [...companies.value].sort((a, b) => a.displayOrder - b.displayOrder)
}
function getCompany(id: string): Company | undefined {
- return getCompanyById(id)
+ return companies.value.find((c) => c.id === id)
+ }
+
+ async function loadCatalogs(companyId: string) {
+ const list = await apiRequest('/catalogs', {
+ query: { companyId, activeOnly: true },
+ })
+ catalogs.value = [
+ ...catalogs.value.filter((c) => c.companyId !== companyId),
+ ...list,
+ ]
+ return list
+ }
+
+ async function loadProducts(catalogId: string, color?: string) {
+ const list = await apiRequest('/products', {
+ query: {
+ catalogId,
+ color: color && color !== 'all' ? color : undefined,
+ activeOnly: true,
+ },
+ })
+ const others = products.value.filter((p) => p.catalogId !== catalogId)
+ products.value = [...others, ...list]
+ return list
+ }
+
+ async function loadBankAccounts(companyId: string) {
+ const list = await apiRequest(`/companies/${companyId}/bank-accounts`)
+ bankAccounts.value = [
+ ...bankAccounts.value.filter((b) => b.companyId !== companyId),
+ ...list.filter((b) => b.isActive),
+ ]
+ return bankAccounts.value.filter((b) => b.companyId === companyId)
}
function fetchCatalogs(companyId: string, query = ''): Catalog[] {
- let list = getCatalogsByCompany(companyId)
+ let list = catalogs.value.filter((c) => c.companyId === companyId && c.isActive)
if (query.trim()) {
const q = query.trim()
list = list.filter((c) => c.code.includes(q) || c.name.includes(q))
}
- return list
+ return [...list].sort((a, b) => a.displayOrder - b.displayOrder)
}
function fetchProducts(catalogId: string, colorFilter?: string): Product[] {
- let list = getProductsByCatalog(catalogId)
+ let list = products.value.filter((p) => p.catalogId === catalogId && p.isActive)
if (colorFilter && colorFilter !== 'all') {
list = list.filter((p) => p.color.includes(colorFilter))
}
@@ -40,21 +88,35 @@ export const useCatalogStore = defineStore('catalog', () => {
}
function getCatalog(id: string) {
- return getCatalogById(id)
+ return catalogs.value.find((c) => c.id === id)
}
function getProduct(id: string) {
- return getProductById(id)
+ return products.value.find((p) => p.id === id)
}
function getUniqueColors(catalogId: string): string[] {
- const products = getProductsByCatalog(catalogId)
- return [...new Set(products.map((p) => p.color))]
+ return [...new Set(fetchProducts(catalogId).map((p) => p.color))]
+ }
+
+ function getBankAccountsByCompany(companyId: string): BankAccount[] {
+ return bankAccounts.value
+ .filter((b) => b.companyId === companyId && b.isActive)
+ .sort((a, b) => a.displayOrder - b.displayOrder)
}
return {
companies,
+ catalogs,
+ products,
+ bankAccounts,
searchQuery,
+ loading,
+ loaded,
+ loadCompanies,
+ loadCatalogs,
+ loadProducts,
+ loadBankAccounts,
getActiveCompanies,
getCompany,
fetchCatalogs,
@@ -62,5 +124,6 @@ export const useCatalogStore = defineStore('catalog', () => {
getCatalog,
getProduct,
getUniqueColors,
+ getBankAccountsByCompany,
}
})
diff --git a/src/stores/order.ts b/src/stores/order.ts
index f781edc..939e04f 100644
--- a/src/stores/order.ts
+++ b/src/stores/order.ts
@@ -5,87 +5,29 @@ import type {
CheckData,
CashPaymentReceipt,
Order,
- OrderItem,
- OrderStatus,
- OrderStatusHistoryEntry,
SettlementType,
CheckRegistrationMode,
} from '@/types'
-import { generateId, generateOrderNumber } from '@/utils/format'
+import { generateId } from '@/utils/format'
import { calculateCheckRass } from '@/utils/checkRass'
-import { getItem, setItem, STORAGE_KEYS } from '@/services/storage'
-import { getCompanyById, getBankAccountsByCompany } from '@/data/mockData'
-
-function loadOrders(): Order[] {
- return getItem(STORAGE_KEYS.orders, [])
-}
-
-function saveOrders(orders: Order[]) {
- setItem(STORAGE_KEYS.orders, orders)
-}
-
-function createStatusEntry(
- from: OrderStatus | null,
- to: OrderStatus,
- note?: string,
-): OrderStatusHistoryEntry {
- return {
- id: generateId(),
- fromStatus: from,
- toStatus: to,
- changedAt: new Date().toISOString(),
- changedBy: 'customer',
- note,
- }
-}
-
-function cartToOrderItems(cart: Cart): OrderItem[] {
- return cart.items.map((item) => ({
- id: generateId(),
- productId: item.productId,
- catalogCode: item.catalogCode,
- productName: item.productName,
- productCode: item.productCode,
- color: item.color,
- requestedMeterage: item.meterage,
- unitPrice: item.unitPrice,
- notes: item.notes,
- }))
-}
-
-function computeCashExpiry(companyId: string): string {
- const company = getCompanyById(companyId)
- const now = new Date()
- if (company?.cashInvoiceValidUntilHour) {
- const expiry = new Date(now)
- expiry.setHours(company.cashInvoiceValidUntilHour, 0, 0, 0)
- if (expiry <= now) {
- expiry.setDate(expiry.getDate() + 1)
- }
- return expiry.toISOString()
- }
- const hours = company?.cashInvoiceValidityHours ?? 8
- return new Date(now.getTime() + hours * 60 * 60 * 1000).toISOString()
-}
+import { apiRequest, ApiError, getAccessToken } from '@/services/http'
+import { useCatalogStore } from './catalog'
export const useOrderStore = defineStore('order', () => {
- const orders = ref(loadOrders())
+ const orders = ref([])
const draftChecks = ref([])
const draftSettlementType = ref(null)
const draftCheckMode = ref(null)
const checkoutCartId = ref(null)
+ const loading = ref(false)
const userOrders = computed(
- () => (userId: string) =>
- orders.value
- .filter((o) => o.userId === userId)
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
+ () => (_userId: string) =>
+ [...orders.value].sort(
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
+ ),
)
- function persist() {
- saveOrders(orders.value)
- }
-
function getOrderById(id: string): Order | undefined {
return orders.value.find((o) => o.id === id)
}
@@ -120,108 +62,131 @@ export const useOrderStore = defineStore('order', () => {
return calculateCheckRass(draftChecks.value, baseDate ?? new Date())
}
- function submitOrder(
- userId: string,
- cart: Cart,
- settlementType: SettlementType,
- checks?: CheckData[],
- checkMode?: CheckRegistrationMode,
- ): Order {
- const items = cartToOrderItems(cart)
- const totalMeterage = items.reduce((s, i) => s + i.requestedMeterage, 0)
- const now = new Date().toISOString()
-
- let status: OrderStatus = 'awaiting_review'
- let checkRassDays: number | undefined
- let checkRassDate: string | undefined
- let cashInvoiceExpiresAt: string | undefined
-
- if (settlementType === 'check' && checks && checks.length > 0) {
- const rass = calculateCheckRass(checks, new Date())
- checkRassDays = rass.rassDays
- checkRassDate = rass.rassDate ?? undefined
- status = 'awaiting_review'
+ async function hydrate() {
+ if (!getAccessToken()) {
+ orders.value = []
+ return
}
-
- if (settlementType === 'cash') {
- cashInvoiceExpiresAt = computeCashExpiry(cart.companyId)
- status = 'awaiting_review'
+ loading.value = true
+ try {
+ orders.value = await apiRequest('/orders')
+ } catch {
+ orders.value = []
+ } finally {
+ loading.value = false
}
+ }
- const order: Order = {
- id: generateId(),
- orderNumber: generateOrderNumber(),
- userId,
- companyId: cart.companyId,
- companyName: cart.companyName,
- items,
- totalMeterage,
- settlementType,
- checkMode,
- checks: checks ? [...checks] : undefined,
- checkRassDays,
- checkRassDate,
- status,
- notes: cart.notes,
- cashInvoiceExpiresAt,
- statusHistory: [
- createStatusEntry(null, 'submitted', 'سفارش ثبت شد'),
- createStatusEntry('submitted', status, 'ارسال برای بررسی مدیریت'),
- ],
- createdAt: now,
- updatedAt: now,
- }
-
- orders.value.unshift(order)
- persist()
-
+ function resetLocal() {
+ orders.value = []
draftChecks.value = []
draftSettlementType.value = null
draftCheckMode.value = null
checkoutCartId.value = null
-
- return order
}
- function submitCashReceipt(
+ async function fetchOrder(id: string): Promise {
+ try {
+ const order = await apiRequest(`/orders/${id}`)
+ const idx = orders.value.findIndex((o) => o.id === id)
+ if (idx >= 0) orders.value[idx] = order
+ else orders.value.unshift(order)
+ return order
+ } catch {
+ return null
+ }
+ }
+
+ async function submitOrder(
+ _userId: string,
+ cart: Cart,
+ settlementType: SettlementType,
+ checks?: CheckData[],
+ checkMode?: CheckRegistrationMode,
+ ): Promise {
+ if (!cart.companyId) {
+ throw new Error('شرکت سبد مشخص نیست.')
+ }
+ if (!cart.items.length) {
+ throw new Error('سبد خرید خالی است.')
+ }
+
+ const body = {
+ companyId: cart.companyId,
+ settlementType,
+ checkMode: settlementType === 'check' ? (checkMode ?? 'full') : undefined,
+ notes: cart.notes,
+ items: cart.items.map((item) => ({
+ productId: item.productId,
+ meterage: item.meterage,
+ notes: item.notes,
+ })),
+ checks:
+ settlementType === 'check' && checks
+ ? checks.map(({ id: _id, ...rest }) => rest)
+ : undefined,
+ }
+
+ try {
+ const order = await apiRequest('/orders', {
+ method: 'POST',
+ body,
+ })
+ orders.value.unshift(order)
+ draftChecks.value = []
+ draftSettlementType.value = null
+ draftCheckMode.value = null
+ checkoutCartId.value = null
+ return order
+ } catch (e) {
+ throw new Error(e instanceof ApiError ? e.message : 'ثبت سفارش ناموفق بود.')
+ }
+ }
+
+ async function submitCashReceipt(
orderId: string,
receipt: Omit,
) {
- const order = getOrderById(orderId)
- if (!order) return null
-
- const fullReceipt: CashPaymentReceipt = {
- ...receipt,
- id: generateId(),
- submittedAt: new Date().toISOString(),
+ try {
+ const order = await apiRequest(`/orders/${orderId}/cash-receipt`, {
+ method: 'POST',
+ body: {
+ amount: receipt.amount,
+ transferDate: receipt.transferDate,
+ transferTime: receipt.transferTime,
+ trackingNumber: receipt.trackingNumber,
+ destinationAccountId: receipt.destinationAccountId,
+ receiptImage: receipt.receiptImage,
+ notes: receipt.notes,
+ },
+ })
+ const idx = orders.value.findIndex((o) => o.id === orderId)
+ if (idx >= 0) orders.value[idx] = order
+ else orders.value.unshift(order)
+ return order
+ } catch (e) {
+ throw new Error(e instanceof ApiError ? e.message : 'ثبت رسید ناموفق بود.')
}
-
- const prevStatus = order.status
- order.cashReceipt = fullReceipt
- order.status = 'awaiting_cash_confirmation'
- order.statusHistory.push(
- createStatusEntry(prevStatus, 'awaiting_cash_confirmation', 'رسید پرداخت ثبت شد'),
- )
- order.updatedAt = new Date().toISOString()
- persist()
- return order
}
- function updateOrderChecks(orderId: string, checks: CheckData[]) {
- const order = getOrderById(orderId)
- if (!order) return null
-
- const rass = calculateCheckRass(checks, new Date(order.createdAt))
- order.checks = checks
- order.checkRassDays = rass.rassDays
- order.checkRassDate = rass.rassDate ?? undefined
- order.updatedAt = new Date().toISOString()
- persist()
- return order
+ async function updateOrderChecks(orderId: string, checks: CheckData[]) {
+ try {
+ const order = await apiRequest(`/orders/${orderId}/checks`, {
+ method: 'PATCH',
+ body: {
+ checks: checks.map(({ id: _id, ...rest }) => rest),
+ },
+ })
+ const idx = orders.value.findIndex((o) => o.id === orderId)
+ if (idx >= 0) orders.value[idx] = order
+ return order
+ } catch {
+ return null
+ }
}
- function hydrate() {
- orders.value = loadOrders()
+ async function getBankAccountsForCompany(companyId: string) {
+ return useCatalogStore().loadBankAccounts(companyId)
}
return {
@@ -230,8 +195,8 @@ export const useOrderStore = defineStore('order', () => {
draftSettlementType,
draftCheckMode,
checkoutCartId,
+ loading,
userOrders,
- persist,
getOrderById,
startCheckout,
setCheckMode,
@@ -243,6 +208,8 @@ export const useOrderStore = defineStore('order', () => {
submitCashReceipt,
updateOrderChecks,
hydrate,
- getBankAccountsForCompany: getBankAccountsByCompany,
+ resetLocal,
+ fetchOrder,
+ getBankAccountsForCompany,
}
})
diff --git a/src/stores/theme.ts b/src/stores/theme.ts
new file mode 100644
index 0000000..4702205
--- /dev/null
+++ b/src/stores/theme.ts
@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import { computed, watchEffect } from 'vue'
+import { usePreferredDark, useStorage } from '@vueuse/core'
+
+export type ThemePreference = 'light' | 'dark' | 'system'
+
+export const useThemeStore = defineStore('theme', () => {
+ const preference = useStorage('ft_theme', 'system')
+ const prefersDark = usePreferredDark()
+
+ const resolved = computed<'light' | 'dark'>(() => {
+ if (preference.value === 'system') return prefersDark.value ? 'dark' : 'light'
+ return preference.value
+ })
+
+ const isDark = computed(() => resolved.value === 'dark')
+
+ watchEffect(() => {
+ const root = document.documentElement
+ root.classList.toggle('dark', isDark.value)
+ root.style.colorScheme = resolved.value
+ })
+
+ function setPreference(value: ThemePreference) {
+ preference.value = value
+ }
+
+ return {
+ preference,
+ resolved,
+ isDark,
+ setPreference,
+ }
+})
diff --git a/src/types/index.ts b/src/types/index.ts
index 9f13623..caeafb5 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -72,6 +72,7 @@ export interface Catalog {
description?: string
displayOrder: number
isActive: boolean
+ productCount?: number
}
export interface Product {
diff --git a/src/utils/checkRass.ts b/src/utils/checkRass.ts
index 48a2c11..0cd6c13 100644
--- a/src/utils/checkRass.ts
+++ b/src/utils/checkRass.ts
@@ -1,20 +1,22 @@
import type { CheckData, CheckRassResult } from '@/types'
-import { parseISODate } from '@/utils/jalali'
+import { parseISODate, toISODate } from '@/utils/jalali'
+
+function emptyRass(): CheckRassResult {
+ return {
+ checkCount: 0,
+ totalAmount: 0,
+ nearestDueDate: null,
+ farthestDueDate: null,
+ rassDays: 0,
+ rassDate: null,
+ }
+}
export function calculateCheckRass(
checks: CheckData[],
baseDate: Date = new Date(),
): CheckRassResult {
- if (checks.length === 0) {
- return {
- checkCount: 0,
- totalAmount: 0,
- nearestDueDate: null,
- farthestDueDate: null,
- rassDays: 0,
- rassDate: null,
- }
- }
+ if (checks.length === 0) return emptyRass()
const base = new Date(baseDate)
base.setHours(0, 0, 0, 0)
@@ -24,7 +26,17 @@ export function calculateCheckRass(
const dueDates: Date[] = []
for (const check of checks) {
+ if (!check.dueDate) {
+ totalAmount += check.amount
+ continue
+ }
+
const due = parseISODate(check.dueDate)
+ if (Number.isNaN(due.getTime())) {
+ totalAmount += check.amount
+ continue
+ }
+
due.setHours(0, 0, 0, 0)
dueDates.push(due)
const diffMs = due.getTime() - base.getTime()
@@ -42,10 +54,10 @@ export function calculateCheckRass(
return {
checkCount: checks.length,
totalAmount,
- nearestDueDate: dueDates[0]?.toISOString() ?? null,
- farthestDueDate: dueDates[dueDates.length - 1]?.toISOString() ?? null,
+ nearestDueDate: dueDates[0] ? toISODate(dueDates[0]) : null,
+ farthestDueDate: dueDates.length ? toISODate(dueDates[dueDates.length - 1]!) : null,
rassDays,
- rassDate: rassDate.toISOString(),
+ rassDate: toISODate(rassDate),
}
}
@@ -53,6 +65,7 @@ export function daysUntilDue(dueDate: string): number {
const base = new Date()
base.setHours(0, 0, 0, 0)
const due = parseISODate(dueDate)
+ if (Number.isNaN(due.getTime())) return 0
due.setHours(0, 0, 0, 0)
return Math.max(0, Math.round((due.getTime() - base.getTime()) / (1000 * 60 * 60 * 24)))
}
diff --git a/src/utils/format.ts b/src/utils/format.ts
index b29c70e..5f71072 100644
--- a/src/utils/format.ts
+++ b/src/utils/format.ts
@@ -45,16 +45,22 @@ const persianDateTimeFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
function toDateValue(date: string | Date): Date {
if (typeof date !== 'string') return date
- if (/^\d{4}-\d{2}-\d{2}$/.test(date)) return parseISODate(date)
+ if (/^\d{4}-\d{2}-\d{2}/.test(date)) return parseISODate(date)
return new Date(date)
}
-export function formatDate(date: string | Date): string {
- return persianDateFormatter.format(toDateValue(date))
+export function formatDate(date: string | Date | null | undefined): string {
+ if (!date) return '—'
+ const value = toDateValue(date)
+ if (Number.isNaN(value.getTime())) return '—'
+ return persianDateFormatter.format(value)
}
-export function formatDateTime(date: string | Date): string {
- return persianDateTimeFormatter.format(toDateValue(date))
+export function formatDateTime(date: string | Date | null | undefined): string {
+ if (!date) return '—'
+ const value = toDateValue(date)
+ if (Number.isNaN(value.getTime())) return '—'
+ return persianDateTimeFormatter.format(value)
}
export function formatTime(date: string | Date): string {
@@ -70,10 +76,10 @@ export function toPersianDigits(input: string): string {
}
export function normalizePhone(phone: string): string {
- const digits = phone.replace(/\D/g, '')
- if (digits.startsWith('98')) return '0' + digits.slice(2)
- if (digits.startsWith('9') && digits.length === 10) return '0' + digits
- return digits
+ let digits = extractNumericString(phone)
+ if (digits.startsWith('98')) digits = '0' + digits.slice(2)
+ else if (digits.startsWith('9') && digits.length === 10) digits = '0' + digits
+ return digits.slice(0, 11)
}
export function generateId(): string {
diff --git a/src/utils/jalali.ts b/src/utils/jalali.ts
index c724465..b7b180f 100644
--- a/src/utils/jalali.ts
+++ b/src/utils/jalali.ts
@@ -30,8 +30,17 @@ export interface JalaliDate {
}
export function parseISODate(iso: string): Date {
- const [year, month, day] = iso.split('-').map(Number)
- return new Date(year!, month! - 1, day!)
+ const match = iso.trim().match(/^(\d{4})-(\d{2})-(\d{2})/)
+ if (match) {
+ return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
+ }
+
+ const parsed = new Date(iso)
+ if (Number.isNaN(parsed.getTime())) {
+ return new Date(NaN)
+ }
+
+ return new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate())
}
export function toISODate(date: Date): string {
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
index 7598664..8b1e44a 100644
--- a/src/utils/validation.ts
+++ b/src/utils/validation.ts
@@ -1,4 +1,5 @@
import type { MeterageValidationResult, Product } from '@/types'
+import { normalizePhone } from '@/utils/format'
export function validateNationalId(nationalId: string): boolean {
if (!/^\d{10}$/.test(nationalId)) return false
@@ -17,7 +18,7 @@ export function validateSayadId(sayadId: string): boolean {
}
export function validatePhone(phone: string): boolean {
- return /^09\d{9}$/.test(phone.replace(/\D/g, '').replace(/^9/, '09').slice(0, 11))
+ return /^09\d{9}$/.test(normalizePhone(phone))
}
export function validateMeterage(
diff --git a/src/views/OrdersView.vue b/src/views/OrdersView.vue
index 30ebce7..3e89d3d 100644
--- a/src/views/OrdersView.vue
+++ b/src/views/OrdersView.vue
@@ -14,14 +14,20 @@ const {
filteredOrdersMobile,
filteredOrdersDesktop,
paginatedOrders,
+ paginatedOrdersMobile,
totalPages,
+ mobileTotalPages,
paginationRange,
+ mobilePaginationRange,
visiblePages,
+ mobileVisiblePages,
currentPage,
+ mobilePage,
companySlug,
iconVariant,
goToOrder,
setPage,
+ setMobilePage,
setDesktopFilter,
viewStatFilter,
} = useOrderList()
@@ -47,12 +53,18 @@ const desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[] = [
class="mobile-only"
:tab="tab"
:tabs="mobileTabs"
- :orders="filteredOrdersMobile"
+ :orders="paginatedOrdersMobile"
+ :total-count="filteredOrdersMobile.length"
+ :current-page="mobilePage"
+ :total-pages="mobileTotalPages"
+ :pagination-range="mobilePaginationRange"
+ :visible-pages="mobileVisiblePages"
:company-slug="companySlug"
:icon-variant="iconVariant"
@update:tab="tab = $event"
@select="goToOrder"
@back="router.push('/')"
+ @set-page="setMobilePage"
/>
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import BaseInput from '@/components/ui/BaseInput.vue'
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
import BaseButton from '@/components/ui/BaseButton.vue'
-import { normalizePhone } from '@/utils/format'
+import { normalizePhone, toPersianDigits } from '@/utils/format'
+import { validatePhone } from '@/utils/validation'
import {
FORM_VALIDATION_SUMMARY,
clearFieldError,
@@ -20,24 +21,42 @@ const phone = ref('')
const fieldErrors = ref({})
const formError = ref('')
+const displayPhone = computed(() => (phone.value ? toPersianDigits(phone.value) : ''))
+
+function phoneErrorMessage(value: string): string {
+ if (!value) return 'شماره موبایل را وارد کنید.'
+ if (!validatePhone(value)) return 'شماره موبایل معتبر نیست. (مثال: ۰۹۱۲۱۲۳۴۵۶۷)'
+ return ''
+}
+
function clearPhoneError() {
fieldErrors.value = clearFieldError(fieldErrors.value, 'phone')
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
}
+function onPhoneInput(value: string) {
+ phone.value = normalizePhone(value)
+ clearPhoneError()
+}
+
+function onPhoneBlur() {
+ if (!phone.value) return
+ const message = phoneErrorMessage(phone.value)
+ if (message) {
+ fieldErrors.value = { ...fieldErrors.value, phone: message }
+ }
+}
+
async function submit() {
fieldErrors.value = {}
formError.value = ''
const normalized = normalizePhone(phone.value)
- const errors: FieldErrors = {}
+ phone.value = normalized
- if (!/^09\d{9}$/.test(normalized)) {
- errors.phone = 'شماره موبایل معتبر نیست.'
- }
-
- if (hasFieldErrors(errors)) {
- fieldErrors.value = errors
+ const message = phoneErrorMessage(normalized)
+ if (message) {
+ fieldErrors.value = { phone: message }
formError.value = FORM_VALIDATION_SUMMARY
return
}
@@ -52,59 +71,41 @@ async function submit() {
-
-
+
+
-
+
+
دریافت کد تأیید
+
-
+
-
-
diff --git a/src/views/auth/OtpView.vue b/src/views/auth/OtpView.vue
index c168c14..8fbe2db 100644
--- a/src/views/auth/OtpView.vue
+++ b/src/views/auth/OtpView.vue
@@ -1,10 +1,11 @@
-
-
+
+
-
+
+
+
+
تأیید و ورود
+
+ ارسال مجدد کد
+ ارسال مجدد کد ({{ timerLabel }})
+
+
+ تغییر شماره موبایل
+
+
diff --git a/src/views/auth/RegisterView.vue b/src/views/auth/RegisterView.vue
index e1ebe03..b2436ea 100644
--- a/src/views/auth/RegisterView.vue
+++ b/src/views/auth/RegisterView.vue
@@ -6,6 +6,7 @@ import BaseInput from '@/components/ui/BaseInput.vue'
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
import BaseButton from '@/components/ui/BaseButton.vue'
import { normalizePhone } from '@/utils/format'
+import { validatePhone } from '@/utils/validation'
import {
FORM_VALIDATION_SUMMARY,
clearFieldError,
@@ -40,7 +41,7 @@ async function submit() {
if (!lastName.value.trim()) {
errors.lastName = 'نام خانوادگی الزامی است.'
}
- if (!/^09\d{9}$/.test(normalized)) {
+ if (!validatePhone(normalized)) {
errors.phone = 'شماره موبایل معتبر نیست.'
}
@@ -64,10 +65,10 @@ async function submit() {
-
-
+
+
@@ -100,36 +101,12 @@ async function submit() {
دریافت کد تأیید
-
+
-
-
diff --git a/src/views/cart/CartView.vue b/src/views/cart/CartView.vue
index 574cd26..01f9de2 100644
--- a/src/views/cart/CartView.vue
+++ b/src/views/cart/CartView.vue
@@ -30,49 +30,59 @@ function goToOrdering() {
-
+
+
-
+
-
-
-
سقف متراژ تکمیل شد
-
- سقف متراژ این فاکتور تکمیل شده است. آیا مایلید ادامه محصولات را در فاکتور جدید ثبت کنید؟
-
-
-
تسویه همین فاکتور
-
فاکتور جدید
+
+
+
سقف متراژ تکمیل شد
+
+ سقف متراژ این فاکتور تکمیل شده است. آیا مایلید ادامه محصولات را در فاکتور جدید ثبت کنید؟
+
+
+ تسویه همین فاکتور
+ فاکتور جدید
+
diff --git a/src/views/catalogs/CatalogListView.vue b/src/views/catalogs/CatalogListView.vue
index 611254b..3f7a104 100644
--- a/src/views/catalogs/CatalogListView.vue
+++ b/src/views/catalogs/CatalogListView.vue
@@ -1,14 +1,16 @@
@@ -42,8 +58,8 @@ function getProductCount(catalogId: string) {
class="catalog-page__search"
/>
-
-
+
+
-
کالیتهای یافت نشد.
+
@@ -76,7 +96,7 @@ function getProductCount(catalogId: string) {
class="catalog-page__search"
/>
-
+
-
کالیتهای یافت نشد.
+
diff --git a/src/views/checkout/CashSettlementView.vue b/src/views/checkout/CashSettlementView.vue
index 9d86896..a952d2e 100644
--- a/src/views/checkout/CashSettlementView.vue
+++ b/src/views/checkout/CashSettlementView.vue
@@ -1,20 +1,23 @@
@@ -115,9 +119,10 @@ function submitOrder() {
-
چکی ثبت نشده است.
-
ثبت چک جدید
-
ثبت سفارش جدید
+
+ ثبت چک جدید
+ ثبت سفارش جدید
+
@@ -268,7 +273,7 @@ function submitOrder() {
-
+
ثبت چک جدید
@@ -293,7 +298,7 @@ function submitOrder() {
min-height: 0;
height: 100%;
padding: 12px var(--page-padding) 0;
- background: #ffffff;
+ background: var(--color-bg);
overflow: hidden;
}
@@ -360,8 +365,10 @@ function submitOrder() {
.summary-card {
padding: 14px 12px;
border-radius: 14px;
- background: #f1f5f9;
+ background: var(--color-summary-bg);
+ border: 1px solid var(--color-border);
text-align: center;
+ color: var(--color-text);
}
.summary-card__label {
@@ -422,8 +429,9 @@ function submitOrder() {
align-items: center;
padding: 12px;
border-radius: 14px;
- background: #f8fafc;
- border: 1px solid #eef2f7;
+ background: var(--color-elevated);
+ border: 1px solid var(--color-border);
+ color: var(--color-text);
}
.check-card__menu-wrap {
@@ -450,7 +458,7 @@ function submitOrder() {
min-width: 88px;
padding: 4px;
border-radius: var(--radius-sm);
- background: #fff;
+ background: var(--color-elevated);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
}
@@ -505,7 +513,7 @@ function submitOrder() {
border-radius: 8px;
object-fit: cover;
border: 1px solid var(--color-border);
- background: #fff;
+ background: var(--color-surface);
}
.check-card__thumb--empty {
@@ -519,8 +527,8 @@ function submitOrder() {
.rass-panel {
padding: 16px;
border-radius: 14px;
- background: #ecfdf3;
- border: 1px solid #bbf7d0;
+ background: var(--color-success-light);
+ border: 1px solid var(--color-success);
margin-bottom: 12px;
text-align: center;
}
@@ -529,26 +537,26 @@ function submitOrder() {
margin: 0 0 6px;
font-size: 0.875rem;
font-weight: 600;
- color: #166534;
+ color: var(--color-success);
}
.rass-panel__days {
font-size: 1.75rem;
font-weight: 700;
- color: #15803d;
+ color: var(--color-success);
line-height: 1.2;
}
.rass-panel__date {
margin: 6px 0 0;
font-size: 0.8125rem;
- color: #166534;
+ color: var(--color-success);
}
.check-list-page__footer {
flex-shrink: 0;
padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
- background: #fff;
+ background: var(--color-bg);
border-top: 1px solid var(--color-border);
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
margin: 0 calc(-1 * var(--page-padding));
diff --git a/src/views/checkout/CheckModeSelectView.vue b/src/views/checkout/CheckModeSelectView.vue
index 21f8cfa..2ee9dda 100644
--- a/src/views/checkout/CheckModeSelectView.vue
+++ b/src/views/checkout/CheckModeSelectView.vue
@@ -71,8 +71,9 @@ function selectMode(mode: 'full' | 'brief') {
padding: 32px 24px;
border: none;
border-radius: 16px;
- background: #ffffff;
- box-shadow: 0 2px 12px rgba(15, 23, 42, 0.06);
+ background: var(--color-elevated);
+ color: var(--color-text);
+ box-shadow: var(--shadow-sm);
cursor: pointer;
transition:
box-shadow 0.15s,
diff --git a/src/views/checkout/SettlementTypeView.vue b/src/views/checkout/SettlementTypeView.vue
index 1262ec8..afae80f 100644
--- a/src/views/checkout/SettlementTypeView.vue
+++ b/src/views/checkout/SettlementTypeView.vue
@@ -99,9 +99,10 @@ function selectType(type: 'cash' | 'check') {
padding: 32px 24px;
border: none;
border-radius: 16px;
- background: #fff;
+ background: var(--color-elevated);
box-shadow: var(--shadow-sm);
cursor: pointer;
+ color: var(--color-text);
}
.settlement-card--desktop {
diff --git a/src/views/companies/CompanySelectView.vue b/src/views/companies/CompanySelectView.vue
index a5bfeb0..e243470 100644
--- a/src/views/companies/CompanySelectView.vue
+++ b/src/views/companies/CompanySelectView.vue
@@ -1,4 +1,5 @@
-
+
+ در حال بارگذاری…
+
+
+
-
+
+
diff --git a/src/views/companies/components/MobileCompanySelect.vue b/src/views/companies/components/MobileCompanySelect.vue
index 45e92e5..7954a24 100644
--- a/src/views/companies/components/MobileCompanySelect.vue
+++ b/src/views/companies/components/MobileCompanySelect.vue
@@ -46,7 +46,8 @@ defineEmits<{
display: flex;
flex-direction: column;
min-height: 0;
- background: #fff;
+ background: var(--color-bg);
+ color: var(--color-text);
padding: 12px 0;
}
@@ -91,13 +92,14 @@ defineEmits<{
.company-brand-card--par {
background: var(--color-primary);
color: #fff;
- box-shadow: 0 8px 24px rgba(29, 53, 87, 0.25);
+ box-shadow: var(--shadow-md);
}
.company-brand-card--fam-baft {
- background: #f3ece3;
- color: #1a202c;
- box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
+ background: var(--color-brand-warm);
+ color: var(--color-brand-warm-text);
+ box-shadow: var(--shadow-card);
+ border: 1px solid var(--color-border);
}
.company-brand-card__logo {
@@ -121,6 +123,6 @@ defineEmits<{
}
.company-brand-card--fam-baft .company-brand-card__desc {
- color: #718096;
+ color: var(--color-brand-warm-muted);
}
diff --git a/src/views/orders/OrderDetailView.vue b/src/views/orders/OrderDetailView.vue
index beb648c..6787398 100644
--- a/src/views/orders/OrderDetailView.vue
+++ b/src/views/orders/OrderDetailView.vue
@@ -64,8 +64,31 @@ watch(destinationAccountId, () => {
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
})
-const bankAccounts = computed(() =>
- order.value ? orderStore.getBankAccountsForCompany(order.value.companyId) : [],
+const bankAccounts = ref(
+ [] as Awaited
>,
+)
+
+watch(
+ () => order.value?.companyId,
+ async (companyId) => {
+ if (!companyId) {
+ bankAccounts.value = []
+ return
+ }
+ bankAccounts.value = await orderStore.getBankAccountsForCompany(companyId)
+ },
+ { immediate: true },
+)
+
+watch(
+ () => orderId.value,
+ async (id) => {
+ if (!id) return
+ if (!orderStore.getOrderById(id)) {
+ await orderStore.fetchOrder(id)
+ }
+ },
+ { immediate: true },
)
const statusHero = computed(() => (order.value ? getStatusHero(order.value.status) : null))
@@ -114,7 +137,7 @@ async function handleReceiptImage(event: Event) {
receiptImage.value = await fileToBase64(file)
}
-function submitReceipt() {
+async function submitReceipt() {
if (!order.value) return
fieldErrors.value = {}
@@ -142,17 +165,21 @@ function submitReceipt() {
return
}
- orderStore.submitCashReceipt(order.value.id, {
- amount,
- transferDate: receiptDate.value,
- transferTime: receiptTime.value || '12:00',
- trackingNumber: trackingNumber.value,
- destinationAccountId: destinationAccountId.value,
- receiptImage: receiptImage.value || undefined,
- })
- showReceiptForm.value = false
- fieldErrors.value = {}
- formError.value = ''
+ try {
+ await orderStore.submitCashReceipt(order.value.id, {
+ amount,
+ transferDate: receiptDate.value,
+ transferTime: receiptTime.value || '12:00',
+ trackingNumber: trackingNumber.value,
+ destinationAccountId: destinationAccountId.value,
+ receiptImage: receiptImage.value || undefined,
+ })
+ showReceiptForm.value = false
+ fieldErrors.value = {}
+ formError.value = ''
+ } catch (e) {
+ formError.value = e instanceof Error ? e.message : 'ثبت رسید ناموفق بود.'
+ }
}
@@ -388,7 +415,8 @@ function submitReceipt() {
min-height: 0;
height: 100%;
padding: 12px var(--page-padding) 0;
- background: #ffffff;
+ background: var(--color-bg);
+ color: var(--color-text);
overflow: hidden;
}
@@ -457,15 +485,15 @@ function submitReceipt() {
}
.order-info-card {
- background: #ffffff;
+ background: var(--color-elevated);
border-radius: 20px;
- box-shadow: 0 4px 20px rgba(15, 23, 42, 0.08);
+ box-shadow: var(--shadow-sm);
padding: 20px 18px 8px;
}
.order-info-card__head {
padding-bottom: 16px;
- border-bottom: 1px solid #eeeeee;
+ border-bottom: 1px solid var(--color-border);
margin-bottom: 4px;
}
@@ -490,7 +518,7 @@ function submitReceipt() {
align-items: center;
gap: 12px;
padding: 14px 0;
- border-bottom: 1px solid #eeeeee;
+ border-bottom: 1px solid var(--color-border);
}
.order-info-card__row--last {
@@ -511,7 +539,7 @@ function submitReceipt() {
}
.order-info-card__value--accent {
- color: #2b66e3;
+ color: var(--color-primary);
font-weight: 500;
}
@@ -525,7 +553,7 @@ function submitReceipt() {
.order-detail-page__footer {
flex-shrink: 0;
padding: 12px 0;
- background: #ffffff;
+ background: var(--color-bg);
border-top: 1px solid var(--color-border);
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
margin: 0 calc(-1 * var(--page-padding));
@@ -550,7 +578,7 @@ function submitReceipt() {
padding: 16px;
border: 1px solid var(--color-border);
border-radius: 16px;
- background: #ffffff;
+ background: var(--color-elevated);
box-shadow: var(--shadow-sm);
}
diff --git a/src/views/orders/components/DesktopOrdersPage.vue b/src/views/orders/components/DesktopOrdersPage.vue
index 263ebba..855cab8 100644
--- a/src/views/orders/components/DesktopOrdersPage.vue
+++ b/src/views/orders/components/DesktopOrdersPage.vue
@@ -203,7 +203,7 @@ const emit = defineEmits<{
-
+
ثبت سفارش جدید
diff --git a/src/views/orders/components/MobileOrdersPage.vue b/src/views/orders/components/MobileOrdersPage.vue
index 27a5223..18413a9 100644
--- a/src/views/orders/components/MobileOrdersPage.vue
+++ b/src/views/orders/components/MobileOrdersPage.vue
@@ -4,15 +4,21 @@ import BaseButton from '@/components/ui/BaseButton.vue'
import FilterTabs from '@/components/ui/FilterTabs.vue'
import StatusBadge from '@/components/ui/StatusBadge.vue'
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
+import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import IconParLogo from '@/components/icons/IconParLogo.vue'
import IconFamBaftLogo from '@/components/icons/IconFamBaftLogo.vue'
-import { formatCurrency } from '@/utils/format'
+import { formatCurrency, formatNumber, toPersianDigits } from '@/utils/format'
import type { Order } from '@/types'
defineProps<{
tab: string
tabs: { id: string; label: string }[]
orders: Order[]
+ totalCount: number
+ currentPage: number
+ totalPages: number
+ paginationRange: { start: number; end: number }
+ visiblePages: (number | 'ellipsis')[]
companySlug: (order: Order) => string
iconVariant: (order: Order) => string
}>()
@@ -21,6 +27,7 @@ const emit = defineEmits<{
'update:tab': [value: string]
select: [id: string]
back: []
+ 'set-page': [page: number]
}>()
@@ -44,7 +51,7 @@ const emit = defineEmits<{
@update:model-value="emit('update:tab', $event)"
/>
-
diff --git a/src/views/products/components/DesktopProductPage.vue b/src/views/products/components/DesktopProductPage.vue
index e91a3c5..3737f19 100644
--- a/src/views/products/components/DesktopProductPage.vue
+++ b/src/views/products/components/DesktopProductPage.vue
@@ -1,5 +1,6 @@
-
-