Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b478f262 | ||
|
|
e4f14a24b7 | ||
|
|
3bd0d055d9 | ||
|
|
746d17e182 | ||
|
|
e96e51598a | ||
|
|
9abec36f07 | ||
|
|
ecd8e3679f | ||
|
|
ba95723b61 | ||
|
|
12289bdce3 | ||
|
|
d1c5234a03 | ||
|
|
27cc876e82 | ||
|
|
82919d39f6 |
11
CLAUDE.md
11
CLAUDE.md
@@ -293,10 +293,11 @@ When adding new features, add tests in `src/__tests__/`. Name test files `<featu
|
|||||||
5. Create tarball: `tar -czf app-ts-X.Y.Z.tar.gz dist dist-client prisma package.json package-lock.json scripts`
|
5. Create tarball: `tar -czf app-ts-X.Y.Z.tar.gz dist dist-client prisma package.json package-lock.json scripts`
|
||||||
6. Deploy via SSH to production server (`boha_admin@192.168.50.100`):
|
6. Deploy via SSH to production server (`boha_admin@192.168.50.100`):
|
||||||
- Path: `/var/www/app-ts`
|
- Path: `/var/www/app-ts`
|
||||||
- Backup: `node_modules`, `.env`, `ecosystem.config.js`
|
- Remove old files: `rm -rf dist dist-client prisma scripts package.json package-lock.json`
|
||||||
- Clean directory (keep backups only)
|
- Copy tarball to server: `scp app-ts-X.Y.Z.tar.gz boha_admin@192.168.50.100:/tmp/`
|
||||||
- Extract tarball
|
- Extract tarball: `tar -xzf /tmp/app-ts-X.Y.Z.tar.gz`
|
||||||
- Restore backups
|
- Install dependencies: `npm install --omit=dev`
|
||||||
- Restart: `pm2 reload ecosystem.config.js`
|
- Apply Prisma migrations: `npx prisma migrate deploy`
|
||||||
|
- Restart: `pm2 restart app-ts --update-env`
|
||||||
|
|
||||||
Do not push directly to production or restart services without confirmation.
|
Do not push directly to production or restart services without confirmation.
|
||||||
|
|||||||
37
boneyard.config.json
Normal file
37
boneyard.config.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": [375, 768, 1280],
|
||||||
|
"out": "./src/admin/bones",
|
||||||
|
"color": "#e0e0e0",
|
||||||
|
"animate": "shimmer",
|
||||||
|
"shimmerColor": "#f0f0f0",
|
||||||
|
"speed": "1.2s",
|
||||||
|
"shimmerAngle": 110,
|
||||||
|
"wait": 3000,
|
||||||
|
"routes": [
|
||||||
|
"/",
|
||||||
|
"/users",
|
||||||
|
"/attendance",
|
||||||
|
"/attendance/history",
|
||||||
|
"/attendance/admin",
|
||||||
|
"/attendance/balances",
|
||||||
|
"/attendance/requests",
|
||||||
|
"/attendance/approval",
|
||||||
|
"/attendance/create",
|
||||||
|
"/trips",
|
||||||
|
"/trips/history",
|
||||||
|
"/trips/admin",
|
||||||
|
"/vehicles",
|
||||||
|
"/offers",
|
||||||
|
"/offers/new",
|
||||||
|
"/offers/customers",
|
||||||
|
"/offers/templates",
|
||||||
|
"/orders",
|
||||||
|
"/orders/1",
|
||||||
|
"/projects",
|
||||||
|
"/projects/80",
|
||||||
|
"/invoices",
|
||||||
|
"/invoices/new",
|
||||||
|
"/settings",
|
||||||
|
"/audit-log"
|
||||||
|
]
|
||||||
|
}
|
||||||
220
package-lock.json
generated
220
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.3",
|
"version": "1.5.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.3",
|
"version": "1.5.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -19,8 +19,10 @@
|
|||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@types/jsdom": "^28.0.1",
|
"@types/jsdom": "^28.0.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"boneyard-js": "^1.8.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
@@ -148,6 +150,13 @@
|
|||||||
"specificity": "bin/cli.js"
|
"specificity": "bin/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@chenglou/pretext": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chenglou/pretext/-/pretext-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@csstools/color-helpers": {
|
"node_modules/@csstools/color-helpers": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||||
@@ -353,7 +362,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -365,7 +373,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -376,7 +383,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -390,7 +396,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -407,7 +412,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -424,7 +428,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -441,7 +444,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -458,7 +460,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -475,7 +476,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -492,7 +492,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -509,7 +508,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -526,7 +524,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -543,7 +540,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -560,7 +556,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -577,7 +572,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -594,7 +588,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -611,7 +604,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -628,7 +620,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -645,7 +636,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -662,7 +652,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -679,7 +668,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -696,7 +684,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -713,7 +700,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -730,7 +716,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -747,7 +732,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -764,7 +748,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -781,7 +764,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -798,7 +780,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -815,7 +796,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1195,7 +1175,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1224,7 +1203,7 @@
|
|||||||
"version": "0.115.0",
|
"version": "0.115.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
||||||
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
@@ -1234,7 +1213,7 @@
|
|||||||
"version": "0.115.0",
|
"version": "0.115.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
||||||
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
@@ -1385,7 +1364,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1402,7 +1380,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1419,7 +1396,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1436,7 +1412,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1453,7 +1428,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1470,7 +1444,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1487,7 +1460,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1504,7 +1476,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1521,7 +1492,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1538,7 +1508,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1555,7 +1524,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1572,7 +1540,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1589,7 +1556,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1606,7 +1572,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1623,7 +1588,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1646,6 +1610,32 @@
|
|||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.100.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz",
|
||||||
|
"integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.100.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz",
|
||||||
|
"integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.100.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/token": {
|
"node_modules/@tokenizer/token": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
@@ -1662,7 +1652,6 @@
|
|||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2332,6 +2321,53 @@
|
|||||||
"require-from-string": "^2.0.2"
|
"require-from-string": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boneyard-js": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/boneyard-js/-/boneyard-js-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-tnffA34xFgGSKpQ6QXebalkz2g/rMRAUIh3S0OBoPTK+OgYR3QEQvD3fHGKNQDbCTjn3W7dgNfiBhZGpnqlcNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"boneyard-js": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@chenglou/pretext": "^0.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=14",
|
||||||
|
"preact": ">=10",
|
||||||
|
"react": ">=18",
|
||||||
|
"react-native": ">=0.71",
|
||||||
|
"svelte": ">=5.29",
|
||||||
|
"vite": ">=5",
|
||||||
|
"vue": ">=3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@angular/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"preact": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
@@ -2889,7 +2925,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -3087,7 +3123,7 @@
|
|||||||
"version": "0.27.4",
|
"version": "0.27.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3444,7 +3480,7 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -3568,7 +3604,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3656,7 +3691,7 @@
|
|||||||
"version": "4.13.6",
|
"version": "4.13.6",
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resolve-pkg-maps": "^1.0.0"
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
@@ -4135,7 +4170,7 @@
|
|||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -4168,7 +4203,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4189,7 +4223,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4210,7 +4243,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4231,7 +4263,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4252,7 +4283,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4273,7 +4303,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4294,7 +4323,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4315,7 +4343,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4336,7 +4363,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4357,7 +4383,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4378,7 +4403,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4618,7 +4642,7 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4931,7 +4955,7 @@
|
|||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -4988,6 +5012,50 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
@@ -5001,7 +5069,7 @@
|
|||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -5521,7 +5589,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
@@ -5556,7 +5624,7 @@
|
|||||||
"version": "1.0.0-rc.9",
|
"version": "1.0.0-rc.9",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
||||||
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.115.0",
|
"@oxc-project/types": "=0.115.0",
|
||||||
@@ -5590,7 +5658,7 @@
|
|||||||
"version": "1.0.0-rc.9",
|
"version": "1.0.0-rc.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
||||||
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
@@ -6159,7 +6227,7 @@
|
|||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -6279,7 +6347,7 @@
|
|||||||
"version": "4.21.0",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.27.0",
|
||||||
@@ -6334,7 +6402,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
||||||
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/runtime": "0.115.0",
|
"@oxc-project/runtime": "0.115.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.4",
|
"version": "1.6.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"bones": "boneyard-js build http://localhost:3000"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -34,8 +35,10 @@
|
|||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@types/jsdom": "^28.0.1",
|
"@types/jsdom": "^28.0.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"boneyard-js": "^1.8.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `users` ADD COLUMN `totp_last_used_counter` INTEGER NULL;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
import { AlertProvider } from "./context/AlertContext";
|
import { AlertProvider } from "./context/AlertContext";
|
||||||
|
import { queryClient } from "./lib/queryClient";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
import AlertContainer from "./components/AlertContainer";
|
import AlertContainer from "./components/AlertContainer";
|
||||||
@@ -14,8 +16,8 @@ import "./buttons.css";
|
|||||||
import "./layout.css";
|
import "./layout.css";
|
||||||
import "./components.css";
|
import "./components.css";
|
||||||
import "./tables.css";
|
import "./tables.css";
|
||||||
import "./skeleton.css";
|
|
||||||
import "./datepicker.css";
|
import "./datepicker.css";
|
||||||
|
import "./bones/registry";
|
||||||
import "./filemanager.css";
|
import "./filemanager.css";
|
||||||
import "./pagination.css";
|
import "./pagination.css";
|
||||||
import "./responsive.css";
|
import "./responsive.css";
|
||||||
@@ -46,7 +48,6 @@ const OffersTemplates = lazy(() => import("./pages/OffersTemplates"));
|
|||||||
const Orders = lazy(() => import("./pages/Orders"));
|
const Orders = lazy(() => import("./pages/Orders"));
|
||||||
const OrderDetail = lazy(() => import("./pages/OrderDetail"));
|
const OrderDetail = lazy(() => import("./pages/OrderDetail"));
|
||||||
const Projects = lazy(() => import("./pages/Projects"));
|
const Projects = lazy(() => import("./pages/Projects"));
|
||||||
const ProjectCreate = lazy(() => import("./pages/ProjectCreate"));
|
|
||||||
const ProjectDetail = lazy(() => import("./pages/ProjectDetail"));
|
const ProjectDetail = lazy(() => import("./pages/ProjectDetail"));
|
||||||
const Invoices = lazy(() => import("./pages/Invoices"));
|
const Invoices = lazy(() => import("./pages/Invoices"));
|
||||||
const InvoiceDetail = lazy(() => import("./pages/InvoiceDetail"));
|
const InvoiceDetail = lazy(() => import("./pages/InvoiceDetail"));
|
||||||
@@ -58,6 +59,7 @@ export default function AdminApp() {
|
|||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<AlertContainer />
|
<AlertContainer />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense
|
<Suspense
|
||||||
@@ -77,13 +79,22 @@ export default function AdminApp() {
|
|||||||
path="attendance/history"
|
path="attendance/history"
|
||||||
element={<AttendanceHistory />}
|
element={<AttendanceHistory />}
|
||||||
/>
|
/>
|
||||||
<Route path="attendance/admin" element={<AttendanceAdmin />} />
|
<Route
|
||||||
|
path="attendance/admin"
|
||||||
|
element={<AttendanceAdmin />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="attendance/balances"
|
path="attendance/balances"
|
||||||
element={<AttendanceBalances />}
|
element={<AttendanceBalances />}
|
||||||
/>
|
/>
|
||||||
<Route path="attendance/requests" element={<LeaveRequests />} />
|
<Route
|
||||||
<Route path="attendance/approval" element={<LeaveApproval />} />
|
path="attendance/requests"
|
||||||
|
element={<LeaveRequests />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="attendance/approval"
|
||||||
|
element={<LeaveApproval />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="attendance/create"
|
path="attendance/create"
|
||||||
element={<AttendanceCreate />}
|
element={<AttendanceCreate />}
|
||||||
@@ -99,12 +110,17 @@ export default function AdminApp() {
|
|||||||
<Route path="offers" element={<Offers />} />
|
<Route path="offers" element={<Offers />} />
|
||||||
<Route path="offers/new" element={<OfferDetail />} />
|
<Route path="offers/new" element={<OfferDetail />} />
|
||||||
<Route path="offers/:id" element={<OfferDetail />} />
|
<Route path="offers/:id" element={<OfferDetail />} />
|
||||||
<Route path="offers/customers" element={<OffersCustomers />} />
|
<Route
|
||||||
<Route path="offers/templates" element={<OffersTemplates />} />
|
path="offers/customers"
|
||||||
|
element={<OffersCustomers />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="offers/templates"
|
||||||
|
element={<OffersTemplates />}
|
||||||
|
/>
|
||||||
<Route path="orders" element={<Orders />} />
|
<Route path="orders" element={<Orders />} />
|
||||||
<Route path="orders/:id" element={<OrderDetail />} />
|
<Route path="orders/:id" element={<OrderDetail />} />
|
||||||
<Route path="projects" element={<Projects />} />
|
<Route path="projects" element={<Projects />} />
|
||||||
<Route path="projects/new" element={<ProjectCreate />} />
|
|
||||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
<Route path="projects/:id" element={<ProjectDetail />} />
|
||||||
<Route path="invoices" element={<Invoices />} />
|
<Route path="invoices" element={<Invoices />} />
|
||||||
<Route path="invoices/new" element={<InvoiceDetail />} />
|
<Route path="invoices/new" element={<InvoiceDetail />} />
|
||||||
@@ -116,6 +132,7 @@ export default function AdminApp() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
</QueryClientProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -330,15 +330,6 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Additional Utilities ─────────────────────────────────────────── */
|
/* ── Additional Utilities ─────────────────────────────────────────── */
|
||||||
|
|
||||||
/* Font sizes */
|
/* Font sizes */
|
||||||
|
|||||||
1142
src/admin/bones/attendance-balances.bones.json
Normal file
1142
src/admin/bones/attendance-balances.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
263
src/admin/bones/attendance-create.bones.json
Normal file
263
src/admin/bones/attendance-create.bones.json
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 403,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
100,
|
||||||
|
361,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
55,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
83,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
127,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
156,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
200,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
229,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
273,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
302,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
346,
|
||||||
|
19.2352,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 420,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
23.3738,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
46,
|
||||||
|
81.5217,
|
||||||
|
373,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
65,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
94,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
138,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
167,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
211,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
284,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
313,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
357,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 369,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
17.2722,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
46,
|
||||||
|
60.241,
|
||||||
|
323,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
65,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
93,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
129,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
156,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
192,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
219,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
255,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
282,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
318,
|
||||||
|
6.3771,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "7c4e446bf97f164a0fba87e2dd7df7d1"
|
||||||
|
}
|
||||||
1094
src/admin/bones/attendance-history-fund.bones.json
Normal file
1094
src/admin/bones/attendance-history-fund.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
1094
src/admin/bones/attendance-history-table.bones.json
Normal file
1094
src/admin/bones/attendance-history-table.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
1149
src/admin/bones/audit-log-rows.bones.json
Normal file
1149
src/admin/bones/audit-log-rows.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
506
src/admin/bones/dash-sessions.bones.json
Normal file
506
src/admin/bones/dash-sessions.bones.json
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 341,
|
||||||
|
"width": 341,
|
||||||
|
"height": 304,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
304,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.8123,
|
||||||
|
17,
|
||||||
|
34.9386,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.3383,
|
||||||
|
13,
|
||||||
|
36.8493,
|
||||||
|
37,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.2933,
|
||||||
|
63,
|
||||||
|
99.4135,
|
||||||
|
83,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
86,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
95,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.0407,
|
||||||
|
79,
|
||||||
|
19.5152,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
110,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
110,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
110,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
167,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
176,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
162,
|
||||||
|
75.9531,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
189,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
189,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
189,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
246,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
255,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
241,
|
||||||
|
75.9531,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
267,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
267,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
267,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 726,
|
||||||
|
"width": 726,
|
||||||
|
"height": 319,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
319,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.6171,
|
||||||
|
19,
|
||||||
|
16.4106,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.0749,
|
||||||
|
15,
|
||||||
|
17.308,
|
||||||
|
37,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.1377,
|
||||||
|
67,
|
||||||
|
99.7245,
|
||||||
|
85,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
89,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
100,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.5278,
|
||||||
|
83,
|
||||||
|
9.1662,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
114,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
114,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
114,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
173,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
184,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
168,
|
||||||
|
85.3994,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
198,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
198,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
198,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
257,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
268,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
251,
|
||||||
|
85.3994,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
281,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
281,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
281,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 484,
|
||||||
|
"width": 484,
|
||||||
|
"height": 309,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
309,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.9256,
|
||||||
|
15,
|
||||||
|
24.6158,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1785,
|
||||||
|
15,
|
||||||
|
23.8959,
|
||||||
|
29,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.2066,
|
||||||
|
59,
|
||||||
|
99.5868,
|
||||||
|
83,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
80,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
91,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.7917,
|
||||||
|
76,
|
||||||
|
12.9358,
|
||||||
|
24,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
105,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
105,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
105,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
164,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
175,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
158,
|
||||||
|
78.0992,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
188,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
188,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
188,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
247,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
258,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
242,
|
||||||
|
78.0992,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
271,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
271,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
271,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "e057ca7b36a30c5971a4225ec3ad4680"
|
||||||
|
}
|
||||||
707
src/admin/bones/invoice-detail.bones.json
Normal file
707
src/admin/bones/invoice-detail.bones.json
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 466,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
3.4188,
|
||||||
|
12,
|
||||||
|
5.698,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8148,
|
||||||
|
9,
|
||||||
|
30.0881,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
30.6713,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.0901,
|
||||||
|
52,
|
||||||
|
31.2455,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.7545,
|
||||||
|
52,
|
||||||
|
31.2455,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
100,
|
||||||
|
152,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
125,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
154,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
125,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
154,
|
||||||
|
44.0171,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
197,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
197,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
226,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
280,
|
||||||
|
100,
|
||||||
|
186,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
293,
|
||||||
|
92.5926,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
322,
|
||||||
|
33.3066,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
322,
|
||||||
|
35.1718,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
322,
|
||||||
|
36.9836,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
322,
|
||||||
|
36.9881,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
353,
|
||||||
|
33.3066,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
353,
|
||||||
|
35.1718,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
353,
|
||||||
|
36.9836,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
353,
|
||||||
|
36.9881,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
387,
|
||||||
|
33.3066,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
387,
|
||||||
|
35.1718,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
387,
|
||||||
|
36.9836,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
387,
|
||||||
|
36.9881,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
420,
|
||||||
|
33.3066,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
420,
|
||||||
|
35.1718,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
420,
|
||||||
|
36.9836,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
420,
|
||||||
|
36.9881,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 444,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
1.6304,
|
||||||
|
12,
|
||||||
|
2.7174,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.0652,
|
||||||
|
7,
|
||||||
|
17.5378,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.9692,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.7729,
|
||||||
|
0,
|
||||||
|
13.6379,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.0413,
|
||||||
|
0,
|
||||||
|
10.9587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
164,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
151,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
180,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
151,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
180,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
100,
|
||||||
|
204,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
259,
|
||||||
|
94.837,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
288,
|
||||||
|
22.3803,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
288,
|
||||||
|
23.5882,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
288,
|
||||||
|
24.431,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
288,
|
||||||
|
24.4374,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
321,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
321,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
321,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
321,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
356,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
356,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
356,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
356,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
391,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
391,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
391,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
391,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 457,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0.6024,
|
||||||
|
7,
|
||||||
|
2.008,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.0161,
|
||||||
|
2,
|
||||||
|
12.9597,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.8407,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.4226,
|
||||||
|
0,
|
||||||
|
9.6762,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
92.3036,
|
||||||
|
0,
|
||||||
|
7.6964,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
160,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
24,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
165,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
138,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
165,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
226,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
245,
|
||||||
|
96.1847,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
273,
|
||||||
|
22.9606,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
273,
|
||||||
|
24.065,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
273,
|
||||||
|
24.578,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
273,
|
||||||
|
24.5811,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
311,
|
||||||
|
22.9606,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
311,
|
||||||
|
24.065,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
311,
|
||||||
|
24.578,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
311,
|
||||||
|
24.5811,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
354,
|
||||||
|
22.9606,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
354,
|
||||||
|
24.065,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
354,
|
||||||
|
24.578,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
354,
|
||||||
|
24.5811,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
22.9606,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
396,
|
||||||
|
24.065,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
396,
|
||||||
|
24.578,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
396,
|
||||||
|
24.5811,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "934452d45a0bef9320dc379fb3f43bb5"
|
||||||
|
}
|
||||||
599
src/admin/bones/leave-approval.bones.json
Normal file
599
src/admin/bones/leave-approval.bones.json
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 294,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
233,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
19.2753,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
74,
|
||||||
|
18.7812,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
74,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
74,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
74,
|
||||||
|
9.4373,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
74,
|
||||||
|
54.4471,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
105,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
105,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
105,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
105,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
105,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
105,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
159,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
159,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
159,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
159,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
159,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
159,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
213,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
213,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
213,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
213,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
213,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
213,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 299,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
29.4264,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
29.4264,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
12.6911,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
86,
|
||||||
|
12.3769,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
86,
|
||||||
|
15.0921,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
86,
|
||||||
|
15.0921,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
86,
|
||||||
|
6.4856,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
86,
|
||||||
|
33.0991,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
119,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
119,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
119,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
119,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
119,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
119,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
173,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
173,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
173,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
173,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
173,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
173,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
227,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
227,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
227,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
227,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
227,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
227,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 299,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
21.7448,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
21.7448,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
13.8664,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
86,
|
||||||
|
13.5589,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
86,
|
||||||
|
16.196,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
86,
|
||||||
|
16.196,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
86,
|
||||||
|
7.8878,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
86,
|
||||||
|
28.4795,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
124,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
124,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
124,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
124,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
124,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
124,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
176,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
176,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
176,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
176,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
176,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
228,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
228,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
228,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
228,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
228,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
228,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "4b74917f659334073252a738cfa9c4ac"
|
||||||
|
}
|
||||||
704
src/admin/bones/leave-requests.bones.json
Normal file
704
src/admin/bones/leave-requests.bones.json
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 367,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
254,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
21.1806,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
126,
|
||||||
|
20.6375,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
126,
|
||||||
|
25.3294,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
126,
|
||||||
|
25.3294,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
126,
|
||||||
|
10.3677,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
126,
|
||||||
|
20.7977,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
126,
|
||||||
|
18.8079,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
157,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
157,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
157,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
157,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
157,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
157,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
157,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
218,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
218,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
218,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
218,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
218,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
218,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
218,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
279,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
279,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
279,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
279,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
279,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
279,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
279,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 320,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
27.1888,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
27.1888,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.6914,
|
||||||
|
4,
|
||||||
|
16.3086,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
253,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
14.313,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
86,
|
||||||
|
13.9585,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
86,
|
||||||
|
17.0219,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
86,
|
||||||
|
17.0219,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
86,
|
||||||
|
7.3157,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
86,
|
||||||
|
13.2006,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
86,
|
||||||
|
12.0053,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
119,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
119,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
119,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
119,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
119,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
119,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
119,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
180,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
180,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
180,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
180,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
180,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
180,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
180,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
241,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
241,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
241,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
241,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
241,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
241,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
241,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 308,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
20.0913,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
20.0913,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
88.3503,
|
||||||
|
10,
|
||||||
|
11.6497,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
241,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
14.9787,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
86,
|
||||||
|
14.6461,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
86,
|
||||||
|
17.495,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
86,
|
||||||
|
17.495,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
86,
|
||||||
|
8.52,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
86,
|
||||||
|
12.74,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
86,
|
||||||
|
10.31,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
124,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
124,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
124,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
124,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
124,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
124,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
124,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
179,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
179,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
179,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
179,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
179,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
179,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
179,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
234,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
234,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
234,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
234,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
234,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
234,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
234,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "125231cbf4c6abc4e73bb48732dc9353"
|
||||||
|
}
|
||||||
620
src/admin/bones/offer-detail.bones.json
Normal file
620
src/admin/bones/offer-detail.bones.json
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 483,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.5356,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
86,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
146,
|
||||||
|
100,
|
||||||
|
338,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
159,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
187,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
159,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
187,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
249,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
249,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
278,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
34.4062,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
339,
|
||||||
|
34.8157,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
339,
|
||||||
|
36.6097,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
339,
|
||||||
|
36.6186,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
370,
|
||||||
|
34.4062,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
370,
|
||||||
|
34.8157,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
370,
|
||||||
|
36.6097,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
370,
|
||||||
|
36.6186,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
404,
|
||||||
|
34.4062,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
404,
|
||||||
|
34.8157,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
404,
|
||||||
|
36.6097,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
404,
|
||||||
|
36.6186,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
437,
|
||||||
|
34.4062,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
437,
|
||||||
|
34.8157,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
437,
|
||||||
|
36.6097,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
437,
|
||||||
|
36.6186,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 416,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
5.9783,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.4829,
|
||||||
|
7,
|
||||||
|
19.8412,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
90.8267,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
356,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
169,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
198,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
169,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
198,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
260,
|
||||||
|
22.8558,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
260,
|
||||||
|
23.4333,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
260,
|
||||||
|
24.2718,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
260,
|
||||||
|
24.2761,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
292,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
292,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
292,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
292,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
327,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
327,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
327,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
327,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
362,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
362,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
362,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 419,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
3.2129,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0878,
|
||||||
|
1,
|
||||||
|
14.6618,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
93.6229,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
48,
|
||||||
|
100,
|
||||||
|
371,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
67,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
94,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
67,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
94,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
151,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
178,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
151,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
178,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
235,
|
||||||
|
23.2194,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
235,
|
||||||
|
23.9787,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
235,
|
||||||
|
24.4917,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
235,
|
||||||
|
24.4949,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
273,
|
||||||
|
23.2194,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
273,
|
||||||
|
23.9787,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
273,
|
||||||
|
24.4917,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
273,
|
||||||
|
24.4949,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
316,
|
||||||
|
23.2194,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
316,
|
||||||
|
23.9787,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
316,
|
||||||
|
24.4917,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
316,
|
||||||
|
24.4949,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
358,
|
||||||
|
23.2194,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
358,
|
||||||
|
23.9787,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
358,
|
||||||
|
24.4917,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
358,
|
||||||
|
24.4949,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "67676fde7dd5c432922d819fc9bf48db"
|
||||||
|
}
|
||||||
641
src/admin/bones/offers-customers.bones.json
Normal file
641
src/admin/bones/offers-customers.bones.json
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 549,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
186,
|
||||||
|
34.562,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
186,
|
||||||
|
23.7936,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
186,
|
||||||
|
32.4653,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
186,
|
||||||
|
51.6293,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
217,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
217,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
217,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
217,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
278,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
278,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
278,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
339,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
339,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
339,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
400,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
400,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
400,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
400,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
461,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
461,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
461,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
461,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.9458,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.9458,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.6322,
|
||||||
|
4,
|
||||||
|
19.3678,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
23.2868,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
146,
|
||||||
|
16.453,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
146,
|
||||||
|
21.9175,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
146,
|
||||||
|
33.1798,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
179,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
179,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
179,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
240,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
240,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
240,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
301,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
301,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
301,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
362,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
362,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
362,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
423,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
423,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
423,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
9.5664,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
9.5664,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.0897,
|
||||||
|
10,
|
||||||
|
13.9103,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
25.673,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
138,
|
||||||
|
19.0904,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
138,
|
||||||
|
24.3254,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
138,
|
||||||
|
27.0959,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
176,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
176,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
176,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
231,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
231,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
231,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
286,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
286,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
286,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
341,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
341,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
341,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
396,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
396,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
396,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "63b2dec2b6ceb84d931a000ab8b669dd"
|
||||||
|
}
|
||||||
452
src/admin/bones/offers-templates.bones.json
Normal file
452
src/admin/bones/offers-templates.bones.json
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 436,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
13,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
73,
|
||||||
|
39.659,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
73,
|
||||||
|
39.6857,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
73,
|
||||||
|
63.1054,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
104,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
104,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
104,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
165,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
165,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
226,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
226,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
287,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
287,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
348,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
348,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 435,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
19,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
26.9744,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
79,
|
||||||
|
26.9977,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
79,
|
||||||
|
40.8649,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
112,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
112,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
112,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
173,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
173,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
173,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
234,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
234,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
234,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
295,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
295,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
295,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
356,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
356,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
356,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 403,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
19,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
71,
|
||||||
|
30.8719,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
71,
|
||||||
|
30.897,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
71,
|
||||||
|
34.4158,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
109,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
109,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
109,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
164,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
164,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
164,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
219,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
219,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
219,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
274,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
274,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
274,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
329,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
329,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
329,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "5e5881859bd932a42345c69a6a30ca65"
|
||||||
|
}
|
||||||
872
src/admin/bones/offers.bones.json
Normal file
872
src/admin/bones/offers.bones.json
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
26.7495,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
134,
|
||||||
|
20.6019,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
134,
|
||||||
|
21.4165,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
134,
|
||||||
|
30.7692,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
165,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
165,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
165,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
226,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
226,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
226,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
287,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
287,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
287,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
348,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
348,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
348,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
409,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
409,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
409,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.3451,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.3451,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
17.6779,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
146,
|
||||||
|
13.7101,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
146,
|
||||||
|
13.3301,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
146,
|
||||||
|
15.2917,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
146,
|
||||||
|
15.2917,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
146,
|
||||||
|
19.5355,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
179,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
179,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
179,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
179,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
179,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
240,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
240,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
240,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
240,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
240,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
301,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
301,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
301,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
301,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
301,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
362,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
362,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
362,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
362,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
362,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
423,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
423,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
423,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
423,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
423,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
8.3835,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
8.3835,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
18.8912,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
138,
|
||||||
|
15.0085,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
138,
|
||||||
|
13.333,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
138,
|
||||||
|
16.5553,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
138,
|
||||||
|
16.5553,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
138,
|
||||||
|
15.8415,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
176,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
176,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
176,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
176,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
176,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
231,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
231,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
231,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
231,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
231,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
286,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
286,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
286,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
286,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
286,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
341,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
341,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
341,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
341,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
341,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
396,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
396,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
396,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
396,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
396,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "62d793eb0343d832087d687b76639e09"
|
||||||
|
}
|
||||||
998
src/admin/bones/orders.bones.json
Normal file
998
src/admin/bones/orders.bones.json
Normal file
@@ -0,0 +1,998 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
26.7495,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
134,
|
||||||
|
29.1043,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
134,
|
||||||
|
20.6019,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
134,
|
||||||
|
26.6515,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
134,
|
||||||
|
21.2028,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
134,
|
||||||
|
17.094,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
165,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
165,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
165,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
165,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
165,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
226,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
226,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
226,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
226,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
226,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
287,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
287,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
287,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
287,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
287,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
348,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
348,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
348,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
348,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
348,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
409,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
409,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
409,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
409,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
409,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.6461,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.6461,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
15.642,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
146,
|
||||||
|
16.9837,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
146,
|
||||||
|
12.1306,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
146,
|
||||||
|
14.5338,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
146,
|
||||||
|
13.5296,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
146,
|
||||||
|
12.4745,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
146,
|
||||||
|
9.5427,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
179,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
179,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
179,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
179,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
179,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
179,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
240,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
240,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
240,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
240,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
240,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
240,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
301,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
301,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
301,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
301,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
301,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
301,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
362,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
362,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
362,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
362,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
362,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
362,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
423,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
423,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
423,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
423,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
423,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
423,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.3008,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.3008,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
16.2243,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
138,
|
||||||
|
17.5044,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
138,
|
||||||
|
12.8891,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
138,
|
||||||
|
13.7535,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
138,
|
||||||
|
14.2178,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
138,
|
||||||
|
13.2138,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
138,
|
||||||
|
8.382,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
176,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
176,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
176,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
176,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
176,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
176,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
231,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
231,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
231,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
231,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
231,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
231,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
286,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
286,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
286,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
286,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
286,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
286,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
341,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
341,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
341,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
341,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
341,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
341,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
396,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
396,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
396,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
396,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
396,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
396,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "677a0002aa805c9f7790bc68c6374bb5"
|
||||||
|
}
|
||||||
371
src/admin/bones/project-detail.bones.json
Normal file
371
src/admin/bones/project-detail.bones.json
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 481,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
3.4188,
|
||||||
|
12,
|
||||||
|
5.698,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8148,
|
||||||
|
9,
|
||||||
|
50.2315,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
48.0057,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.4245,
|
||||||
|
52,
|
||||||
|
48.5755,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
100,
|
||||||
|
188,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
125,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
154,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
125,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
154,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
214,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
243,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
214,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
243,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
316,
|
||||||
|
100,
|
||||||
|
165,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
329,
|
||||||
|
92.5926,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
357,
|
||||||
|
92.5926,
|
||||||
|
104,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 453,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
1.6304,
|
||||||
|
12,
|
||||||
|
2.7174,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.0652,
|
||||||
|
7,
|
||||||
|
29.2799,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
78.2375,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.0413,
|
||||||
|
0,
|
||||||
|
10.9587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
168,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
197,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
168,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
197,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
276,
|
||||||
|
100,
|
||||||
|
177,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
295,
|
||||||
|
94.837,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
323,
|
||||||
|
94.837,
|
||||||
|
104,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 404,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0.6024,
|
||||||
|
7,
|
||||||
|
2.008,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.0161,
|
||||||
|
2,
|
||||||
|
21.6365,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7217,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
92.3036,
|
||||||
|
0,
|
||||||
|
7.6964,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
180,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
148,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
175,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
148,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
175,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
246,
|
||||||
|
100,
|
||||||
|
157,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
265,
|
||||||
|
96.1847,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
294,
|
||||||
|
96.1847,
|
||||||
|
84,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "ab5e1f108d42c55b0e6382fcaffff793"
|
||||||
|
}
|
||||||
746
src/admin/bones/projects.bones.json
Normal file
746
src/admin/bones/projects.bones.json
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
34.6065,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
134,
|
||||||
|
31.3568,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
134,
|
||||||
|
26.6515,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
134,
|
||||||
|
27.7066,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
134,
|
||||||
|
22.1287,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
165,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
165,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
165,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
165,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
226,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
226,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
226,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
226,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
287,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
287,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
287,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
287,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
348,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
348,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
348,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
348,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
409,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
409,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
409,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
409,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
10.9099,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
10.9099,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
23.429,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
146,
|
||||||
|
21.2806,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
146,
|
||||||
|
18.1704,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
146,
|
||||||
|
17.6694,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
146,
|
||||||
|
14.2875,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
179,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
179,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
179,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
179,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
240,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
240,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
240,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
240,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
301,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
301,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
301,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
301,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
362,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
362,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
362,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
362,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
423,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
423,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
423,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
423,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
8.0619,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
8.0619,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
24.4588,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
138,
|
||||||
|
22.4068,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
138,
|
||||||
|
19.4308,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
138,
|
||||||
|
17.2612,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
138,
|
||||||
|
12.6271,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
176,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
176,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
176,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
176,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
231,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
231,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
231,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
231,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
286,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
286,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
286,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
286,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
341,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
341,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
341,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
341,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
396,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
396,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
396,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
396,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "17f8285c3ca514ddef6d48c1183ed642"
|
||||||
|
}
|
||||||
50
src/admin/bones/registry.ts
Normal file
50
src/admin/bones/registry.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client"
|
||||||
|
// Auto-generated by `npx boneyard-js build` — do not edit
|
||||||
|
import { registerBones } from 'boneyard-js'
|
||||||
|
import { configureBoneyard } from 'boneyard-js/react'
|
||||||
|
|
||||||
|
import _dash_sessions from './dash-sessions.bones.json'
|
||||||
|
import _attendance_history_fund from './attendance-history-fund.bones.json'
|
||||||
|
import _attendance_history_table from './attendance-history-table.bones.json'
|
||||||
|
import _leave_requests from './leave-requests.bones.json'
|
||||||
|
import _leave_approval from './leave-approval.bones.json'
|
||||||
|
import _attendance_balances from './attendance-balances.bones.json'
|
||||||
|
import _trips_history from './trips-history.bones.json'
|
||||||
|
import _trips_admin from './trips-admin.bones.json'
|
||||||
|
import _vehicles from './vehicles.bones.json'
|
||||||
|
import _offers from './offers.bones.json'
|
||||||
|
import _orders from './orders.bones.json'
|
||||||
|
import _projects from './projects.bones.json'
|
||||||
|
import _offers_customers from './offers-customers.bones.json'
|
||||||
|
import _users from './users.bones.json'
|
||||||
|
import _audit_log_rows from './audit-log-rows.bones.json'
|
||||||
|
import _offer_detail from './offer-detail.bones.json'
|
||||||
|
import _invoice_detail from './invoice-detail.bones.json'
|
||||||
|
import _project_detail from './project-detail.bones.json'
|
||||||
|
import _attendance_create from './attendance-create.bones.json'
|
||||||
|
import _offers_templates from './offers-templates.bones.json'
|
||||||
|
|
||||||
|
configureBoneyard({"color":"#e0e0e0","animate":"shimmer","shimmerColor":"#f0f0f0","speed":"1.2s","shimmerAngle":110})
|
||||||
|
|
||||||
|
registerBones({
|
||||||
|
"dash-sessions": _dash_sessions,
|
||||||
|
"attendance-history-fund": _attendance_history_fund,
|
||||||
|
"attendance-history-table": _attendance_history_table,
|
||||||
|
"leave-requests": _leave_requests,
|
||||||
|
"leave-approval": _leave_approval,
|
||||||
|
"attendance-balances": _attendance_balances,
|
||||||
|
"trips-history": _trips_history,
|
||||||
|
"trips-admin": _trips_admin,
|
||||||
|
"vehicles": _vehicles,
|
||||||
|
"offers": _offers,
|
||||||
|
"orders": _orders,
|
||||||
|
"projects": _projects,
|
||||||
|
"offers-customers": _offers_customers,
|
||||||
|
"users": _users,
|
||||||
|
"audit-log-rows": _audit_log_rows,
|
||||||
|
"offer-detail": _offer_detail,
|
||||||
|
"invoice-detail": _invoice_detail,
|
||||||
|
"project-detail": _project_detail,
|
||||||
|
"attendance-create": _attendance_create,
|
||||||
|
"offers-templates": _offers_templates,
|
||||||
|
})
|
||||||
725
src/admin/bones/trips-admin.bones.json
Normal file
725
src/admin/bones/trips-admin.bones.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 317,
|
||||||
|
"width": 317,
|
||||||
|
"height": 437,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
376,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
74,
|
||||||
|
37.8795,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
74,
|
||||||
|
43.4592,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
74,
|
||||||
|
31.6739,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
74,
|
||||||
|
16.6108,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
74,
|
||||||
|
28.1053,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
105,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
105,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
105,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
105,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
105,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
166,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
166,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
166,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
166,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
166,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
227,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
227,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
227,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
227,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
227,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
288,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
288,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
288,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
288,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
288,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
349,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
349,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
349,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
349,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
349,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 690,
|
||||||
|
"width": 690,
|
||||||
|
"height": 442,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.3202,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.3202,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
375,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
86,
|
||||||
|
22.8057,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
86,
|
||||||
|
26.0711,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
86,
|
||||||
|
19.1757,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
86,
|
||||||
|
10.3601,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
86,
|
||||||
|
16.0802,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
119,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
119,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
119,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
119,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
119,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
180,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
180,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
180,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
180,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
180,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
241,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
241,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
241,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
241,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
241,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
302,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
302,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
302,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
302,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
302,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
363,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
363,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
363,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
363,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
363,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 950,
|
||||||
|
"width": 950,
|
||||||
|
"height": 418,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.8536,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.8536,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
351,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
86,
|
||||||
|
23.523,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
86,
|
||||||
|
26.574,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
86,
|
||||||
|
20.1382,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
86,
|
||||||
|
11.9046,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
86,
|
||||||
|
13.8602,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
124,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
124,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
124,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
124,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
124,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
179,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
179,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
179,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
179,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
179,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
234,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
234,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
234,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
234,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
234,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
289,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
289,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
289,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
289,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
289,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
344,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
344,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
344,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
344,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
344,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "39a325f430c84bb51960a684759a8f0c"
|
||||||
|
}
|
||||||
725
src/admin/bones/trips-history.bones.json
Normal file
725
src/admin/bones/trips-history.bones.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 317,
|
||||||
|
"width": 317,
|
||||||
|
"height": 300,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
239,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
74,
|
||||||
|
35.2326,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
74,
|
||||||
|
40.4278,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
74,
|
||||||
|
29.4657,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
74,
|
||||||
|
37.1402,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
74,
|
||||||
|
15.4623,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
105,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
105,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
105,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
105,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
105,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
138,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
138,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
138,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
138,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
138,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
172,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
172,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
172,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
172,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
172,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
205,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
205,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
205,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
205,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
205,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
239,
|
||||||
|
35.2326,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
239,
|
||||||
|
40.4278,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
239,
|
||||||
|
29.4657,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
239,
|
||||||
|
37.1402,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
239,
|
||||||
|
15.4623,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 690,
|
||||||
|
"width": 690,
|
||||||
|
"height": 312,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.6033,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.6033,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
245,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
86,
|
||||||
|
21.0417,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
86,
|
||||||
|
24.0534,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
86,
|
||||||
|
17.6925,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
86,
|
||||||
|
22.1445,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
86,
|
||||||
|
9.5607,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
119,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
119,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
119,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
119,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
119,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
154,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
154,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
154,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
154,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
154,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
189,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
189,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
189,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
189,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
189,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
224,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
224,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
224,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
224,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
224,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
259,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
259,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
259,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
259,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
259,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 958,
|
||||||
|
"width": 958,
|
||||||
|
"height": 355,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.9585,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.9585,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
288,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
86,
|
||||||
|
21.1541,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
86,
|
||||||
|
23.8974,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
86,
|
||||||
|
18.1106,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
86,
|
||||||
|
22.1604,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
86,
|
||||||
|
10.7108,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
124,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
124,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
124,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
124,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
124,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
167,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
167,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
167,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
167,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
167,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
209,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
209,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
209,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
209,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
209,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
252,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
252,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
252,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
252,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
252,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
294,
|
||||||
|
21.1541,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
294,
|
||||||
|
23.8974,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
294,
|
||||||
|
18.1106,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
294,
|
||||||
|
22.1604,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
294,
|
||||||
|
10.7108,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "6b54a0afbb4863895e318916b1fdca67"
|
||||||
|
}
|
||||||
767
src/admin/bones/users.bones.json
Normal file
767
src/admin/bones/users.bones.json
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 549,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
186,
|
||||||
|
37.362,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
186,
|
||||||
|
26.429,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
186,
|
||||||
|
36.1779,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
186,
|
||||||
|
23.62,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
186,
|
||||||
|
18.8613,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
217,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
217,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
217,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
217,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
217,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
278,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
278,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
278,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
278,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
339,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
339,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
339,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
339,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
400,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
400,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
400,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
400,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
400,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
461,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
461,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
461,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
461,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
461,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.6741,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.6741,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.2479,
|
||||||
|
4,
|
||||||
|
18.7521,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
24.3079,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
146,
|
||||||
|
18.6481,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
146,
|
||||||
|
23.5628,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
146,
|
||||||
|
15.6568,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
146,
|
||||||
|
12.6613,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
179,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
179,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
179,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
179,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
240,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
240,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
240,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
240,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
301,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
301,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
301,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
301,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
362,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
362,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
362,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
362,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
423,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
423,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
423,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
423,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 505,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
9.3656,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
9.3656,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.5446,
|
||||||
|
10,
|
||||||
|
13.4554,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
438,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
25.3655,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
138,
|
||||||
|
20.4302,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
138,
|
||||||
|
22.8571,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
138,
|
||||||
|
15.9011,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
138,
|
||||||
|
11.6309,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
176,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
176,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
176,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
176,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
238,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
238,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
238,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
238,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
238,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
300,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
300,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
300,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
300,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
300,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
362,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
362,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
362,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
362,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
362,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
424,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
424,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
424,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
424,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
424,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "53e8df6c8f8bf975b3b88bfca3bbd804"
|
||||||
|
}
|
||||||
746
src/admin/bones/vehicles.bones.json
Normal file
746
src/admin/bones/vehicles.bones.json
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 530,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
34,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
94,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
107,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
167,
|
||||||
|
23.9583,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
167,
|
||||||
|
24.4168,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
167,
|
||||||
|
29.042,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
167,
|
||||||
|
18.8435,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
167,
|
||||||
|
46.1895,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
197,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
197,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
197,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
197,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
197,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
258,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
258,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
258,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
258,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
258,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
319,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
319,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
319,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
319,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
319,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
380,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
380,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
380,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
380,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
380,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
441,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
441,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
441,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
441,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
441,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 495,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
10.1478,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.6427,
|
||||||
|
0,
|
||||||
|
17.3573,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
139,
|
||||||
|
16.4126,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
139,
|
||||||
|
16.5039,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
139,
|
||||||
|
19.5058,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
139,
|
||||||
|
12.8843,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
139,
|
||||||
|
29.5304,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
172,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
172,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
172,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
172,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
172,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
233,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
233,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
233,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
233,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
233,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
294,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
294,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
294,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
294,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
294,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
355,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
355,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
355,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
355,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
355,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
416,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
416,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
416,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
416,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
416,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 451,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
7.4987,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.5753,
|
||||||
|
0,
|
||||||
|
12.4247,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
48,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
67,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
119,
|
||||||
|
18.3531,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
119,
|
||||||
|
18.2762,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
119,
|
||||||
|
21.1785,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
119,
|
||||||
|
14.7841,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
119,
|
||||||
|
23.5928,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
157,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
157,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
157,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
157,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
157,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
212,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
212,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
212,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
212,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
212,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
267,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
267,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
267,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
267,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
267,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
322,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
322,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
322,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
322,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
322,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
377,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
377,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
377,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
377,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
377,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "567bad6080dc9ba9767c6e40a88559b9"
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { projectFilesOptions } from "../lib/queries/projects";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import ConfirmModal from "./ConfirmModal";
|
import ConfirmModal from "./ConfirmModal";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import ProjectFileManagerFixture from "../fixtures/ProjectFileManagerFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -196,14 +200,11 @@ export default function ProjectFileManager({
|
|||||||
hasNasFolder,
|
hasNasFolder,
|
||||||
}: ProjectFileManagerProps) {
|
}: ProjectFileManagerProps) {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const isCancelling = useRef(false);
|
const isCancelling = useRef(false);
|
||||||
|
|
||||||
const [items, setItems] = useState<FileItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [currentPath, setCurrentPath] = useState("");
|
const [currentPath, setCurrentPath] = useState("");
|
||||||
const [breadcrumb, setBreadcrumb] = useState<string[]>([""]);
|
|
||||||
const [fullPath, setFullPath] = useState("");
|
|
||||||
|
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -217,59 +218,25 @@ export default function ProjectFileManager({
|
|||||||
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const canManage = hasPermission("projects.files");
|
const canManage = hasPermission("projects.files");
|
||||||
|
|
||||||
const fetchFiles = useCallback(
|
const {
|
||||||
async (path = "", options: { ignore?: boolean } = {}) => {
|
data: filesData,
|
||||||
setLoading(true);
|
isPending: filesLoading,
|
||||||
setErrorMessage(null);
|
error: filesError,
|
||||||
try {
|
} = useQuery(projectFilesOptions(projectId, currentPath));
|
||||||
const params = new URLSearchParams({ project_id: String(projectId) });
|
const items = filesData?.items ?? [];
|
||||||
if (path) {
|
const breadcrumb = filesData?.breadcrumb ?? [""];
|
||||||
params.set("path", path);
|
const fullPath = filesData?.full_path ?? "";
|
||||||
}
|
const errorMessage = filesError
|
||||||
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
|
? filesError.message || "Nepodařilo se načíst soubory"
|
||||||
if (options.ignore) return;
|
: null;
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
setItems(data.data.items || []);
|
|
||||||
setBreadcrumb(data.data.breadcrumb || [""]);
|
|
||||||
setCurrentPath(data.data.path || "");
|
|
||||||
setFullPath(data.data.full_path || "");
|
|
||||||
} else if (res.status === 404) {
|
|
||||||
setItems([]);
|
|
||||||
setBreadcrumb([""]);
|
|
||||||
} else {
|
|
||||||
setErrorMessage(data.error || "Nepodařilo se načíst soubory");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!options.ignore) {
|
|
||||||
setErrorMessage("Chyba připojení");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!options.ignore) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectId],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const opts = { ignore: false };
|
|
||||||
fetchFiles("", opts);
|
|
||||||
return () => {
|
|
||||||
opts.ignore = true;
|
|
||||||
};
|
|
||||||
}, [fetchFiles]);
|
|
||||||
|
|
||||||
const navigateTo = (path: string) => {
|
const navigateTo = (path: string) => {
|
||||||
setNewFolderMode(false);
|
setNewFolderMode(false);
|
||||||
setRenamingItem(null);
|
setRenamingItem(null);
|
||||||
fetchFiles(path);
|
setCurrentPath(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBreadcrumbClick = (index: number) => {
|
const handleBreadcrumbClick = (index: number) => {
|
||||||
@@ -332,7 +299,9 @@ export default function ProjectFileManager({
|
|||||||
? "Soubor byl nahrán"
|
? "Soubor byl nahrán"
|
||||||
: `Nahráno ${successCount} souborů`;
|
: `Nahráno ${successCount} souborů`;
|
||||||
alert.success(msg);
|
alert.success(msg);
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
alert.error(errorMsg);
|
alert.error(errorMsg);
|
||||||
@@ -383,7 +352,9 @@ export default function ProjectFileManager({
|
|||||||
alert.success("Složka byla vytvořena");
|
alert.success("Složka byla vytvořena");
|
||||||
setNewFolderMode(false);
|
setNewFolderMode(false);
|
||||||
setNewFolderName("");
|
setNewFolderName("");
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se vytvořit složku");
|
alert.error(data.error || "Nepodařilo se vytvořit složku");
|
||||||
}
|
}
|
||||||
@@ -444,7 +415,9 @@ export default function ProjectFileManager({
|
|||||||
? "Složka byla smazána"
|
? "Složka byla smazána"
|
||||||
: "Soubor byl smazán",
|
: "Soubor byl smazán",
|
||||||
);
|
);
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se smazat");
|
alert.error(data.error || "Nepodařilo se smazat");
|
||||||
}
|
}
|
||||||
@@ -479,7 +452,9 @@ export default function ProjectFileManager({
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success("Přejmenováno");
|
alert.success("Přejmenováno");
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se přejmenovat");
|
alert.error(data.error || "Nepodařilo se přejmenovat");
|
||||||
}
|
}
|
||||||
@@ -495,32 +470,15 @@ export default function ProjectFileManager({
|
|||||||
setRenameValue(item.name);
|
setRenameValue(item.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && items.length === 0 && !errorMessage) {
|
if (filesLoading && items.length === 0 && !errorMessage) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-card">
|
<Skeleton
|
||||||
<div className="admin-card-body">
|
name="project-file-manager"
|
||||||
<h3 className="admin-card-title">Soubory</h3>
|
loading={filesLoading && items.length === 0}
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "0.5rem" }}>
|
fixture={<ProjectFileManagerFixture />}
|
||||||
{[0, 1, 2, 3].map((i) => (
|
>
|
||||||
<div key={i} className="admin-skeleton-row">
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: `${60 + i * 10}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +668,7 @@ export default function ProjectFileManager({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{items.length === 0 && !loading ? (
|
{items.length === 0 && !filesLoading ? (
|
||||||
<div className="fm-empty">
|
<div className="fm-empty">
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef, useCallback, useEffect } from "react";
|
import { useMemo, useRef, useCallback, useLayoutEffect } from "react";
|
||||||
import ReactQuill from "react-quill-new";
|
import ReactQuill from "react-quill-new";
|
||||||
import "react-quill-new/dist/quill.snow.css";
|
import "react-quill-new/dist/quill.snow.css";
|
||||||
|
|
||||||
@@ -96,11 +96,14 @@ export default function RichEditor({
|
|||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!quillRef.current) return;
|
if (!quillRef.current) return;
|
||||||
const editor = quillRef.current.getEditor();
|
const editor = quillRef.current.getEditor();
|
||||||
editor.format("font", "tahoma");
|
editor.format("font", "tahoma");
|
||||||
editor.format("size", "14px");
|
editor.format("size", "14px");
|
||||||
|
// Quill auto-focuses on mount with existing content, which scrolls
|
||||||
|
// the page to the editor. Blur to prevent unwanted scroll.
|
||||||
|
editor.blur();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAlert } from "../../context/AlertContext";
|
import { useAlert } from "../../context/AlertContext";
|
||||||
import ConfirmModal from "../ConfirmModal";
|
import ConfirmModal from "../ConfirmModal";
|
||||||
import useModalLock from "../../hooks/useModalLock";
|
import useModalLock from "../../hooks/useModalLock";
|
||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
||||||
|
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import DashSessionsFixture from "../../fixtures/DashSessionsFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface DeviceInfo {
|
|
||||||
icon?: string;
|
|
||||||
browser?: string;
|
|
||||||
os?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Session {
|
|
||||||
id: number | string;
|
|
||||||
is_current: boolean;
|
|
||||||
device_info?: DeviceInfo;
|
|
||||||
ip_address: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteModalState {
|
interface DeleteModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
@@ -77,9 +67,10 @@ function getDeviceIcon(iconType?: string) {
|
|||||||
|
|
||||||
export default function DashSessions() {
|
export default function DashSessions() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [sessions, setSessions] = useState<Session[]>([]);
|
const { data: sessions = [], isPending: sessionsLoading } =
|
||||||
const [sessionsLoading, setSessionsLoading] = useState(true);
|
useQuery(sessionsOptions());
|
||||||
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({
|
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
session: null,
|
session: null,
|
||||||
@@ -89,26 +80,6 @@ export default function DashSessions() {
|
|||||||
|
|
||||||
useModalLock(deleteAllModal);
|
useModalLock(deleteAllModal);
|
||||||
|
|
||||||
const fetchSessions = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/sessions`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setSessions(
|
|
||||||
Array.isArray(data.data) ? data.data : data.data?.sessions || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// session fetch failed silently
|
|
||||||
} finally {
|
|
||||||
setSessionsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSessions();
|
|
||||||
}, [fetchSessions]);
|
|
||||||
|
|
||||||
const handleDeleteSession = async () => {
|
const handleDeleteSession = async () => {
|
||||||
if (!deleteModal.session) {
|
if (!deleteModal.session) {
|
||||||
return;
|
return;
|
||||||
@@ -122,7 +93,7 @@ export default function DashSessions() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setDeleteModal({ isOpen: false, session: null });
|
setDeleteModal({ isOpen: false, session: null });
|
||||||
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||||
alert.success("Relace byla ukončena");
|
alert.success("Relace byla ukončena");
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se ukončit relaci");
|
alert.error(data.error || "Nepodařilo se ukončit relaci");
|
||||||
@@ -143,7 +114,7 @@ export default function DashSessions() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setDeleteAllModal(false);
|
setDeleteAllModal(false);
|
||||||
setSessions((prev) => prev.filter((s) => s.is_current));
|
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||||
alert.success(data.message || "Ostatní relace byly ukončeny");
|
alert.success(data.message || "Ostatní relace byly ukončeny");
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se ukončit relace");
|
alert.error(data.error || "Nepodařilo se ukončit relace");
|
||||||
@@ -183,29 +154,13 @@ export default function DashSessions() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
{sessionsLoading && (
|
<Skeleton
|
||||||
<div
|
name="dash-sessions"
|
||||||
className="admin-skeleton"
|
loading={sessionsLoading}
|
||||||
style={{ padding: "1rem", gap: "1rem" }}
|
fixture={<DashSessionsFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2].map((i) => (
|
<>
|
||||||
<div key={i} className="admin-skeleton-row">
|
{sessions.length === 0 && (
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/2"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!sessionsLoading && sessions.length === 0 && (
|
|
||||||
<div
|
<div
|
||||||
className="text-secondary"
|
className="text-secondary"
|
||||||
style={{
|
style={{
|
||||||
@@ -217,7 +172,7 @@ export default function DashSessions() {
|
|||||||
Žádné aktivní relace
|
Žádné aktivní relace
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!sessionsLoading && sessions.length > 0 && (
|
{sessions.length > 0 && (
|
||||||
<div className="dash-sessions-list">
|
<div className="dash-sessions-list">
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<div
|
<div
|
||||||
@@ -275,6 +230,8 @@ export default function DashSessions() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const cachedUserRef = useRef<User | null>(null);
|
const cachedUserRef = useRef<User | null>(null);
|
||||||
const sessionFetchedRef = useRef(false);
|
const sessionFetchedRef = useRef(false);
|
||||||
const silentRefreshInFlightRef = useRef<Promise<boolean> | null>(null);
|
const silentRefreshInFlightRef = useRef<Promise<boolean> | null>(null);
|
||||||
|
const hadValidSessionRef = useRef(false);
|
||||||
const [user, setUser] = useState<User | null>(cachedUserRef.current);
|
const [user, setUser] = useState<User | null>(cachedUserRef.current);
|
||||||
const [loading, setLoading] = useState(!sessionFetchedRef.current);
|
const [loading, setLoading] = useState(!sessionFetchedRef.current);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -138,13 +139,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (data.success && data.data?.access_token) {
|
if (data.success && data.data?.access_token) {
|
||||||
setAccessTokenFn(data.data.access_token, data.data.expires_in);
|
setAccessTokenFn(data.data.access_token, data.data.expires_in);
|
||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
accessTokenRef.current = null;
|
accessTokenRef.current = null;
|
||||||
tokenExpiresAtRef.current = null;
|
tokenExpiresAtRef.current = null;
|
||||||
setUser(null);
|
setUser(null);
|
||||||
cachedUserRef.current = null;
|
cachedUserRef.current = null;
|
||||||
setSessionExpired();
|
if (hadValidSessionRef.current) setSessionExpired();
|
||||||
return false;
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
// Network error — don't kick the user out, just return false
|
// Network error — don't kick the user out, just return false
|
||||||
@@ -178,6 +180,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (data.data.access_token) setAccessTokenFn(data.data.access_token);
|
if (data.data.access_token) setAccessTokenFn(data.data.access_token);
|
||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +236,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
sessionFetchedRef.current = true;
|
sessionFetchedRef.current = true;
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
@@ -273,6 +277,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
sessionFetchedRef.current = true;
|
sessionFetchedRef.current = true;
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
@@ -302,6 +307,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
cachedUserRef.current = null;
|
cachedUserRef.current = null;
|
||||||
sessionFetchedRef.current = false;
|
sessionFetchedRef.current = false;
|
||||||
|
hadValidSessionRef.current = false;
|
||||||
if (refreshTimeoutRef.current) {
|
if (refreshTimeoutRef.current) {
|
||||||
clearTimeout(refreshTimeoutRef.current);
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
refreshTimeoutRef.current = null;
|
refreshTimeoutRef.current = null;
|
||||||
|
|||||||
69
src/admin/fixtures/AttendanceAdminFixture.tsx
Normal file
69
src/admin/fixtures/AttendanceAdminFixture.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default function AttendanceAdminFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa docházky</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-secondary">
|
||||||
|
Vyplnit měsíc
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-primary">Přidat záznam</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Měsíc</label>
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
<label className="admin-form-label">Zaměstnanec</label>
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-grid admin-grid-3">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div key={i} className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="flex-row gap-2 mb-2">
|
||||||
|
<span style={{ fontWeight: 600 }}>Jan Novák</span>
|
||||||
|
<span className="attendance-working-badge finished">
|
||||||
|
✗
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-value">8:00</div>
|
||||||
|
<div className="admin-stat-label">odpracováno</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Příchod</th>
|
||||||
|
<th>Odchod</th>
|
||||||
|
<th>Hodiny</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>1. 4. 2025</td>
|
||||||
|
<td className="admin-mono">08:00</td>
|
||||||
|
<td className="admin-mono">16:00</td>
|
||||||
|
<td className="admin-mono">8:00</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/admin/fixtures/AttendanceBalancesFixture.tsx
Normal file
104
src/admin/fixtures/AttendanceBalancesFixture.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
export default function AttendanceBalancesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa bilancí</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<select className="admin-form-select" style={{ minWidth: 100 }}>
|
||||||
|
<option>2025</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zaměstnanec</th>
|
||||||
|
<th>Nárok (h)</th>
|
||||||
|
<th>Čerpáno (h)</th>
|
||||||
|
<th>Zbývá (h)</th>
|
||||||
|
<th>Nemoc (h)</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="fw-500">Jan Novák</td>
|
||||||
|
<td className="admin-mono">160</td>
|
||||||
|
<td className="admin-mono">40.0</td>
|
||||||
|
<td className="admin-mono">120.0</td>
|
||||||
|
<td className="admin-mono">8.0</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon" title="Upravit">
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<h2 className="admin-page-title mb-4" style={{ fontSize: "1.25rem" }}>
|
||||||
|
Měsíční přehled fondu 2025
|
||||||
|
</h2>
|
||||||
|
<div className="admin-grid admin-grid-3">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div key={i} className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ fontWeight: 600, fontSize: "1rem", margin: 0 }}>
|
||||||
|
Duben
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.375rem",
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Jan Novák</span>
|
||||||
|
<span className="text-secondary">8h</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 3,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "75%",
|
||||||
|
background: "var(--gradient)",
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/admin/fixtures/AttendanceCreateFixture.tsx
Normal file
43
src/admin/fixtures/AttendanceCreateFixture.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export default function AttendanceCreateFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<h1 className="admin-page-title">Zapsat docházku</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ maxWidth: 600 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<FormField label="Uživatel">
|
||||||
|
<select className="admin-form-select">
|
||||||
|
<option>Jan Novák</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Datum">
|
||||||
|
<input type="date" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Příchod">
|
||||||
|
<input type="time" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Odchod">
|
||||||
|
<input type="time" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormField({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/admin/fixtures/AttendanceFixture.tsx
Normal file
79
src/admin/fixtures/AttendanceFixture.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export default function AttendanceFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Docházka</h1>
|
||||||
|
<p className="admin-page-subtitle">pondělí 28. dubna 2025</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-layout">
|
||||||
|
<div className="attendance-main">
|
||||||
|
<div className="attendance-clock-card">
|
||||||
|
<div className="attendance-clock-header">
|
||||||
|
<div className="attendance-clock-status">
|
||||||
|
<span className="attendance-status-dot" />
|
||||||
|
<span>Nepracuji</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-clock-time">08:30</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-clock-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary w-full">
|
||||||
|
Příchod
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary w-full">
|
||||||
|
Žádost o nepřítomnost
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-sidebar">
|
||||||
|
<div className="attendance-balance-card">
|
||||||
|
<h3 className="attendance-balance-title">Dovolená 2025</h3>
|
||||||
|
<div className="attendance-balance-value">
|
||||||
|
<span className="attendance-balance-number">12</span>
|
||||||
|
<span className="attendance-balance-unit">dnů</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-detail">
|
||||||
|
<span>Celkem: 160h</span>
|
||||||
|
<span>Čerpáno: 64h</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-bar">
|
||||||
|
<div
|
||||||
|
className="attendance-balance-progress"
|
||||||
|
style={{ width: "60%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card">
|
||||||
|
<div className="admin-stat-icon danger">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-label">Nemoc 2025</span>
|
||||||
|
<span className="admin-stat-value">16h čerpáno</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-quick-links">
|
||||||
|
<h4 className="attendance-quick-title">Rychlé odkazy</h4>
|
||||||
|
<a className="attendance-quick-link">
|
||||||
|
<span>Moje žádosti</span>
|
||||||
|
</a>
|
||||||
|
<a className="attendance-quick-link">
|
||||||
|
<span>Historie docházky</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/admin/fixtures/AttendanceHistoryFixture.tsx
Normal file
102
src/admin/fixtures/AttendanceHistoryFixture.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
export default function AttendanceHistoryFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Historie docházky</h1>
|
||||||
|
<p className="admin-page-subtitle">duben 2025</p>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-secondary">Tisk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Měsíc</label>
|
||||||
|
<input className="admin-form-input" readOnly value="04/2025" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<div className="admin-stat-icon info">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 200 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "baseline",
|
||||||
|
marginBottom: "0.375rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 600 }}>Fond: 120h / 160h</span>
|
||||||
|
<span
|
||||||
|
className="text-secondary"
|
||||||
|
style={{ fontSize: "0.8125rem" }}
|
||||||
|
>
|
||||||
|
20 prac. dnů
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-bar">
|
||||||
|
<div
|
||||||
|
className="attendance-balance-progress"
|
||||||
|
style={{ width: "75%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Příchod</th>
|
||||||
|
<th>Pauza</th>
|
||||||
|
<th>Odchod</th>
|
||||||
|
<th>Hodiny</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 4. 2025</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-info">
|
||||||
|
Práce
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">08:00</td>
|
||||||
|
<td className="admin-mono">12:00 – 12:30</td>
|
||||||
|
<td className="admin-mono">16:30</td>
|
||||||
|
<td className="admin-mono">8:00</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/admin/fixtures/AttendanceLocationFixture.tsx
Normal file
49
src/admin/fixtures/AttendanceLocationFixture.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export default function AttendanceLocationFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
<h1 className="admin-page-title">Lokace</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ height: 300, marginBottom: "1rem" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
||||||
|
>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ marginBottom: "0.5rem" }}>Poloha</h3>
|
||||||
|
<p>50.0755° N, 14.4378° E</p>
|
||||||
|
<p>Praha, Česká republika</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ marginBottom: "0.5rem" }}>Čas záznamu</h3>
|
||||||
|
<p>1. 1. 2024 08:00</p>
|
||||||
|
<p>Přesnost: 10 m</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/admin/fixtures/AuditLogFixture.tsx
Normal file
52
src/admin/fixtures/AuditLogFixture.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
export default function AuditLogFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Audit log</h1>
|
||||||
|
<p className="admin-page-subtitle">Záznam změn v systému</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-search-bar mb-4"
|
||||||
|
style={{ display: "flex", gap: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Čas</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
<th>Entita</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024 10:00</td>
|
||||||
|
<td>admin</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-create">
|
||||||
|
Vytvoření
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>Faktura</td>
|
||||||
|
<td style={{ maxWidth: 300 }}>Nová faktura FV-2024-001</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
src/admin/fixtures/CompanySettingsFixture.tsx
Normal file
69
src/admin/fixtures/CompanySettingsFixture.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default function CompanySettingsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nastavení firmy</h1>
|
||||||
|
<p className="admin-page-subtitle">Firemní údaje a bankovní účty</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
Uložit nastavení
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-settings-grid">
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h3 className="admin-card-title">Firemní údaje</h3>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form">
|
||||||
|
<label className="admin-form-label">Název firmy</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
readOnly
|
||||||
|
value="BOHA s.r.o."
|
||||||
|
/>
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Ulice</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
readOnly
|
||||||
|
value="Hlavní 123"
|
||||||
|
/>
|
||||||
|
<label className="admin-form-label">Město</label>
|
||||||
|
<input className="admin-form-input" readOnly value="Praha" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h3 className="admin-card-title">Bankovní účty</h3>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Banka</th>
|
||||||
|
<th>Číslo účtu</th>
|
||||||
|
<th>Měna</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Hlavní účet</td>
|
||||||
|
<td>ČSOB</td>
|
||||||
|
<td className="admin-mono">123456/0300</td>
|
||||||
|
<td>CZK</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/admin/fixtures/DashSessionsFixture.tsx
Normal file
63
src/admin/fixtures/DashSessionsFixture.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export default function DashSessionsFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div
|
||||||
|
className="admin-card-header"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "0.75rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="admin-card-title">Přihlášená zařízení</h2>
|
||||||
|
<button className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||||
|
Odhlásit ostatní
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
|
<div className="dash-sessions-list">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`dash-session-item${i === 0 ? " dash-session-item-current" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="dash-session-icon">
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||||
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="dash-session-info">
|
||||||
|
<div className="dash-session-device">
|
||||||
|
Chrome na Windows
|
||||||
|
{i === 0 && (
|
||||||
|
<span
|
||||||
|
className="admin-badge admin-badge-success"
|
||||||
|
style={{ marginLeft: "0.5rem" }}
|
||||||
|
>
|
||||||
|
Aktuální
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dash-session-meta">
|
||||||
|
<span>192.168.1.100</span>
|
||||||
|
<span className="dash-session-meta-separator">|</span>
|
||||||
|
<span>před 2 hodinami</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
src/admin/fixtures/DashboardFixture.tsx
Normal file
94
src/admin/fixtures/DashboardFixture.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
export default function DashboardFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Dashboard</h1>
|
||||||
|
<p className="admin-page-subtitle">Přehled</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["Nabídky", "Objednávky", "Faktury", "Projekty"].map((label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>12</div>
|
||||||
|
<div style={{ fontSize: "0.75rem" }}>tento měsíc</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-quick-actions"
|
||||||
|
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
||||||
|
>
|
||||||
|
{["Nová nabídka", "Nová faktura", "Zapsat docházku"].map((label, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "2fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-card" style={{ minHeight: 320 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Docházka dnes</h3>
|
||||||
|
<div style={{ height: 200 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
<div className="admin-card" style={{ minHeight: 150 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Aktivita</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ minHeight: 150 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Profil</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
||||||
|
>
|
||||||
|
<div className="admin-card" style={{ minHeight: 200 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Relace</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ minHeight: 200 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Poslední aktivity</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/admin/fixtures/InvoiceDetailFixture.tsx
Normal file
86
src/admin/fixtures/InvoiceDetailFixture.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export default function InvoiceDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
<a href="/invoices" className="admin-btn-icon">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">FV-2024-001</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Zaplaceno</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div>Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
Vystavena
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum vystavení</label>
|
||||||
|
<div>1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum splatnosti</label>
|
||||||
|
<div>15. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Položky</h3>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Služba {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
src/admin/fixtures/InvoicesFixture.tsx
Normal file
83
src/admin/fixtures/InvoicesFixture.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
export default function InvoicesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Faktury</h1>
|
||||||
|
<p className="admin-page-subtitle">15 faktur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["Vystaveno", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
||||||
|
(label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
||||||
|
{i * 5 + 3}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Částka</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">FV-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
Vystaveno
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right">50 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/admin/fixtures/LeaveApprovalFixture.tsx
Normal file
51
src/admin/fixtures/LeaveApprovalFixture.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default function LeaveApprovalFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Schvalování dovolené</h1>
|
||||||
|
<p className="admin-page-subtitle">2 čekající</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Od</th>
|
||||||
|
<th>Do</th>
|
||||||
|
<th>Dní</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Dovolená</td>
|
||||||
|
<td className="admin-mono">1. 7. 2024</td>
|
||||||
|
<td className="admin-mono">5. 7. 2024</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn admin-btn-sm admin-btn-primary">
|
||||||
|
Schválit
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-sm admin-btn-secondary">
|
||||||
|
Zamítnout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/admin/fixtures/LeaveRequestsFixture.tsx
Normal file
53
src/admin/fixtures/LeaveRequestsFixture.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default function LeaveRequestsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Žádosti o dovolenou</h1>
|
||||||
|
<p className="admin-page-subtitle">3 žádosti</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">+ Nová žádost</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Od</th>
|
||||||
|
<th>Do</th>
|
||||||
|
<th>Dní</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Dovolená</td>
|
||||||
|
<td className="admin-mono">1. 7. 2024</td>
|
||||||
|
<td className="admin-mono">5. 7. 2024</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-pending">
|
||||||
|
Čeká
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">Zrušit</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/admin/fixtures/OfferDetailFixture.tsx
Normal file
60
src/admin/fixtures/OfferDetailFixture.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export default function OfferDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<button className="admin-btn-icon">←</button>
|
||||||
|
<h1 className="admin-page-title">NAB-2024-001</h1>
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Číslo nabídky</label>
|
||||||
|
<div className="admin-form-input">NAB-2024-001</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div className="admin-form-input">Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum</label>
|
||||||
|
<div className="admin-form-input">1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Platnost do</label>
|
||||||
|
<div className="admin-form-input">31. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table className="admin-table" style={{ marginTop: "1rem" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Položka {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/admin/fixtures/OffersCustomersFixture.tsx
Normal file
49
src/admin/fixtures/OffersCustomersFixture.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export default function OffersCustomersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Zákazníci</h1>
|
||||||
|
<p className="admin-page-subtitle">8 zákazníků</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat zákazníka
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Město</th>
|
||||||
|
<th>IČO</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>Praha</td>
|
||||||
|
<td className="admin-mono">12345678</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/admin/fixtures/OffersFixture.tsx
Normal file
54
src/admin/fixtures/OffersFixture.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export default function OffersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nabídky</h1>
|
||||||
|
<p className="admin-page-subtitle">12 nabídek</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Celkem</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">NAB-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-offer-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right fw-500">100 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/admin/fixtures/OffersTemplatesFixture.tsx
Normal file
36
src/admin/fixtures/OffersTemplatesFixture.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export default function OffersTemplatesFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Šablona {i + 1}</td>
|
||||||
|
<td className="admin-mono text-right">1 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/admin/fixtures/OrderDetailFixture.tsx
Normal file
89
src/admin/fixtures/OrderDetailFixture.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
export default function OrderDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div className="admin-page-header-left">
|
||||||
|
<Link
|
||||||
|
to="/orders"
|
||||||
|
className="admin-btn-icon"
|
||||||
|
style={{ marginRight: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">OBJ-2024-001</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Stornovat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div>Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<span className="admin-badge admin-badge-order-realizace">
|
||||||
|
V realizaci
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum vytvoření</label>
|
||||||
|
<div>1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Celkem</label>
|
||||||
|
<div className="fw-500">50 000 Kč</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Položky</h3>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Položka {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/admin/fixtures/OrdersFixture.tsx
Normal file
55
src/admin/fixtures/OrdersFixture.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export default function OrdersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Objednávky</h1>
|
||||||
|
<p className="admin-page-subtitle">8 objednávek</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Nabídka</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Celkem</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">OBJ-2024-00{i + 1}</td>
|
||||||
|
<td>NAB-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-order-realizace">
|
||||||
|
V realizaci
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right fw-500">50 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/admin/fixtures/ProjectDetailFixture.tsx
Normal file
70
src/admin/fixtures/ProjectDetailFixture.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export default function ProjectDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
<a href="/projects" className="admin-btn-icon">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">PRJ-001 Projekt Alpha</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Název projektu</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
value="Projekt Alpha"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<select className="admin-form-select">
|
||||||
|
<option>Aktivní</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Začátek</label>
|
||||||
|
<input type="date" className="admin-form-input" readOnly />
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Konec</label>
|
||||||
|
<input type="date" className="admin-form-input" readOnly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Poznámky</h3>
|
||||||
|
<textarea className="admin-form-input" rows={4} readOnly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/admin/fixtures/ProjectFileManagerFixture.tsx
Normal file
53
src/admin/fixtures/ProjectFileManagerFixture.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default function ProjectFileManagerFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Soubory</h3>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
marginBottom: "0.75rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Projekt</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span>Dokumentace</span>
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
padding: "0.5rem 0",
|
||||||
|
borderBottom: i < 3 ? "1px solid var(--border-color)" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
</svg>
|
||||||
|
<span style={{ flex: 1 }}>dokument_{i + 1}.pdf</span>
|
||||||
|
<span
|
||||||
|
style={{ color: "var(--text-secondary)", fontSize: "0.8rem" }}
|
||||||
|
>
|
||||||
|
2.{i + 1} MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/admin/fixtures/ProjectsFixture.tsx
Normal file
51
src/admin/fixtures/ProjectsFixture.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default function ProjectsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Projekty</h1>
|
||||||
|
<p className="admin-page-subtitle">6 projektů</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">PRJ-2024-00{i + 1}</td>
|
||||||
|
<td>Projekt Alpha</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-project-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/admin/fixtures/ReceivedInvoicesFixture.tsx
Normal file
80
src/admin/fixtures/ReceivedInvoicesFixture.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
export default function ReceivedInvoicesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Přijaté faktury</h1>
|
||||||
|
<p className="admin-page-subtitle">8 faktur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["K úhradě", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
||||||
|
(label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
||||||
|
{i * 3 + 2}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Dodavatel</th>
|
||||||
|
<th>Částka</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">PF-2024-00{i + 1}</td>
|
||||||
|
<td>Dodavatel s.r.o.</td>
|
||||||
|
<td className="admin-mono text-right">10 000 Kč</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
K úhradě
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/admin/fixtures/SettingsFixture.tsx
Normal file
70
src/admin/fixtures/SettingsFixture.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export default function SettingsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nastavení</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-tab-bar">
|
||||||
|
<button className="admin-tab active">Role</button>
|
||||||
|
<button className="admin-tab">Systém</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název role</th>
|
||||||
|
<th>Popis</th>
|
||||||
|
<th>Oprávnění</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="fw-500">admin</td>
|
||||||
|
<td>Správa celého systému</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-info">
|
||||||
|
všechna
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon" title="Upravit">
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h2 className="admin-card-title">Docházka</h2>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">
|
||||||
|
Limit pro přestávku (hodiny)
|
||||||
|
</label>
|
||||||
|
<input className="admin-form-input" readOnly value="6" />
|
||||||
|
<label className="admin-form-label">
|
||||||
|
Délka krátké přestávky (min)
|
||||||
|
</label>
|
||||||
|
<input className="admin-form-input" readOnly value="15" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/admin/fixtures/TripsAdminFixture.tsx
Normal file
44
src/admin/fixtures/TripsAdminFixture.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export default function TripsAdminFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">10 jízd</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Km</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>Škoda Octavia</td>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td className="admin-mono">200</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/admin/fixtures/TripsFixture.tsx
Normal file
132
src/admin/fixtures/TripsFixture.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
export default function TripsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Kniha jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">duben 2025</p>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Přidat jízdu</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-grid admin-grid-4">
|
||||||
|
<div className="admin-stat-card info">
|
||||||
|
<div className="admin-stat-icon info">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="20" x2="12" y2="10" />
|
||||||
|
<line x1="18" y1="20" x2="18" y2="4" />
|
||||||
|
<line x1="6" y1="20" x2="6" y2="16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">12</span>
|
||||||
|
<span className="admin-stat-label">Počet jízd</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card">
|
||||||
|
<div className="admin-stat-icon">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">1 240 km</span>
|
||||||
|
<span className="admin-stat-label">Celkem naježděno</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card success">
|
||||||
|
<div className="admin-stat-icon success">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
|
||||||
|
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
|
||||||
|
<circle cx="5.5" cy="18" r="2" />
|
||||||
|
<circle cx="18.5" cy="18" r="2" />
|
||||||
|
<path d="M8 18h8" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">980 km</span>
|
||||||
|
<span className="admin-stat-label">Služební</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card warning">
|
||||||
|
<div className="admin-stat-icon warning">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">260 km</span>
|
||||||
|
<span className="admin-stat-label">Soukromé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mt-6">
|
||||||
|
<div className="admin-card-header flex-between">
|
||||||
|
<h2 className="admin-card-title">Poslední jízdy</h2>
|
||||||
|
<a className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||||
|
Zobrazit historii
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Trasa</th>
|
||||||
|
<th>Vzdálenost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 4. 2025</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge">1A2 3456</span>
|
||||||
|
</td>
|
||||||
|
<td>Praha → Brno</td>
|
||||||
|
<td className="admin-mono">
|
||||||
|
<strong>200 km</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/admin/fixtures/TripsHistoryFixture.tsx
Normal file
40
src/admin/fixtures/TripsHistoryFixture.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export default function TripsHistoryFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Historie jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">10 jízd</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Trasa</th>
|
||||||
|
<th>Km</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>Škoda Octavia</td>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Praha → Brno</td>
|
||||||
|
<td className="admin-mono">200</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/admin/fixtures/UsersFixture.tsx
Normal file
66
src/admin/fixtures/UsersFixture.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export default function UsersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Uživatelé</h1>
|
||||||
|
<p className="admin-page-subtitle">5 uživatelů</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat uživatele
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-user">
|
||||||
|
<div className="admin-table-avatar">A</div>
|
||||||
|
<div>
|
||||||
|
<div className="admin-table-name">Jan Novák</div>
|
||||||
|
<div className="admin-table-username">@jan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>jan@email.cz</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-admin">
|
||||||
|
Administrátor
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/admin/fixtures/VehiclesFixture.tsx
Normal file
48
src/admin/fixtures/VehiclesFixture.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export default function VehiclesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<h1 className="admin-page-title">Vozidla</h1>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat vozidlo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Značka</th>
|
||||||
|
<th>Model</th>
|
||||||
|
<th>SPZ</th>
|
||||||
|
<th>Rok</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Škoda</td>
|
||||||
|
<td>Octavia</td>
|
||||||
|
<td className="admin-mono">1A2 3456</td>
|
||||||
|
<td>2024</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/admin/hooks/usePaginatedQuery.ts
Normal file
44
src/admin/hooks/usePaginatedQuery.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
useQuery,
|
||||||
|
keepPreviousData,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
interface PaginatedResult<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around useQuery for paginated list endpoints.
|
||||||
|
* Accepts the return value of queryOptions() from lib/queries/*
|
||||||
|
* and extracts items + pagination from the response.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function usePaginatedQuery<T>(options: any) {
|
||||||
|
const query = useQuery({
|
||||||
|
...options,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
} as UseQueryOptions<PaginatedResult<T>>);
|
||||||
|
|
||||||
|
const data = query.data as PaginatedResult<T> | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (data?.data ?? []) as T[],
|
||||||
|
pagination: data?.pagination ?? null,
|
||||||
|
isPending: query.isPending,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
isPlaceholderData: query.isPlaceholderData,
|
||||||
|
isError: query.isError,
|
||||||
|
error: query.error,
|
||||||
|
refetch: query.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useQueryClient, useQuery, keepPreviousData };
|
||||||
83
src/admin/lib/apiAdapter.ts
Normal file
83
src/admin/lib/apiAdapter.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import apiFetch from "../utils/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin adapter that converts apiFetch responses into the shape TanStack Query expects.
|
||||||
|
* - Checks response.ok and result.success
|
||||||
|
* - Throws on errors so TanStack Query can handle retry/error states
|
||||||
|
* - Returns result.data directly (unwrapped from the API envelope)
|
||||||
|
*/
|
||||||
|
export async function jsonQuery<T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await apiFetch(url, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: { success: boolean; data?: unknown; error?: string };
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as typeof result;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.error || `Request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationMeta {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResult<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: PaginationMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function paginatedJsonQuery<T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
const response = await apiFetch(url, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: {
|
||||||
|
success: boolean;
|
||||||
|
data?: unknown;
|
||||||
|
error?: string;
|
||||||
|
pagination?: PaginationMeta;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as typeof result;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.error || `Request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(result.data)
|
||||||
|
? result.data
|
||||||
|
: ((result.data as { items?: T[] })?.items ?? []);
|
||||||
|
const pagination = result.pagination ??
|
||||||
|
(result.data as { pagination?: PaginationMeta })?.pagination ?? {
|
||||||
|
total: items.length,
|
||||||
|
page: 1,
|
||||||
|
per_page: items.length,
|
||||||
|
total_pages: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data: items as T[], pagination };
|
||||||
|
}
|
||||||
99
src/admin/lib/queries/attendance.ts
Normal file
99
src/admin/lib/queries/attendance.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
interface LocationRaw {
|
||||||
|
users?: { first_name: string; last_name: string };
|
||||||
|
user_name?: string;
|
||||||
|
shift_date: string;
|
||||||
|
arrival_time?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
arrival_lat?: string | number | null;
|
||||||
|
arrival_lng?: string | number | null;
|
||||||
|
arrival_accuracy?: number | null;
|
||||||
|
arrival_address?: string | null;
|
||||||
|
departure_lat?: string | number | null;
|
||||||
|
departure_lng?: string | number | null;
|
||||||
|
departure_accuracy?: number | null;
|
||||||
|
departure_address?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationRecord {
|
||||||
|
user_name: string;
|
||||||
|
shift_date: string;
|
||||||
|
arrival_time?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
arrival_lat?: string | number | null;
|
||||||
|
arrival_lng?: string | number | null;
|
||||||
|
arrival_accuracy?: number | null;
|
||||||
|
arrival_address?: string | null;
|
||||||
|
departure_lat?: string | number | null;
|
||||||
|
departure_lng?: string | number | null;
|
||||||
|
departure_accuracy?: number | null;
|
||||||
|
departure_address?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const attendanceHistoryOptions = (filters: {
|
||||||
|
month?: string;
|
||||||
|
userId?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "history", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("limit", "1000");
|
||||||
|
if (filters.month) params.set("month", filters.month);
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/attendance?${params.toString()}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceBalancesOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "balances", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=balances&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceWorkFundOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "workfund", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=workfund&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceProjectReportOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "project-report", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=project_report&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceLocationOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "location", id],
|
||||||
|
queryFn: async (): Promise<LocationRecord> => {
|
||||||
|
const response = await apiFetch(
|
||||||
|
`/api/admin/attendance?action=location&id=${id}`,
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Záznam nebyl nalezen");
|
||||||
|
}
|
||||||
|
const raw = (result.data.record || result.data) as LocationRaw;
|
||||||
|
const userName = raw.users
|
||||||
|
? `${raw.users.first_name} ${raw.users.last_name}`.trim()
|
||||||
|
: raw.user_name || "";
|
||||||
|
const { users: _users, ...rest } = raw;
|
||||||
|
return { ...rest, user_name: userName };
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
28
src/admin/lib/queries/auditLog.ts
Normal file
28
src/admin/lib/queries/auditLog.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const auditLogOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
action?: string;
|
||||||
|
entityType?: string;
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
page?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["audit-log", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.action) params.set("action", filters.action);
|
||||||
|
if (filters.entityType) params.set("entity_type", filters.entityType);
|
||||||
|
if (filters.dateFrom) params.set("date_from", filters.dateFrom);
|
||||||
|
if (filters.dateTo) params.set("date_to", filters.dateTo);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<{
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
pagination: Record<string, unknown>;
|
||||||
|
}>(`/api/admin/audit-log${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
18
src/admin/lib/queries/common.ts
Normal file
18
src/admin/lib/queries/common.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const bankAccountsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["bank-accounts"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>("/api/admin/bank-accounts"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const supplierListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["suppliers"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<string[]>("/api/admin/received-invoices/suppliers"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
42
src/admin/lib/queries/dashboard.ts
Normal file
42
src/admin/lib/queries/dashboard.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const dashboardOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["dashboard"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>>("/api/admin/dashboard"),
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const require2FAOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "2fa"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ require_2fa: boolean }>("/api/admin/totp/required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: number | string;
|
||||||
|
is_current: boolean;
|
||||||
|
device_info?: {
|
||||||
|
icon?: string;
|
||||||
|
browser?: string;
|
||||||
|
os?: string;
|
||||||
|
};
|
||||||
|
ip_address: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["sessions"],
|
||||||
|
queryFn: async (): Promise<Session[]> => {
|
||||||
|
const response = await apiFetch("/api/admin/sessions");
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
return Array.isArray(data.data) ? data.data : data.data?.sessions || [];
|
||||||
|
}
|
||||||
|
throw new Error(data.error || "Nepodařilo se načíst relace");
|
||||||
|
},
|
||||||
|
});
|
||||||
126
src/admin/lib/queries/invoices.ts
Normal file
126
src/admin/lib/queries/invoices.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export interface CurrencyAmount {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Invoice {
|
||||||
|
id: number;
|
||||||
|
invoice_number: string;
|
||||||
|
customer_name: string | null;
|
||||||
|
status: string;
|
||||||
|
issue_date: string;
|
||||||
|
due_date: string;
|
||||||
|
total: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoiceStats {
|
||||||
|
paid_month: CurrencyAmount[];
|
||||||
|
paid_month_czk: number;
|
||||||
|
paid_month_count: number;
|
||||||
|
awaiting: CurrencyAmount[];
|
||||||
|
awaiting_czk: number;
|
||||||
|
awaiting_count: number;
|
||||||
|
overdue: CurrencyAmount[];
|
||||||
|
overdue_czk: number;
|
||||||
|
overdue_count: number;
|
||||||
|
vat_month: CurrencyAmount[];
|
||||||
|
vat_month_czk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invoiceListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
status?: string;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.status) params.set("status", filters.status);
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery<Invoice>(
|
||||||
|
`/api/admin/invoices${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const receivedInvoiceListOptions = (filters: {
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"invoices",
|
||||||
|
"received",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
year: filters.year,
|
||||||
|
search: filters.search,
|
||||||
|
sort: filters.sort,
|
||||||
|
order: filters.order,
|
||||||
|
page: filters.page,
|
||||||
|
perPage: filters.perPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(
|
||||||
|
`/api/admin/received-invoices${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const invoiceStatsOptions = (month: number, year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "stats", month, year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<InvoiceStats>(
|
||||||
|
`/api/admin/invoices/stats?month=${month}&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const receivedInvoiceStatsOptions = (month: number, year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "received", "stats", month, year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/received-invoices/stats?month=${month}&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const invoiceDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/invoices/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
29
src/admin/lib/queries/leave.ts
Normal file
29
src/admin/lib/queries/leave.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const leaveRequestsOptions = (mine = true) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave-requests", mine ? "mine" : "all"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/leave-requests${mine ? "?mine=1" : ""}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const leavePendingOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave", "pending"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>(
|
||||||
|
"/api/admin/leave-requests?status=pending",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const leaveProcessedOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave", "processed"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>(
|
||||||
|
"/api/admin/leave-requests?status=approved,rejected",
|
||||||
|
),
|
||||||
|
});
|
||||||
58
src/admin/lib/queries/offers.ts
Normal file
58
src/admin/lib/queries/offers.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const offerCustomersOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offer-customers"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>>("/api/admin/customers"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerTemplatesOptions = (action?: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offer-templates", action ?? "all"],
|
||||||
|
queryFn: () => {
|
||||||
|
const url = action
|
||||||
|
? `/api/admin/offers-templates?action=${action}`
|
||||||
|
: "/api/admin/offers-templates";
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(url);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/offers/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerNextNumberOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", "next-number"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ next_number?: string; number?: string }>(
|
||||||
|
"/api/admin/offers/next-number",
|
||||||
|
),
|
||||||
|
});
|
||||||
31
src/admin/lib/queries/orders.ts
Normal file
31
src/admin/lib/queries/orders.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const orderListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["orders", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/orders${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const orderDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["orders", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/orders/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
78
src/admin/lib/queries/projects.ts
Normal file
78
src/admin/lib/queries/projects.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
|
||||||
|
export const projectListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/projects${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/projects/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ProjectFilesData {
|
||||||
|
items: Array<{
|
||||||
|
name: string;
|
||||||
|
type: "file" | "folder";
|
||||||
|
size?: number;
|
||||||
|
size_formatted?: string;
|
||||||
|
modified?: string;
|
||||||
|
extension?: string;
|
||||||
|
item_count?: number;
|
||||||
|
is_symlink?: boolean;
|
||||||
|
link_target?: string;
|
||||||
|
}>;
|
||||||
|
breadcrumb: string[];
|
||||||
|
path: string;
|
||||||
|
full_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectFilesOptions = (projectId: number, path: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", String(projectId), "files", path],
|
||||||
|
queryFn: async (): Promise<ProjectFilesData> => {
|
||||||
|
const params = new URLSearchParams({ project_id: String(projectId) });
|
||||||
|
if (path) params.set("path", path);
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await apiFetch(`/api/admin/project-files?${params}`);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Chyba připojení");
|
||||||
|
}
|
||||||
|
if (res.status === 401) throw new Error("Unauthorized");
|
||||||
|
if (res.status === 404) {
|
||||||
|
return { items: [], breadcrumb: [""], path: "", full_path: "" };
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || !data.success) {
|
||||||
|
throw new Error(data.error || `Request failed (${res.status})`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: data.data.items || [],
|
||||||
|
breadcrumb: data.data.breadcrumb || [""],
|
||||||
|
path: data.data.path || "",
|
||||||
|
full_path: data.data.full_path || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
29
src/admin/lib/queries/settings.ts
Normal file
29
src/admin/lib/queries/settings.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const companySettingsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["company-settings"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>("/api/admin/company-settings"),
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const systemInfoOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "system-info"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
"/api/admin/company-settings/system-info",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @deprecated Use systemInfoOptions instead — this query fetches system-info, not system settings. */
|
||||||
|
export const systemSettingsOptions = systemInfoOptions;
|
||||||
|
|
||||||
|
export const require2FAOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "2fa"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ require_2fa: boolean }>("/api/admin/totp/required"),
|
||||||
|
});
|
||||||
82
src/admin/lib/queries/trips.ts
Normal file
82
src/admin/lib/queries/trips.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const tripListOptions = (filters: {
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
vehicleId?: number;
|
||||||
|
userId?: number;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"trips",
|
||||||
|
"list",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
year: filters.year,
|
||||||
|
vehicleId: filters.vehicleId,
|
||||||
|
userId: filters.userId,
|
||||||
|
page: filters.page,
|
||||||
|
perPage: filters.perPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.vehicleId)
|
||||||
|
params.set("vehicle_id", String(filters.vehicleId));
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/trips${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripVehiclesOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["trips", "vehicles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/vehicles"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripUsersOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["trips", "users"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>("/api/admin/trips/users"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripHistoryOptions = (filters: {
|
||||||
|
month?: string;
|
||||||
|
vehicleId?: number;
|
||||||
|
userId?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"trips",
|
||||||
|
"history",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
vehicleId: filters.vehicleId,
|
||||||
|
userId: filters.userId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", filters.month);
|
||||||
|
if (filters.vehicleId)
|
||||||
|
params.set("vehicle_id", String(filters.vehicleId));
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/trips${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
18
src/admin/lib/queries/users.ts
Normal file
18
src/admin/lib/queries/users.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const userListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: () => {
|
||||||
|
// The users endpoint returns { success, data: { items, ... } } or { success, data: [...] }
|
||||||
|
return jsonQuery<Record<string, unknown>>("/api/admin/users");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["roles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/roles"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
8
src/admin/lib/queries/vehicles.ts
Normal file
8
src/admin/lib/queries/vehicles.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const vehicleListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["vehicles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/vehicles"),
|
||||||
|
});
|
||||||
13
src/admin/lib/queryClient.ts
Normal file
13
src/admin/lib/queryClient.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
gcTime: 5 * 60_000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
retry: 1,
|
||||||
|
refetchOnReconnect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -491,10 +491,69 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.offers-items-table .admin-table td .admin-form-input {
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Give the description column more room by shrinking numeric columns */
|
||||||
|
.offers-col-qty {
|
||||||
|
width: 4rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-unit {
|
||||||
|
width: 4rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-price {
|
||||||
|
width: 5.5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-included {
|
||||||
|
width: 3.5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-total {
|
||||||
|
width: 5.5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-del {
|
||||||
|
width: 2.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.offers-items-table {
|
.offers-items-table {
|
||||||
margin: 0 -1rem;
|
margin: 0 -1rem;
|
||||||
width: calc(100% + 2rem);
|
width: calc(100% + 2rem);
|
||||||
|
border-radius: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offers-items-table .admin-table {
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offers-items-table .admin-table td {
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offers-items-table .admin-table th {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Further reduce numeric columns on very small screens */
|
||||||
|
.offers-col-qty {
|
||||||
|
width: 3.5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-unit {
|
||||||
|
width: 3.5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-price {
|
||||||
|
width: 5rem !important;
|
||||||
|
}
|
||||||
|
.offers-col-total {
|
||||||
|
width: 5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -14,6 +15,9 @@ import {
|
|||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { jsonQuery } from "../lib/apiAdapter";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceFixture from "../fixtures/AttendanceFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -92,22 +96,20 @@ function getFundBarBackground(fund: MonthlyFund) {
|
|||||||
export default function Attendance() {
|
export default function Attendance() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [data, setData] = useState<AttendanceData>({
|
const statusQuery = useQuery({
|
||||||
ongoing_shift: null,
|
queryKey: ["attendance", "status"],
|
||||||
today_shifts: [],
|
queryFn: () => jsonQuery<AttendanceData>("/api/admin/attendance/status"),
|
||||||
date: "",
|
|
||||||
leave_balance: {
|
|
||||||
vacation_total: 160,
|
|
||||||
vacation_used: 0,
|
|
||||||
vacation_remaining: 160,
|
|
||||||
sick_used: 0,
|
|
||||||
},
|
|
||||||
monthly_fund: null,
|
|
||||||
project_logs: [],
|
|
||||||
active_project_id: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectsQuery = useQuery({
|
||||||
|
queryKey: ["attendance", "projects"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Project[]>("/api/admin/attendance?action=projects"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
||||||
const [leaveForm, setLeaveForm] = useState({
|
const [leaveForm, setLeaveForm] = useState({
|
||||||
leave_type: "vacation",
|
leave_type: "vacation",
|
||||||
@@ -117,10 +119,7 @@ export default function Attendance() {
|
|||||||
});
|
});
|
||||||
const [requestSubmitting, setRequestSubmitting] = useState(false);
|
const [requestSubmitting, setRequestSubmitting] = useState(false);
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
|
||||||
const [switchingProject, setSwitchingProject] = useState(false);
|
const [switchingProject, setSwitchingProject] = useState(false);
|
||||||
const [projectLogs, setProjectLogs] = useState<ProjectLog[]>([]);
|
|
||||||
const [activeProjectId, setActiveProjectId] = useState<number | null>(null);
|
|
||||||
const [gpsConfirm, setGpsConfirm] = useState<{
|
const [gpsConfirm, setGpsConfirm] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
action: string | null;
|
action: string | null;
|
||||||
@@ -139,45 +138,12 @@ export default function Attendance() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// Sync notes from query data when the shift changes
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/attendance/status`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setData(result.data);
|
|
||||||
setNotes(result.data.ongoing_shift?.notes || "");
|
|
||||||
setProjectLogs(result.data.project_logs || []);
|
|
||||||
setActiveProjectId(result.data.active_project_id || null);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
if (statusQuery.data) {
|
||||||
}, [fetchData]);
|
setNotes(statusQuery.data.ongoing_shift?.notes || "");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadProjects = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=projects`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const items = Array.isArray(result.data) ? result.data : [];
|
|
||||||
setProjects(items);
|
|
||||||
}
|
}
|
||||||
} catch {
|
}, [statusQuery.data]);
|
||||||
// silent - projects are supplementary
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadProjects();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useModalLock(isLeaveModalOpen);
|
useModalLock(isLeaveModalOpen);
|
||||||
|
|
||||||
@@ -277,7 +243,9 @@ export default function Attendance() {
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
punchTimeoutRef.current = setTimeout(() => {
|
punchTimeoutRef.current = setTimeout(() => {
|
||||||
alert.success(result.data?.message || result.message || "Uloženo");
|
alert.success(result.data?.message || result.message || "Uloženo");
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -302,7 +270,9 @@ export default function Attendance() {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Přestávka zaznamenána",
|
result.data?.message || result.message || "Přestávka zaznamenána",
|
||||||
);
|
);
|
||||||
@@ -348,7 +318,9 @@ export default function Attendance() {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Projekt přepnut",
|
result.data?.message || result.message || "Projekt přepnut",
|
||||||
);
|
);
|
||||||
@@ -390,7 +362,9 @@ export default function Attendance() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsLeaveModalOpen(false);
|
setIsLeaveModalOpen(false);
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Žádost odeslána",
|
result.data?.message || result.message || "Žádost odeslána",
|
||||||
@@ -411,103 +385,15 @@ export default function Attendance() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (!statusQuery.data) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="attendance"
|
||||||
className="admin-skeleton-row"
|
loading={statusQuery.isPending}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AttendanceFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: "1.5rem" }}>
|
|
||||||
<div className="admin-card" style={{ flex: 2 }}>
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "120px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "180px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-row">
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.25rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.25rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +401,11 @@ export default function Attendance() {
|
|||||||
ongoing_shift: ongoingShift,
|
ongoing_shift: ongoingShift,
|
||||||
today_shifts: todayShifts,
|
today_shifts: todayShifts,
|
||||||
leave_balance: leaveBalance,
|
leave_balance: leaveBalance,
|
||||||
} = data;
|
} = statusQuery.data;
|
||||||
|
const data = statusQuery.data;
|
||||||
|
const projects = projectsQuery.data ?? [];
|
||||||
|
const projectLogs = data.project_logs ?? [];
|
||||||
|
const activeProjectId = data.active_project_id ?? null;
|
||||||
const isOngoingShift = ongoingShift && !ongoingShift.departure_time;
|
const isOngoingShift = ongoingShift && !ongoingShift.departure_time;
|
||||||
const completedToday = todayShifts.filter((s) => s.departure_time);
|
const completedToday = todayShifts.filter((s) => s.departure_time);
|
||||||
const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8);
|
const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import useModalLock from "../hooks/useModalLock";
|
|||||||
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { formatMinutes } from "../utils/attendanceHelpers";
|
import { formatMinutes } from "../utils/attendanceHelpers";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceAdminFixture from "../fixtures/AttendanceAdminFixture";
|
||||||
|
|
||||||
interface UserTotalData {
|
interface UserTotalData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -95,84 +97,13 @@ export default function AttendanceAdmin() {
|
|||||||
|
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="attendance-admin"
|
||||||
className="admin-skeleton-row"
|
loading={isInitialLoad}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AttendanceAdminFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-row" style={{ gap: "0.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton"
|
|
||||||
style={{ gap: "0.75rem", padding: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-skeleton-row">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ flex: 1, borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ flex: 1, borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{[0, 1, 2].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "0.75rem" }}>
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-full"
|
|
||||||
style={{ height: "4px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -6,8 +6,16 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
attendanceBalancesOptions,
|
||||||
|
attendanceWorkFundOptions,
|
||||||
|
attendanceProjectReportOptions,
|
||||||
|
} from "../lib/queries/attendance";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceBalancesFixture from "../fixtures/AttendanceBalancesFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface BalanceEntry {
|
interface BalanceEntry {
|
||||||
@@ -134,23 +142,20 @@ const getProgressBackground = (
|
|||||||
export default function AttendanceBalances() {
|
export default function AttendanceBalances() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [year, setYear] = useState(new Date().getFullYear());
|
const [year, setYear] = useState(new Date().getFullYear());
|
||||||
const [data, setData] = useState<BalancesData>({
|
const { data: balancesRaw, isPending: balancesPending } = useQuery(
|
||||||
users: [],
|
attendanceBalancesOptions(year),
|
||||||
balances: {},
|
);
|
||||||
});
|
const { data: fundRaw, isPending: fundPending } = useQuery(
|
||||||
|
attendanceWorkFundOptions(year),
|
||||||
const [fundLoading, setFundLoading] = useState(true);
|
);
|
||||||
const [fundData, setFundData] = useState<FundData>({
|
const { data: projectRaw, isPending: projectPending } = useQuery(
|
||||||
months: {},
|
attendanceProjectReportOptions(year),
|
||||||
holidays: [],
|
);
|
||||||
users: [],
|
const balancesData = balancesRaw as BalancesData | undefined;
|
||||||
balances: {},
|
const fundData = fundRaw as FundData | undefined;
|
||||||
});
|
const projectData = projectRaw as ProjectData | undefined;
|
||||||
|
|
||||||
const [projectLoading, setProjectLoading] = useState(true);
|
|
||||||
const [projectData, setProjectData] = useState<ProjectData>({ months: {} });
|
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<{
|
const [editingUser, setEditingUser] = useState<{
|
||||||
@@ -169,67 +174,6 @@ export default function AttendanceBalances() {
|
|||||||
userName: string;
|
userName: string;
|
||||||
}>({ show: false, userId: null, userName: "" });
|
}>({ show: false, userId: null, userName: "" });
|
||||||
|
|
||||||
const fetchData = useCallback(
|
|
||||||
async (showLoading = true) => {
|
|
||||||
if (showLoading) setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=balances&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
|
||||||
} finally {
|
|
||||||
if (showLoading) setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[year, alert],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchFundData = useCallback(async () => {
|
|
||||||
setFundLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=workfund&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setFundData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent - fund data is supplementary
|
|
||||||
} finally {
|
|
||||||
setFundLoading(false);
|
|
||||||
}
|
|
||||||
}, [year]);
|
|
||||||
|
|
||||||
const fetchProjectData = useCallback(async () => {
|
|
||||||
setProjectLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=project_report&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setProjectData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent - project data is supplementary
|
|
||||||
} finally {
|
|
||||||
setProjectLoading(false);
|
|
||||||
}
|
|
||||||
}, [year]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadAll = async () => {
|
|
||||||
await Promise.all([fetchData(), fetchFundData(), fetchProjectData()]);
|
|
||||||
};
|
|
||||||
loadAll();
|
|
||||||
}, [fetchData, fetchFundData, fetchProjectData]);
|
|
||||||
|
|
||||||
useModalLock(showEditModal);
|
useModalLock(showEditModal);
|
||||||
|
|
||||||
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
||||||
@@ -265,8 +209,7 @@ export default function AttendanceBalances() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
await fetchData(false);
|
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -297,7 +240,7 @@ export default function AttendanceBalances() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setResetConfirm({ show: false, userId: null, userName: "" });
|
setResetConfirm({ show: false, userId: null, userName: "" });
|
||||||
await fetchData(false);
|
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -315,7 +258,7 @@ export default function AttendanceBalances() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getYearFundTotals = (userId: string) => {
|
const getYearFundTotals = (userId: string) => {
|
||||||
if (!fundData.months || Object.keys(fundData.months).length === 0)
|
if (!fundData?.months || Object.keys(fundData.months).length === 0)
|
||||||
return null;
|
return null;
|
||||||
let totalFund = 0;
|
let totalFund = 0;
|
||||||
let totalWorked = 0;
|
let totalWorked = 0;
|
||||||
@@ -380,23 +323,20 @@ export default function AttendanceBalances() {
|
|||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances"
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
loading={balancesPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
{balancesData &&
|
||||||
</div>
|
Object.keys(balancesData.balances).length === 0 && (
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && Object.keys(data.balances).length === 0 && (
|
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
<p>Žádní uživatelé k zobrazení.</p>
|
<p>Žádní uživatelé k zobrazení.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && Object.keys(data.balances).length > 0 && (
|
{balancesData &&
|
||||||
|
Object.keys(balancesData.balances).length > 0 && (
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -413,12 +353,15 @@ export default function AttendanceBalances() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(data.balances).map(([userId, balance]) => {
|
{Object.entries(balancesData.balances).map(
|
||||||
|
([userId, balance]) => {
|
||||||
const yf = getYearFundTotals(userId);
|
const yf = getYearFundTotals(userId);
|
||||||
return (
|
return (
|
||||||
<tr key={userId}>
|
<tr key={userId}>
|
||||||
<td className="fw-500">{balance.name}</td>
|
<td className="fw-500">{balance.name}</td>
|
||||||
<td className="admin-mono">{balance.vacation_total}</td>
|
<td className="admin-mono">
|
||||||
|
{balance.vacation_total}
|
||||||
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{balance.vacation_used.toFixed(1)}
|
{balance.vacation_used.toFixed(1)}
|
||||||
</td>
|
</td>
|
||||||
@@ -446,7 +389,9 @@ export default function AttendanceBalances() {
|
|||||||
<td>
|
<td>
|
||||||
<div className="admin-table-actions">
|
<div className="admin-table-actions">
|
||||||
<button
|
<button
|
||||||
onClick={() => openEditModal(userId, balance)}
|
onClick={() =>
|
||||||
|
openEditModal(userId, balance)
|
||||||
|
}
|
||||||
className="admin-btn-icon"
|
className="admin-btn-icon"
|
||||||
title="Upravit"
|
title="Upravit"
|
||||||
aria-label="Upravit"
|
aria-label="Upravit"
|
||||||
@@ -495,17 +440,20 @@ export default function AttendanceBalances() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Monthly Fund Overview */}
|
{/* Monthly Fund Overview */}
|
||||||
{!fundLoading &&
|
{!fundPending &&
|
||||||
fundData.months &&
|
fundData?.months &&
|
||||||
Object.keys(fundData.months).length > 0 && (
|
Object.keys(fundData.months).length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
@@ -587,7 +535,7 @@ export default function AttendanceBalances() {
|
|||||||
gap: "0.375rem",
|
gap: "0.375rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fundData.users &&
|
{fundData?.users &&
|
||||||
fundData.users.map((user) => {
|
fundData.users.map((user) => {
|
||||||
const us = monthData.users?.[String(user.id)];
|
const us = monthData.users?.[String(user.id)];
|
||||||
if (!us) return null;
|
if (!us) return null;
|
||||||
@@ -668,23 +616,19 @@ export default function AttendanceBalances() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fundLoading && (
|
{fundPending && (
|
||||||
<div className="mt-6">
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances-fund"
|
||||||
{[0, 1, 2].map((i) => (
|
loading={fundPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<div className="mt-6" />
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
</Skeleton>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Monthly Project Overview */}
|
{/* Monthly Project Overview */}
|
||||||
{!projectLoading &&
|
{!projectPending &&
|
||||||
projectData.months &&
|
projectData?.months &&
|
||||||
Object.keys(projectData.months).length > 0 && (
|
Object.keys(projectData.months).length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
@@ -876,18 +820,14 @@ export default function AttendanceBalances() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projectLoading && (
|
{projectPending && (
|
||||||
<div className="mt-6">
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances-projects"
|
||||||
{[0, 1, 2].map((i) => (
|
loading={projectPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<div className="mt-6" />
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
</Skeleton>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { userListOptions } from "../lib/queries/users";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -8,6 +10,8 @@ import { motion } from "framer-motion";
|
|||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceCreateFixture from "../fixtures/AttendanceCreateFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -35,9 +39,9 @@ export default function AttendanceCreate() {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(true);
|
const { data: usersData, isPending: loading } = useQuery(userListOptions());
|
||||||
|
const users = (usersData as unknown as User[] | undefined) ?? [];
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateForm>(() => {
|
const [form, setForm] = useState<CreateForm>(() => {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
const today = new Date().toISOString().split("T")[0];
|
||||||
@@ -58,26 +62,6 @@ export default function AttendanceCreate() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/users`);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setUsers(
|
|
||||||
Array.isArray(result.data) ? result.data : result.data?.items || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst uživatele");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUsers();
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -125,37 +109,12 @@ export default function AttendanceCreate() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="attendance-create"
|
||||||
className="admin-skeleton-row"
|
loading={loading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AttendanceCreateFixture />}
|
||||||
>
|
>
|
||||||
<div className="admin-skeleton-line h-8" style={{ width: "200px" }} />
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ maxWidth: "600px" }}>
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ marginBottom: "0.5rem", height: "10px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line w-full h-10" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -367,5 +326,6 @@ export default function AttendanceCreate() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
import { attendanceHistoryOptions } from "../lib/queries/attendance";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDatetime,
|
formatDatetime,
|
||||||
@@ -16,10 +18,8 @@ import {
|
|||||||
formatTimeOrDatetimePrint,
|
formatTimeOrDatetimePrint,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import apiFetch from "../utils/api";
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceHistoryFixture from "../fixtures/AttendanceHistoryFixture";
|
||||||
const API_BASE = "/api/admin";
|
|
||||||
|
|
||||||
interface ProjectLog {
|
interface ProjectLog {
|
||||||
id?: number;
|
id?: number;
|
||||||
project_id?: number;
|
project_id?: number;
|
||||||
@@ -193,48 +193,21 @@ const renderProjectCell = (record: AttendanceRecord) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AttendanceHistory() {
|
export default function AttendanceHistory() {
|
||||||
const alert = useAlert();
|
|
||||||
const { user, hasPermission } = useAuth();
|
const { user, hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [companyName, setCompanyName] = useState("");
|
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||||
|
const companyName =
|
||||||
|
((companySettings as Record<string, unknown> | undefined)
|
||||||
|
?.company_name as string) || "";
|
||||||
const printRef = useRef<HTMLDivElement>(null);
|
const printRef = useRef<HTMLDivElement>(null);
|
||||||
const [month, setMonth] = useState(() => {
|
const [month, setMonth] = useState(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||||
});
|
});
|
||||||
const [records, setRecords] = useState<AttendanceRecord[]>([]);
|
const { data, isPending } = useQuery(
|
||||||
|
attendanceHistoryOptions({ month, userId: user?.id }),
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const [yearStr, monthStr] = month.split("-");
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000&user_id=${user?.id || ""}`,
|
|
||||||
);
|
);
|
||||||
if (response.status === 401) return;
|
const records = (data as AttendanceRecord[] | undefined) ?? [];
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setRecords(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [month, alert, user?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
apiFetch(`${API_BASE}/company-settings`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
|
||||||
if (d.success) setCompanyName(d.data.company_name || "");
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const computed = useMemo(() => {
|
const computed = useMemo(() => {
|
||||||
const [yearStr, monthStr] = month.split("-");
|
const [yearStr, monthStr] = month.split("-");
|
||||||
@@ -459,36 +432,13 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
transition={{ duration: 0.25, delay: 0.08 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "0.5rem" }}>
|
name="attendance-history-fund"
|
||||||
<div className="admin-skeleton-row" style={{ gap: "1rem" }}>
|
loading={isPending}
|
||||||
<div
|
fixture={<AttendanceHistoryFixture />}
|
||||||
className="admin-skeleton-line"
|
>
|
||||||
style={{
|
<>
|
||||||
width: "48px",
|
{computed.monthlyFund && (
|
||||||
height: "48px",
|
|
||||||
borderRadius: "12px",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/2"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-full"
|
|
||||||
style={{ height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px", marginTop: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && computed.monthlyFund && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -585,7 +535,7 @@ export default function AttendanceHistory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !computed.monthlyFund && (
|
{!computed.monthlyFund && (
|
||||||
<div
|
<div
|
||||||
className="text-muted"
|
className="text-muted"
|
||||||
style={{
|
style={{
|
||||||
@@ -597,6 +547,8 @@ export default function AttendanceHistory() {
|
|||||||
Fond měsíce není k dispozici
|
Fond měsíce není k dispozici
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -608,23 +560,18 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-history-table"
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
loading={isPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceHistoryFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
{records.length === 0 && (
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && records.length === 0 && (
|
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && records.length > 0 && (
|
{records.length > 0 && (
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -660,7 +607,9 @@ export default function AttendanceHistory() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "—" : formatDatetime(record.arrival_time)}
|
{isLeave
|
||||||
|
? "—"
|
||||||
|
: formatDatetime(record.arrival_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "—" : formatBreakRange(record)}
|
{isLeave ? "—" : formatBreakRange(record)}
|
||||||
@@ -693,6 +642,8 @@ export default function AttendanceHistory() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -9,65 +10,35 @@ import L from "leaflet";
|
|||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import {
|
||||||
const API_BASE = "/api/admin";
|
attendanceLocationOptions,
|
||||||
|
type LocationRecord,
|
||||||
interface LocationRecord {
|
} from "../lib/queries/attendance";
|
||||||
user_name: string;
|
import { Skeleton } from "boneyard-js/react";
|
||||||
shift_date: string;
|
import AttendanceLocationFixture from "../fixtures/AttendanceLocationFixture";
|
||||||
arrival_time?: string | null;
|
|
||||||
departure_time?: string | null;
|
|
||||||
arrival_lat?: string | number | null;
|
|
||||||
arrival_lng?: string | number | null;
|
|
||||||
arrival_accuracy?: number | null;
|
|
||||||
arrival_address?: string | null;
|
|
||||||
departure_lat?: string | number | null;
|
|
||||||
departure_lng?: string | number | null;
|
|
||||||
departure_accuracy?: number | null;
|
|
||||||
departure_address?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AttendanceLocation() {
|
export default function AttendanceLocation() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [record, setRecord] = useState<LocationRecord | null>(null);
|
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
const mapInstanceRef = useRef<unknown>(null);
|
const mapInstanceRef = useRef<unknown>(null);
|
||||||
|
|
||||||
|
const locationQuery = useQuery(attendanceLocationOptions(id));
|
||||||
|
const record = locationQuery.data ?? null;
|
||||||
|
const isPending = locationQuery.isPending;
|
||||||
|
|
||||||
|
// Navigate away on fetch error
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
if (locationQuery.error) {
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=location&id=${id}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const raw = result.data.record || result.data;
|
|
||||||
// Enrich with user_name from nested users relation
|
|
||||||
const userName = raw.users
|
|
||||||
? `${raw.users.first_name} ${raw.users.last_name}`.trim()
|
|
||||||
: raw.user_name || "";
|
|
||||||
setRecord({ ...raw, user_name: userName });
|
|
||||||
} else {
|
|
||||||
alert.error("Záznam nebyl nalezen");
|
|
||||||
navigate("/attendance/admin");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
alert.error("Nepodařilo se načíst data");
|
||||||
navigate("/attendance/admin");
|
navigate("/attendance/admin");
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, [locationQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [id, alert, navigate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!record || loading) return;
|
if (!record || isPending) return;
|
||||||
|
|
||||||
const hasArrivalLocation = record.arrival_lat && record.arrival_lng;
|
const hasArrivalLocation = record.arrival_lat && record.arrival_lng;
|
||||||
const hasDepartureLocation = record.departure_lat && record.departure_lng;
|
const hasDepartureLocation = record.departure_lat && record.departure_lng;
|
||||||
@@ -175,7 +146,7 @@ export default function AttendanceLocation() {
|
|||||||
mapInstanceRef.current = null;
|
mapInstanceRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [record, loading]);
|
}, [record, isPending]);
|
||||||
|
|
||||||
const formatDatetimeLocal = (datetime: string | null | undefined): string => {
|
const formatDatetimeLocal = (datetime: string | null | undefined): string => {
|
||||||
if (!datetime) return "—";
|
if (!datetime) return "—";
|
||||||
@@ -185,56 +156,6 @@ export default function AttendanceLocation() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "300px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1.25rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[0, 1].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "50%" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line w-full" />
|
|
||||||
<div className="admin-skeleton-line w-3/4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -248,6 +169,11 @@ export default function AttendanceLocation() {
|
|||||||
const month = shiftDateStr.substring(0, 7);
|
const month = shiftDateStr.substring(0, 7);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="attendance-location"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<AttendanceLocationFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -362,5 +288,6 @@ export default function AttendanceLocation() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -8,6 +9,8 @@ import FormField from "../components/FormField";
|
|||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AuditLogFixture from "../fixtures/AuditLogFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -77,13 +80,6 @@ interface AuditLogEntry {
|
|||||||
user_ip: string | null;
|
user_ip: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginationData {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Filters {
|
interface Filters {
|
||||||
search: string;
|
search: string;
|
||||||
action: string;
|
action: string;
|
||||||
@@ -95,9 +91,7 @@ interface Filters {
|
|||||||
export default function AuditLog() {
|
export default function AuditLog() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [pagination, setPagination] = useState<PaginationData | null>(null);
|
|
||||||
const [filters, setFilters] = useState<Filters>({
|
const [filters, setFilters] = useState<Filters>({
|
||||||
search: "",
|
search: "",
|
||||||
action: "",
|
action: "",
|
||||||
@@ -105,19 +99,30 @@ export default function AuditLog() {
|
|||||||
date_from: "",
|
date_from: "",
|
||||||
date_to: "",
|
date_to: "",
|
||||||
});
|
});
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(50);
|
||||||
const [showCleanup, setShowCleanup] = useState(false);
|
const [showCleanup, setShowCleanup] = useState(false);
|
||||||
const [cleanupDays, setCleanupDays] = useState(90);
|
const [cleanupDays, setCleanupDays] = useState(90);
|
||||||
const [cleaning, setCleaning] = useState(false);
|
const [cleaning, setCleaning] = useState(false);
|
||||||
|
|
||||||
const fetchLogs = useCallback(
|
const { data: logsData, isPending } = useQuery({
|
||||||
async (page = 1, perPage = 50) => {
|
queryKey: [
|
||||||
setLoading(true);
|
"audit-log",
|
||||||
try {
|
{
|
||||||
|
search: filters.search,
|
||||||
|
action: filters.action,
|
||||||
|
entityType: filters.entity_type,
|
||||||
|
dateFrom: filters.date_from,
|
||||||
|
dateTo: filters.date_to,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: String(page),
|
page: String(page),
|
||||||
per_page: String(perPage),
|
per_page: String(perPage),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filters.search) params.set("search", filters.search);
|
if (filters.search) params.set("search", filters.search);
|
||||||
if (filters.action) params.set("action", filters.action);
|
if (filters.action) params.set("action", filters.action);
|
||||||
if (filters.entity_type) params.set("entity_type", filters.entity_type);
|
if (filters.entity_type) params.set("entity_type", filters.entity_type);
|
||||||
@@ -127,31 +132,24 @@ export default function AuditLog() {
|
|||||||
const response = await apiFetch(
|
const response = await apiFetch(
|
||||||
`${API_BASE}/audit-log?${params.toString()}`,
|
`${API_BASE}/audit-log?${params.toString()}`,
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
if (response.status === 401) throw new Error("Unauthorized");
|
||||||
|
const result = await response.json();
|
||||||
if (data.success) {
|
if (!result.success)
|
||||||
setLogs(Array.isArray(data.data) ? data.data : []);
|
throw new Error(result.error || "Nepodařilo se načíst audit log");
|
||||||
setPagination({
|
return {
|
||||||
total: data.pagination?.total ?? 0,
|
data: Array.isArray(result.data) ? result.data : [],
|
||||||
page: data.pagination?.page ?? 1,
|
pagination: {
|
||||||
per_page: data.pagination?.limit ?? 50,
|
total: result.pagination?.total ?? 0,
|
||||||
total_pages: data.pagination?.total_pages ?? 1,
|
page: result.pagination?.page ?? 1,
|
||||||
});
|
per_page: result.pagination?.limit ?? perPage,
|
||||||
} else {
|
total_pages: result.pagination?.total_pages ?? 1,
|
||||||
alert.error(data.error || "Nepodařilo se načíst audit log");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[filters, alert],
|
};
|
||||||
);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const logs = logsData?.data ?? [];
|
||||||
fetchLogs();
|
const pagination = logsData?.pagination ?? null;
|
||||||
}, [fetchLogs]);
|
|
||||||
|
|
||||||
if (!hasPermission("settings.audit")) {
|
if (!hasPermission("settings.audit")) {
|
||||||
return <Forbidden />;
|
return <Forbidden />;
|
||||||
@@ -159,14 +157,16 @@ export default function AuditLog() {
|
|||||||
|
|
||||||
const handleFilterChange = (key: keyof Filters, value: string) => {
|
const handleFilterChange = (key: keyof Filters, value: string) => {
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
fetchLogs(newPage, pagination?.per_page || 50);
|
setPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePerPageChange = (newPerPage: number) => {
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
fetchLogs(1, newPerPage);
|
setPage(1);
|
||||||
|
setPerPage(newPerPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCleanup = async () => {
|
const handleCleanup = async () => {
|
||||||
@@ -181,7 +181,7 @@ export default function AuditLog() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success(data.message);
|
alert.success(data.message);
|
||||||
setShowCleanup(false);
|
setShowCleanup(false);
|
||||||
fetchLogs();
|
queryClient.invalidateQueries({ queryKey: ["audit-log"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error);
|
alert.error(data.error);
|
||||||
}
|
}
|
||||||
@@ -197,66 +197,15 @@ export default function AuditLog() {
|
|||||||
return new Date(dateString).toLocaleString("cs-CZ");
|
return new Date(dateString).toLocaleString("cs-CZ");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && logs.length === 0) {
|
if (isPending && logs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="audit-log"
|
||||||
className="admin-skeleton-row"
|
loading={isPending && logs.length === 0}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AuditLogFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "160px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "100px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton"
|
|
||||||
style={{ gap: "0.75rem", padding: "1rem" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "4px" }}
|
|
||||||
/>
|
|
||||||
{Array.from({ length: 8 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "120px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "70px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line flex-1" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "90px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +391,10 @@ export default function AuditLog() {
|
|||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
|
<Skeleton
|
||||||
|
name="audit-log-rows"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -454,52 +407,87 @@ export default function AuditLog() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading &&
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
Array.from({ length: 10 }, (_, i) => (
|
<tr key={i}>
|
||||||
<tr key={`skeleton-${i}`}>
|
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "110px", height: "14px" }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px", height: "14px" }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
style={{
|
||||||
width: "70px",
|
width: 110,
|
||||||
height: "22px",
|
height: 14,
|
||||||
borderRadius: "10px",
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="admin-skeleton-line"
|
style={{
|
||||||
style={{ width: "80px", height: "14px" }}
|
width: 80,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="admin-skeleton-line"
|
style={{
|
||||||
style={{ width: "60%", height: "14px" }}
|
width: 70,
|
||||||
|
height: 22,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className="admin-skeleton-line"
|
style={{
|
||||||
style={{ width: "90px", height: "14px" }}
|
width: 80,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 90,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!loading && logs.length === 0 && (
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Čas</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
<th>Typ entity</th>
|
||||||
|
<th>Popis</th>
|
||||||
|
<th>IP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6}>
|
<td colSpan={6}>
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
@@ -523,7 +511,7 @@ export default function AuditLog() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!loading &&
|
{logs.length > 0 &&
|
||||||
logs.map((log) => (
|
logs.map((log) => (
|
||||||
<tr key={log.id}>
|
<tr key={log.id}>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
@@ -548,6 +536,7 @@ export default function AuditLog() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
import { bankAccountsOptions } from "../lib/queries/common";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import CompanySettingsFixture from "../fixtures/CompanySettingsFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
const DEFAULT_FIELD_ORDER = [
|
const DEFAULT_FIELD_ORDER = [
|
||||||
@@ -68,7 +73,7 @@ export default function CompanySettings({
|
|||||||
}: { embedded?: boolean } = {}) {
|
}: { embedded?: boolean } = {}) {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [uploadingLogo, setUploadingLogo] = useState(false);
|
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||||
const [uploadingLogoDark, setUploadingLogoDark] = useState(false);
|
const [uploadingLogoDark, setUploadingLogoDark] = useState(false);
|
||||||
@@ -89,14 +94,12 @@ export default function CompanySettings({
|
|||||||
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
||||||
...DEFAULT_FIELD_ORDER,
|
...DEFAULT_FIELD_ORDER,
|
||||||
]);
|
]);
|
||||||
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
||||||
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
|
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
|
||||||
"CZK",
|
"CZK",
|
||||||
"EUR",
|
"EUR",
|
||||||
"USD",
|
"USD",
|
||||||
"GBP",
|
"GBP",
|
||||||
]);
|
]);
|
||||||
const [bankLoading, setBankLoading] = useState(true);
|
|
||||||
const [bankSaving, setBankSaving] = useState(false);
|
const [bankSaving, setBankSaving] = useState(false);
|
||||||
const [editingBank, setEditingBank] = useState<number | null>(null);
|
const [editingBank, setEditingBank] = useState<number | null>(null);
|
||||||
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
|
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
|
||||||
@@ -182,45 +185,47 @@ export default function CompanySettings({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// ── TanStack Query: company settings ──
|
||||||
try {
|
const { data: settingsData, isPending: settingsLoading } = useQuery(
|
||||||
const response = await apiFetch(`${API_BASE}/company-settings`);
|
companySettingsOptions(),
|
||||||
if (response.status === 401) return;
|
);
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
// ── TanStack Query: bank accounts ──
|
||||||
const d = result.data;
|
const { data: bankAccountsData, isPending: bankLoading } = useQuery(
|
||||||
|
bankAccountsOptions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const bankAccountsList: BankAccount[] = Array.isArray(bankAccountsData)
|
||||||
|
? (bankAccountsData as unknown as BankAccount[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Populate form state when settings data arrives
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settingsData) return;
|
||||||
|
const d = settingsData as Record<string, unknown>;
|
||||||
setForm({
|
setForm({
|
||||||
company_name: d.company_name || "",
|
company_name: (d.company_name as string) || "",
|
||||||
street: d.street || "",
|
street: (d.street as string) || "",
|
||||||
city: d.city || "",
|
city: (d.city as string) || "",
|
||||||
postal_code: d.postal_code || "",
|
postal_code: (d.postal_code as string) || "",
|
||||||
country: d.country || "",
|
country: (d.country as string) || "",
|
||||||
company_id: d.company_id || "",
|
company_id: (d.company_id as string) || "",
|
||||||
vat_id: d.vat_id || "",
|
vat_id: (d.vat_id as string) || "",
|
||||||
});
|
});
|
||||||
const cf =
|
const cf: CustomField[] =
|
||||||
Array.isArray(d.custom_fields) && d.custom_fields.length > 0
|
Array.isArray(d.custom_fields) && d.custom_fields.length > 0
|
||||||
? d.custom_fields.map(
|
? (d.custom_fields as CustomField[]).map((f, i) => ({
|
||||||
(
|
|
||||||
f: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
showLabel?: boolean;
|
|
||||||
_key?: string;
|
|
||||||
},
|
|
||||||
i: number,
|
|
||||||
) => ({
|
|
||||||
...f,
|
...f,
|
||||||
|
showLabel: f.showLabel !== false,
|
||||||
_key: f._key || `cf-${Date.now()}-${i}`,
|
_key: f._key || `cf-${Date.now()}-${i}`,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
: [];
|
: [];
|
||||||
setCustomFields(cf);
|
setCustomFields(cf);
|
||||||
if (
|
if (
|
||||||
Array.isArray(d.supplier_field_order) &&
|
Array.isArray(d.supplier_field_order) &&
|
||||||
d.supplier_field_order.length > 0
|
d.supplier_field_order.length > 0
|
||||||
) {
|
) {
|
||||||
setFieldOrder(d.supplier_field_order);
|
setFieldOrder(d.supplier_field_order as string[]);
|
||||||
} else {
|
} else {
|
||||||
setFieldOrder([...DEFAULT_FIELD_ORDER]);
|
setFieldOrder([...DEFAULT_FIELD_ORDER]);
|
||||||
}
|
}
|
||||||
@@ -228,7 +233,7 @@ export default function CompanySettings({
|
|||||||
Array.isArray(d.available_currencies) &&
|
Array.isArray(d.available_currencies) &&
|
||||||
d.available_currencies.length > 0
|
d.available_currencies.length > 0
|
||||||
) {
|
) {
|
||||||
setAvailableCurrencies(d.available_currencies);
|
setAvailableCurrencies(d.available_currencies as string[]);
|
||||||
}
|
}
|
||||||
if (d.has_logo) {
|
if (d.has_logo) {
|
||||||
fetchLogo("light");
|
fetchLogo("light");
|
||||||
@@ -236,30 +241,7 @@ export default function CompanySettings({
|
|||||||
if (d.has_logo_dark) {
|
if (d.has_logo_dark) {
|
||||||
fetchLogo("dark");
|
fetchLogo("dark");
|
||||||
}
|
}
|
||||||
} else {
|
}, [settingsData, fetchLogo]);
|
||||||
alert.error(result.error || "Nepodařilo se načíst nastavení");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert, fetchLogo]);
|
|
||||||
|
|
||||||
const fetchBankAccounts = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/bank-accounts`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setBankAccounts(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
} finally {
|
|
||||||
setBankLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resetBankForm = () => {
|
const resetBankForm = () => {
|
||||||
setEditingBank(null);
|
setEditingBank(null);
|
||||||
@@ -294,7 +276,7 @@ export default function CompanySettings({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
resetBankForm();
|
resetBankForm();
|
||||||
fetchBankAccounts();
|
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při ukládání");
|
alert.error(result.error || "Chyba při ukládání");
|
||||||
}
|
}
|
||||||
@@ -322,7 +304,7 @@ export default function CompanySettings({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||||
fetchBankAccounts();
|
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při mazání");
|
alert.error(result.error || "Chyba při mazání");
|
||||||
}
|
}
|
||||||
@@ -346,11 +328,6 @@ export default function CompanySettings({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
fetchBankAccounts();
|
|
||||||
}, [fetchData, fetchBankAccounts]);
|
|
||||||
|
|
||||||
// Cleanup blob URLs on unmount
|
// Cleanup blob URLs on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -377,6 +354,7 @@ export default function CompanySettings({
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Nastavení bylo uloženo");
|
alert.success(result.message || "Nastavení bylo uloženo");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||||
}
|
}
|
||||||
@@ -411,6 +389,7 @@ export default function CompanySettings({
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Logo bylo nahráno");
|
alert.success(result.message || "Logo bylo nahráno");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||||
fetchLogo(variant);
|
fetchLogo(variant);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se nahrát logo");
|
alert.error(result.error || "Nepodařilo se nahrát logo");
|
||||||
@@ -429,50 +408,15 @@ export default function CompanySettings({
|
|||||||
|
|
||||||
if (!embedded && !hasPermission("settings.manage")) return <Forbidden />;
|
if (!embedded && !hasPermission("settings.manage")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
if (settingsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="company-settings"
|
||||||
className="admin-skeleton-row"
|
loading={settingsLoading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<CompanySettingsFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: "1.25rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "60%" }}
|
|
||||||
/>
|
|
||||||
{[0, 1, 2].map((j) => (
|
|
||||||
<div key={j} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,18 +718,16 @@ export default function CompanySettings({
|
|||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{bankLoading ? (
|
{bankLoading ? (
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
<Skeleton
|
||||||
{[0, 1, 2].map((i) => (
|
name="company-settings-bank"
|
||||||
<div key={i} className="admin-skeleton-row">
|
loading={bankLoading}
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
fixture={<CompanySettingsFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
<div />
|
||||||
</div>
|
</Skeleton>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bankAccounts.length > 0 && (
|
{bankAccountsList.length > 0 && (
|
||||||
<div className="admin-table-responsive mb-4">
|
<div className="admin-table-responsive mb-4">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -801,7 +743,7 @@ export default function CompanySettings({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{bankAccounts.map((acc) => (
|
{bankAccountsList.map((acc) => (
|
||||||
<tr
|
<tr
|
||||||
key={acc.id}
|
key={acc.id}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { dashboardOptions } from "../lib/queries/dashboard";
|
||||||
|
import { require2FAOptions } from "../lib/queries/settings";
|
||||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||||
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||||
@@ -12,6 +15,8 @@ import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
|||||||
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
||||||
import DashProfile from "../components/dashboard/DashProfile";
|
import DashProfile from "../components/dashboard/DashProfile";
|
||||||
import DashSessions from "../components/dashboard/DashSessions";
|
import DashSessions from "../components/dashboard/DashSessions";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import DashboardFixture from "../fixtures/DashboardFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -69,13 +74,17 @@ export default function Dashboard() {
|
|||||||
const { user, updateUser, hasPermission } = useAuth();
|
const { user, updateUser, hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
|
||||||
const [dashData, setDashData] = useState<DashData | null>(null);
|
|
||||||
const [dashLoading, setDashLoading] = useState(true);
|
|
||||||
const [punching, setPunching] = useState(false);
|
const [punching, setPunching] = useState(false);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: dashDataRaw, isPending: dashLoading } =
|
||||||
|
useQuery(dashboardOptions());
|
||||||
|
const dashData = dashDataRaw as DashData | undefined;
|
||||||
|
const { data: totpData, isPending: totpLoading } =
|
||||||
|
useQuery(require2FAOptions());
|
||||||
|
const totpEnabled = totpData?.require_2fa ?? !!user?.totpEnabled;
|
||||||
|
|
||||||
// 2FA state - sdileny mezi profilem a bannerem
|
// 2FA state - sdileny mezi profilem a bannerem
|
||||||
const [totpEnabled, setTotpEnabled] = useState(false);
|
|
||||||
const [totpLoading, setTotpLoading] = useState(true);
|
|
||||||
const [show2FASetup, setShow2FASetup] = useState(false);
|
const [show2FASetup, setShow2FASetup] = useState(false);
|
||||||
const [show2FADisable, setShow2FADisable] = useState(false);
|
const [show2FADisable, setShow2FADisable] = useState(false);
|
||||||
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
||||||
@@ -88,46 +97,6 @@ export default function Dashboard() {
|
|||||||
useModalLock(show2FASetup);
|
useModalLock(show2FASetup);
|
||||||
useModalLock(show2FADisable);
|
useModalLock(show2FADisable);
|
||||||
|
|
||||||
const fetchDashboard = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/dashboard`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success !== false) {
|
|
||||||
setDashData(data.data || data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.error("Dashboard fetch error:", err);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setDashLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDashboard();
|
|
||||||
}, [fetchDashboard]);
|
|
||||||
|
|
||||||
// 2FA status fetch
|
|
||||||
const fetch2FAStatus = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/totp/setup`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setTotpEnabled(!!user?.totpEnabled);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 2FA status fetch failed silently
|
|
||||||
setTotpEnabled(!!user?.totpEnabled);
|
|
||||||
} finally {
|
|
||||||
setTotpLoading(false);
|
|
||||||
}
|
|
||||||
}, [user?.totpEnabled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch2FAStatus();
|
|
||||||
}, [fetch2FAStatus]);
|
|
||||||
|
|
||||||
// Punch (prichod/odchod) primo z dashboardu
|
// Punch (prichod/odchod) primo z dashboardu
|
||||||
const handleQuickPunch = useCallback(() => {
|
const handleQuickPunch = useCallback(() => {
|
||||||
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
||||||
@@ -143,7 +112,7 @@ export default function Dashboard() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
alert.success(result.data?.message || "Docházka zaznamenána");
|
||||||
fetchDashboard();
|
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při záznamu docházky");
|
alert.error(result.error || "Chyba při záznamu docházky");
|
||||||
}
|
}
|
||||||
@@ -167,7 +136,7 @@ export default function Dashboard() {
|
|||||||
() => submitPunch({}),
|
() => submitPunch({}),
|
||||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||||
);
|
);
|
||||||
}, [dashData, alert, fetchDashboard]);
|
}, [dashData, alert, queryClient]);
|
||||||
|
|
||||||
// 2FA handlery
|
// 2FA handlery
|
||||||
const handleStart2FASetup = async () => {
|
const handleStart2FASetup = async () => {
|
||||||
@@ -202,7 +171,7 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTotpEnabled(true);
|
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||||
setBackupCodes(data.data?.backup_codes || null);
|
setBackupCodes(data.data?.backup_codes || null);
|
||||||
setTotpSecret(null);
|
setTotpSecret(null);
|
||||||
setTotpQrUri(null);
|
setTotpQrUri(null);
|
||||||
@@ -230,7 +199,7 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTotpEnabled(false);
|
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||||
setShow2FADisable(false);
|
setShow2FADisable(false);
|
||||||
setDisableCode("");
|
setDisableCode("");
|
||||||
updateUser({ totpEnabled: false });
|
updateUser({ totpEnabled: false });
|
||||||
@@ -337,62 +306,13 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Skeleton loading */}
|
{/* Skeleton loading */}
|
||||||
{dashLoading && (
|
{dashLoading && (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.25rem" }}>
|
<Skeleton
|
||||||
<div className="admin-kpi-grid admin-kpi-4">
|
name="dashboard"
|
||||||
{[0, 1, 2, 3].map((i) => (
|
loading={dashLoading}
|
||||||
<div
|
fixture={<DashboardFixture />}
|
||||||
key={i}
|
|
||||||
className="admin-skeleton-line h-24"
|
|
||||||
style={{ borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="dash-quick-actions">
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "52px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="dash-main-grid">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "320px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "320px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1.25rem",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div />
|
||||||
className="admin-skeleton-line"
|
</Skeleton>
|
||||||
style={{ height: "150px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "150px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="dash-bottom">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "200px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "200px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
{/* KPI cards — only show if user has any admin-level permissions */}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {
|
|||||||
useParams,
|
useParams,
|
||||||
Link,
|
Link,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import InvoiceDetailFixture from "../fixtures/InvoiceDetailFixture";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
@@ -35,6 +38,11 @@ import {
|
|||||||
} from "@dnd-kit/modifiers";
|
} from "@dnd-kit/modifiers";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
import { invoiceDetailOptions } from "../lib/queries/invoices";
|
||||||
|
import { offerCustomersOptions } from "../lib/queries/offers";
|
||||||
|
import { bankAccountsOptions } from "../lib/queries/common";
|
||||||
|
import { jsonQuery } from "../lib/apiAdapter";
|
||||||
import { formatCurrency, formatDate } from "../utils/formatters";
|
import { formatCurrency, formatDate } from "../utils/formatters";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -367,7 +375,6 @@ export default function InvoiceDetail() {
|
|||||||
bank_account: "",
|
bank_account: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
||||||
const [dueDays, setDueDays] = useState(14);
|
const [dueDays, setDueDays] = useState(14);
|
||||||
const [items, setItems] = useState<InvoiceItem[]>([
|
const [items, setItems] = useState<InvoiceItem[]>([
|
||||||
{
|
{
|
||||||
@@ -381,44 +388,37 @@ export default function InvoiceDetail() {
|
|||||||
]);
|
]);
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [dataReady, setDataReady] = useState(false);
|
||||||
const [invoiceNumber, setInvoiceNumber] = useState("");
|
const [invoiceNumber, setInvoiceNumber] = useState("");
|
||||||
const initialSnapshotRef = useRef<string | null>(null);
|
const initialSnapshotRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
||||||
const [customerSearch, setCustomerSearch] = useState("");
|
const [customerSearch, setCustomerSearch] = useState("");
|
||||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
||||||
|
|
||||||
const [companySettings, setCompanySettings] = useState<{
|
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||||
|
| {
|
||||||
default_currency: string;
|
default_currency: string;
|
||||||
default_vat_rate: number;
|
default_vat_rate: number;
|
||||||
available_currencies: string[];
|
available_currencies: string[];
|
||||||
available_vat_rates: number[];
|
available_vat_rates: number[];
|
||||||
} | null>(null);
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch(`${API_BASE}/company-settings`)
|
if (companySettings && !isEdit) {
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
|
||||||
if (d.success) {
|
|
||||||
setCompanySettings(d.data);
|
|
||||||
if (!isEdit) {
|
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
currency:
|
currency:
|
||||||
prev.currency === "CZK"
|
prev.currency === "CZK"
|
||||||
? d.data.default_currency || "CZK"
|
? companySettings.default_currency || "CZK"
|
||||||
: prev.currency,
|
: prev.currency,
|
||||||
vat_rate:
|
vat_rate:
|
||||||
prev.vat_rate === 21
|
prev.vat_rate === 21
|
||||||
? (d.data.default_vat_rate ?? 21)
|
? (companySettings.default_vat_rate ?? 21)
|
||||||
: prev.vat_rate,
|
: prev.vat_rate,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}, [companySettings, isEdit]);
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const vatOptions = (
|
const vatOptions = (
|
||||||
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
|
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
|
||||||
@@ -437,8 +437,44 @@ export default function InvoiceDetail() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ─── TanStack Query ───
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const customersQuery = useQuery(offerCustomersOptions());
|
||||||
|
const customers = useMemo<Customer[]>(() => {
|
||||||
|
const data = customersQuery.data;
|
||||||
|
if (!data) return [];
|
||||||
|
if (Array.isArray(data)) return data as Customer[];
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
if (Array.isArray(obj.customers)) return obj.customers as Customer[];
|
||||||
|
return [];
|
||||||
|
}, [customersQuery.data]);
|
||||||
|
|
||||||
|
const bankAccountsQuery = useQuery(bankAccountsOptions());
|
||||||
|
const bankAccounts = (bankAccountsQuery.data ?? []) as BankAccount[];
|
||||||
|
|
||||||
|
const invoiceQuery = useQuery(invoiceDetailOptions(id));
|
||||||
|
const invoice = (invoiceQuery.data as Invoice | undefined) ?? null;
|
||||||
|
|
||||||
|
const nextNumberQuery = useQuery({
|
||||||
|
queryKey: ["invoices", "next-number"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ next_number?: string; number?: string }>(
|
||||||
|
`${API_BASE}/invoices/next-number`,
|
||||||
|
).then((d) => d?.next_number || d?.number || ""),
|
||||||
|
enabled: !isEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderDataQuery = useQuery({
|
||||||
|
queryKey: ["invoices", "order-data", fromOrderId],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`${API_BASE}/invoices/order-data/${fromOrderId}`,
|
||||||
|
),
|
||||||
|
enabled: !!fromOrderId,
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Edit mode state ───
|
// ─── Edit mode state ───
|
||||||
const [invoice, setInvoice] = useState<Invoice | null>(null);
|
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [statusChanging, setStatusChanging] = useState<string | null>(null);
|
const [statusChanging, setStatusChanging] = useState<string | null>(null);
|
||||||
const [statusConfirm, setStatusConfirm] = useState<{
|
const [statusConfirm, setStatusConfirm] = useState<{
|
||||||
@@ -456,186 +492,69 @@ export default function InvoiceDetail() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ─── Data loading ───
|
// ─── Sync query data to form state ───
|
||||||
|
|
||||||
|
// Edit mode: populate form from invoice data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit) return;
|
if (!isEdit || dataReady) return;
|
||||||
const load = async () => {
|
if (
|
||||||
try {
|
invoiceQuery.isLoading ||
|
||||||
const promises = [
|
bankAccountsQuery.isLoading ||
|
||||||
apiFetch(`${API_BASE}/invoices/next-number`),
|
customersQuery.isLoading
|
||||||
apiFetch(`${API_BASE}/customers`),
|
)
|
||||||
apiFetch(`${API_BASE}/bank-accounts`),
|
return;
|
||||||
];
|
if (!invoiceQuery.data) return;
|
||||||
if (fromOrderId) {
|
|
||||||
promises.push(
|
|
||||||
apiFetch(`${API_BASE}/invoices/order-data/${fromOrderId}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const inv = invoiceQuery.data as Record<string, unknown>;
|
||||||
|
|
||||||
const numRes = results[0];
|
// Match bank account from invoice's bank details
|
||||||
if (numRes.ok) {
|
|
||||||
const numData = await numRes.json();
|
|
||||||
if (numData.success)
|
|
||||||
setInvoiceNumber(
|
|
||||||
numData.data?.next_number || numData.data?.number || "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const custRes = results[1];
|
|
||||||
if (custRes.ok) {
|
|
||||||
const custData = await custRes.json();
|
|
||||||
if (custData.success)
|
|
||||||
setCustomers(
|
|
||||||
Array.isArray(custData.data)
|
|
||||||
? custData.data
|
|
||||||
: custData.data?.customers || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bankRes = results[2];
|
|
||||||
if (bankRes.ok) {
|
|
||||||
const bankData = await bankRes.json();
|
|
||||||
if (bankData.success && Array.isArray(bankData.data)) {
|
|
||||||
setBankAccounts(bankData.data);
|
|
||||||
const defaultAcc = bankData.data.find(
|
|
||||||
(a: BankAccount) => a.is_default,
|
|
||||||
);
|
|
||||||
if (defaultAcc) {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
bank_account_id: defaultAcc.id,
|
|
||||||
bank_name: defaultAcc.bank_name || "",
|
|
||||||
bank_swift: defaultAcc.bic || "",
|
|
||||||
bank_iban: defaultAcc.iban || "",
|
|
||||||
bank_account: defaultAcc.account_number || "",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-fill from order
|
|
||||||
if (fromOrderId && results[3]?.ok) {
|
|
||||||
const orderData = await results[3].json();
|
|
||||||
if (orderData.success) {
|
|
||||||
const order = orderData.data;
|
|
||||||
const vatRate =
|
|
||||||
Number(order.vat_rate) ||
|
|
||||||
(companySettings?.default_vat_rate ?? 21);
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
customer_id: order.customer_id,
|
|
||||||
customer_name: order.customer_name || "",
|
|
||||||
order_id: order.id,
|
|
||||||
currency:
|
|
||||||
order.currency || companySettings?.default_currency || "CZK",
|
|
||||||
apply_vat: Number(order.apply_vat) || 0,
|
|
||||||
vat_rate: vatRate,
|
|
||||||
}));
|
|
||||||
if (order.items?.length > 0) {
|
|
||||||
setItems(
|
|
||||||
order.items.map((item: Record<string, unknown>) => ({
|
|
||||||
_key: `inv-${++keyCounterRef.current}`,
|
|
||||||
description: (item.description as string) || "",
|
|
||||||
quantity: Number(item.quantity) || 1,
|
|
||||||
unit: (item.unit as string) || "",
|
|
||||||
unit_price: Number(item.unit_price) || 0,
|
|
||||||
vat_rate: vatRate,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba při načítání dat");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
load();
|
|
||||||
}, [isEdit, fromOrderId, alert]);
|
|
||||||
|
|
||||||
// Edit mode: load existing invoice
|
|
||||||
const fetchDetail = useCallback(async () => {
|
|
||||||
if (!id) return;
|
|
||||||
try {
|
|
||||||
const [response, custRes, bankRes] = await Promise.all([
|
|
||||||
apiFetch(`${API_BASE}/invoices/${id}`),
|
|
||||||
apiFetch(`${API_BASE}/customers`),
|
|
||||||
apiFetch(`${API_BASE}/bank-accounts`),
|
|
||||||
]);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const inv = result.data;
|
|
||||||
setInvoice(inv);
|
|
||||||
setNotes(inv.notes || "");
|
|
||||||
setInvoiceNumber(inv.invoice_number || "");
|
|
||||||
|
|
||||||
// Populate customers list
|
|
||||||
if (custRes.ok) {
|
|
||||||
const custData = await custRes.json();
|
|
||||||
if (custData.success)
|
|
||||||
setCustomers(
|
|
||||||
Array.isArray(custData.data)
|
|
||||||
? custData.data
|
|
||||||
: custData.data?.customers || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate bank accounts and match existing
|
|
||||||
let matchedBankId: number | string = "";
|
let matchedBankId: number | string = "";
|
||||||
if (bankRes.ok) {
|
const bankData = bankAccountsQuery.data ?? [];
|
||||||
const bankData = await bankRes.json();
|
if (Array.isArray(bankData)) {
|
||||||
if (bankData.success && Array.isArray(bankData.data)) {
|
const match = bankData.find(
|
||||||
setBankAccounts(bankData.data);
|
|
||||||
// Match by IBAN or account number
|
|
||||||
const match = bankData.data.find(
|
|
||||||
(b: BankAccount) =>
|
(b: BankAccount) =>
|
||||||
(inv.bank_iban && b.iban === inv.bank_iban) ||
|
(inv.bank_iban && b.iban === inv.bank_iban) ||
|
||||||
(inv.bank_account && b.account_number === inv.bank_account),
|
(inv.bank_account && b.account_number === inv.bank_account),
|
||||||
);
|
);
|
||||||
if (match) matchedBankId = match.id;
|
if (match) matchedBankId = match.id;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Populate form state from existing invoice
|
|
||||||
const formData = {
|
const formData = {
|
||||||
customer_id: inv.customer_id || null,
|
customer_id: (inv.customer_id as number) || null,
|
||||||
customer_name: inv.customer_name || "",
|
customer_name: (inv.customer_name as string) || "",
|
||||||
order_id: inv.order_id || null,
|
order_id: (inv.order_id as number) || null,
|
||||||
issue_date: inv.issue_date
|
issue_date: inv.issue_date
|
||||||
? new Date(inv.issue_date).toISOString().split("T")[0]
|
? new Date(inv.issue_date as string).toISOString().split("T")[0]
|
||||||
: "",
|
: "",
|
||||||
due_date: inv.due_date
|
due_date: inv.due_date
|
||||||
? new Date(inv.due_date).toISOString().split("T")[0]
|
? new Date(inv.due_date as string).toISOString().split("T")[0]
|
||||||
: "",
|
: "",
|
||||||
tax_date: inv.tax_date
|
tax_date: inv.tax_date
|
||||||
? new Date(inv.tax_date).toISOString().split("T")[0]
|
? new Date(inv.tax_date as string).toISOString().split("T")[0]
|
||||||
: "",
|
: "",
|
||||||
currency: inv.currency || "CZK",
|
currency: (inv.currency as string) || "CZK",
|
||||||
apply_vat: Number(inv.apply_vat) ? 1 : 0,
|
apply_vat: Number(inv.apply_vat) ? 1 : 0,
|
||||||
vat_rate: Number(inv.vat_rate) || 21,
|
vat_rate: Number(inv.vat_rate) || 21,
|
||||||
payment_method: inv.payment_method || "Příkazem",
|
payment_method: (inv.payment_method as string) || "Příkazem",
|
||||||
constant_symbol: inv.constant_symbol || "0308",
|
constant_symbol: (inv.constant_symbol as string) || "0308",
|
||||||
issued_by: inv.issued_by || "",
|
issued_by: (inv.issued_by as string) || "",
|
||||||
billing_text: inv.billing_text || "",
|
billing_text: (inv.billing_text as string) || "",
|
||||||
notes: inv.notes || "",
|
notes: (inv.notes as string) || "",
|
||||||
language: inv.language || "cs",
|
language: (inv.language as string) || "cs",
|
||||||
bank_account_id: matchedBankId,
|
bank_account_id: matchedBankId,
|
||||||
bank_name: inv.bank_name || "",
|
bank_name: (inv.bank_name as string) || "",
|
||||||
bank_swift: inv.bank_swift || "",
|
bank_swift: (inv.bank_swift as string) || "",
|
||||||
bank_iban: inv.bank_iban || "",
|
bank_iban: (inv.bank_iban as string) || "",
|
||||||
bank_account: inv.bank_account || "",
|
bank_account: (inv.bank_account as string) || "",
|
||||||
};
|
};
|
||||||
setForm(formData);
|
setForm(formData);
|
||||||
|
setNotes((inv.notes as string) || "");
|
||||||
|
setInvoiceNumber((inv.invoice_number as string) || "");
|
||||||
|
|
||||||
// Calculate dueDays from existing dates
|
// Calculate dueDays from existing dates
|
||||||
if (inv.issue_date && inv.due_date) {
|
if (inv.issue_date && inv.due_date) {
|
||||||
const issue = new Date(inv.issue_date);
|
const issue = new Date(inv.issue_date as string);
|
||||||
const due = new Date(inv.due_date);
|
const due = new Date(inv.due_date as string);
|
||||||
const diffDays = Math.round(
|
const diffDays = Math.round(
|
||||||
(due.getTime() - issue.getTime()) / (1000 * 60 * 60 * 24),
|
(due.getTime() - issue.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
);
|
);
|
||||||
@@ -643,9 +562,10 @@ export default function InvoiceDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate items from existing invoice
|
// Populate items from existing invoice
|
||||||
|
const invItems = inv.items as Record<string, unknown>[] | undefined;
|
||||||
const mappedItems =
|
const mappedItems =
|
||||||
inv.items?.length > 0
|
invItems && invItems.length > 0
|
||||||
? inv.items.map((item: Record<string, unknown>) => ({
|
? invItems.map((item) => ({
|
||||||
_key: `inv-${++keyCounterRef.current}`,
|
_key: `inv-${++keyCounterRef.current}`,
|
||||||
id: item.id as number | undefined,
|
id: item.id as number | undefined,
|
||||||
description: (item.description as string) || "",
|
description: (item.description as string) || "",
|
||||||
@@ -664,26 +584,99 @@ export default function InvoiceDetail() {
|
|||||||
form: formData,
|
form: formData,
|
||||||
items: mappedItems,
|
items: mappedItems,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se načíst fakturu");
|
|
||||||
navigate("/invoices");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
navigate("/invoices");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [id, alert, navigate]);
|
|
||||||
|
|
||||||
|
setDataReady(true);
|
||||||
|
}, [
|
||||||
|
isEdit,
|
||||||
|
dataReady,
|
||||||
|
invoiceQuery.isLoading,
|
||||||
|
invoiceQuery.data,
|
||||||
|
bankAccountsQuery.isLoading,
|
||||||
|
bankAccountsQuery.data,
|
||||||
|
customersQuery.isLoading,
|
||||||
|
]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Create mode: populate form from query data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit) fetchDetail();
|
if (isEdit || dataReady) return;
|
||||||
}, [isEdit, fetchDetail]);
|
if (
|
||||||
|
nextNumberQuery.isLoading ||
|
||||||
|
bankAccountsQuery.isLoading ||
|
||||||
|
customersQuery.isLoading
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (fromOrderId && orderDataQuery.isLoading) return;
|
||||||
|
|
||||||
// Capture initial snapshot for dirty-checking once data finishes loading.
|
// Set invoice number
|
||||||
// Edit mode: captured inside fetchDetail from raw API data.
|
if (nextNumberQuery.data) {
|
||||||
// Create mode: captured on first stable render after loading completes.
|
setInvoiceNumber(nextNumberQuery.data);
|
||||||
if (!loading && !initialSnapshotRef.current) {
|
}
|
||||||
|
|
||||||
|
// Set default bank account
|
||||||
|
const defaultAcc = bankAccounts.find((a: BankAccount) => a.is_default);
|
||||||
|
if (defaultAcc) {
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
bank_account_id: defaultAcc.id,
|
||||||
|
bank_name: defaultAcc.bank_name || "",
|
||||||
|
bank_swift: defaultAcc.bic || "",
|
||||||
|
bank_iban: defaultAcc.iban || "",
|
||||||
|
bank_account: defaultAcc.account_number || "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fill from order
|
||||||
|
if (fromOrderId && orderDataQuery.data) {
|
||||||
|
const order = orderDataQuery.data;
|
||||||
|
const vatRate =
|
||||||
|
Number(order.vat_rate) || (companySettings?.default_vat_rate ?? 21);
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
customer_id: order.customer_id as number,
|
||||||
|
customer_name: (order.customer_name as string) || "",
|
||||||
|
order_id: order.id as number,
|
||||||
|
currency:
|
||||||
|
(order.currency as string) ||
|
||||||
|
companySettings?.default_currency ||
|
||||||
|
"CZK",
|
||||||
|
apply_vat: Number(order.apply_vat) || 0,
|
||||||
|
vat_rate: vatRate,
|
||||||
|
}));
|
||||||
|
const orderItems = order.items as Record<string, unknown>[] | undefined;
|
||||||
|
if (orderItems && orderItems.length > 0) {
|
||||||
|
setItems(
|
||||||
|
orderItems.map((item) => ({
|
||||||
|
_key: `inv-${++keyCounterRef.current}`,
|
||||||
|
description: (item.description as string) || "",
|
||||||
|
quantity: Number(item.quantity) || 1,
|
||||||
|
unit: (item.unit as string) || "",
|
||||||
|
unit_price: Number(item.unit_price) || 0,
|
||||||
|
vat_rate: vatRate,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataReady(true);
|
||||||
|
}, [
|
||||||
|
isEdit,
|
||||||
|
dataReady,
|
||||||
|
nextNumberQuery.isLoading,
|
||||||
|
nextNumberQuery.data,
|
||||||
|
bankAccountsQuery.isLoading,
|
||||||
|
bankAccountsQuery.data,
|
||||||
|
customersQuery.isLoading,
|
||||||
|
fromOrderId,
|
||||||
|
orderDataQuery.isLoading,
|
||||||
|
orderDataQuery.data,
|
||||||
|
companySettings,
|
||||||
|
bankAccounts,
|
||||||
|
]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Capture initial snapshot for dirty-checking once data sync completes.
|
||||||
|
// Edit mode: captured inside the sync effect from raw query data.
|
||||||
|
// Create mode: captured on the first render after sync effects populate the form.
|
||||||
|
if (dataReady && !initialSnapshotRef.current) {
|
||||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,9 +859,12 @@ export default function InvoiceDetail() {
|
|||||||
);
|
);
|
||||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
fetchDetail();
|
queryClient.invalidateQueries({ queryKey: ["invoices", id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
navigate(`/invoices/${result.data.invoice_id}`);
|
navigate(`/invoices/${result.data.invoice_id}`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert.error(
|
alert.error(
|
||||||
@@ -899,7 +895,8 @@ export default function InvoiceDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Stav byl změněn");
|
alert.success(result.message || "Stav byl změněn");
|
||||||
fetchDetail();
|
queryClient.invalidateQueries({ queryKey: ["invoices", id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
alert.error(result.error || "Nepodařilo se změnit stav");
|
||||||
}
|
}
|
||||||
@@ -944,6 +941,8 @@ export default function InvoiceDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Faktura byla smazána");
|
alert.success(result.message || "Faktura byla smazána");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
navigate("/invoices");
|
navigate("/invoices");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
||||||
@@ -960,53 +959,6 @@ export default function InvoiceDetail() {
|
|||||||
if (!isEdit && !hasPermission("invoices.create")) return <Forbidden />;
|
if (!isEdit && !hasPermission("invoices.create")) return <Forbidden />;
|
||||||
if (isEdit && !hasPermission("invoices.view")) return <Forbidden />;
|
if (isEdit && !hasPermission("invoices.view")) return <Forbidden />;
|
||||||
|
|
||||||
// ─── Loading skeleton ───
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div className="flex-row-gap">
|
|
||||||
{isEdit && (
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isEdit && (
|
|
||||||
<div className="admin-skeleton-row gap-2">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// PAID INVOICE — read-only view
|
// PAID INVOICE — read-only view
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -1016,6 +968,11 @@ export default function InvoiceDetail() {
|
|||||||
|
|
||||||
if (isPaid && invoice) {
|
if (isPaid && invoice) {
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="invoice-detail"
|
||||||
|
loading={!dataReady}
|
||||||
|
fixture={<InvoiceDetailFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -1228,14 +1185,19 @@ export default function InvoiceDetail() {
|
|||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
{item.quantity}{" "}
|
{item.quantity}{" "}
|
||||||
{item.unit && (
|
{item.unit && (
|
||||||
<span className="text-tertiary">{item.unit}</span>
|
<span className="text-tertiary">
|
||||||
|
{item.unit}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
{item.unit || "\u2014"}
|
{item.unit || "\u2014"}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono text-right">
|
<td className="admin-mono text-right">
|
||||||
{formatCurrency(item.unit_price, invoice.currency)}
|
{formatCurrency(
|
||||||
|
item.unit_price,
|
||||||
|
invoice.currency,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
{Number(invoice.apply_vat)
|
{Number(invoice.apply_vat)
|
||||||
@@ -1267,12 +1229,14 @@ export default function InvoiceDetail() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{Number(invoice.apply_vat) > 0 &&
|
{Number(invoice.apply_vat) > 0 &&
|
||||||
Object.entries(createTotals.vatByRate).map(([rate, amount]) => (
|
Object.entries(createTotals.vatByRate).map(
|
||||||
|
([rate, amount]) => (
|
||||||
<div key={rate} className="admin-totals-row">
|
<div key={rate} className="admin-totals-row">
|
||||||
<span>DPH {rate}%:</span>
|
<span>DPH {rate}%:</span>
|
||||||
<span>{formatCurrency(amount, invoice.currency)}</span>
|
<span>{formatCurrency(amount, invoice.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
<div className="admin-totals-row admin-totals-total">
|
<div className="admin-totals-row admin-totals-total">
|
||||||
<span>Celkem k úhradě:</span>
|
<span>Celkem k úhradě:</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -1313,6 +1277,7 @@ export default function InvoiceDetail() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,6 +1285,11 @@ export default function InvoiceDetail() {
|
|||||||
// CREATE MODE + EDIT (not paid) — shared form
|
// CREATE MODE + EDIT (not paid) — shared form
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="invoice-detail"
|
||||||
|
loading={!dataReady}
|
||||||
|
fixture={<InvoiceDetailFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -1473,7 +1443,11 @@ export default function InvoiceDetail() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Odběratel" error={errors.customer_id} required>
|
<FormField
|
||||||
|
label="Odběratel"
|
||||||
|
error={errors.customer_id}
|
||||||
|
required
|
||||||
|
>
|
||||||
{form.customer_id ? (
|
{form.customer_id ? (
|
||||||
<div className="admin-customer-selected">
|
<div className="admin-customer-selected">
|
||||||
<span>{form.customer_name}</span>
|
<span>{form.customer_name}</span>
|
||||||
@@ -1815,15 +1789,19 @@ export default function InvoiceDetail() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{form.apply_vat &&
|
{form.apply_vat &&
|
||||||
Object.entries(createTotals.vatByRate).map(([rate, amount]) => (
|
Object.entries(createTotals.vatByRate).map(
|
||||||
|
([rate, amount]) => (
|
||||||
<div key={rate} className="admin-totals-row">
|
<div key={rate} className="admin-totals-row">
|
||||||
<span>DPH {rate}%:</span>
|
<span>DPH {rate}%:</span>
|
||||||
<span>{formatCurrency(amount, form.currency)}</span>
|
<span>{formatCurrency(amount, form.currency)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
<div className="admin-totals-row admin-totals-total">
|
<div className="admin-totals-row admin-totals-total">
|
||||||
<span>Celkem k úhradě:</span>
|
<span>Celkem k úhradě:</span>
|
||||||
<span>{formatCurrency(createTotals.total, form.currency)}</span>
|
<span>
|
||||||
|
{formatCurrency(createTotals.total, form.currency)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1879,5 +1857,6 @@ export default function InvoiceDetail() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import { useState, useEffect, useRef, lazy, Suspense } from "react";
|
||||||
useState,
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
lazy,
|
|
||||||
Suspense,
|
|
||||||
} from "react";
|
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link, useSearchParams } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
@@ -17,8 +11,18 @@ import apiFetch from "../utils/api";
|
|||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import {
|
||||||
|
invoiceListOptions,
|
||||||
|
invoiceStatsOptions,
|
||||||
|
type Invoice,
|
||||||
|
type InvoiceStats,
|
||||||
|
type CurrencyAmount,
|
||||||
|
} from "../lib/queries/invoices";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import InvoicesFixture from "../fixtures/InvoicesFixture";
|
||||||
|
import ReceivedInvoicesFixture from "../fixtures/ReceivedInvoicesFixture";
|
||||||
|
|
||||||
const ReceivedInvoices = lazy(() => import("./ReceivedInvoices"));
|
const ReceivedInvoices = lazy(() => import("./ReceivedInvoices"));
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -39,11 +43,6 @@ const MONTH_NAMES = [
|
|||||||
"prosinec",
|
"prosinec",
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CurrencyAmount {
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMultiCurrency(amounts: CurrencyAmount[]): string {
|
function formatMultiCurrency(amounts: CurrencyAmount[]): string {
|
||||||
if (!Array.isArray(amounts) || amounts.length === 0) return "0 Kč";
|
if (!Array.isArray(amounts) || amounts.length === 0) return "0 Kč";
|
||||||
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
|
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
|
||||||
@@ -84,31 +83,6 @@ const STATUS_FILTERS = [
|
|||||||
{ value: "overdue", label: "Po splatnosti" },
|
{ value: "overdue", label: "Po splatnosti" },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Invoice {
|
|
||||||
id: number;
|
|
||||||
invoice_number: string;
|
|
||||||
customer_name: string | null;
|
|
||||||
status: string;
|
|
||||||
issue_date: string;
|
|
||||||
due_date: string;
|
|
||||||
total: number;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InvoiceStats {
|
|
||||||
paid_month: CurrencyAmount[];
|
|
||||||
paid_month_czk: number;
|
|
||||||
paid_month_count: number;
|
|
||||||
awaiting: CurrencyAmount[];
|
|
||||||
awaiting_czk: number;
|
|
||||||
awaiting_count: number;
|
|
||||||
overdue: CurrencyAmount[];
|
|
||||||
overdue_czk: number;
|
|
||||||
overdue_count: number;
|
|
||||||
vat_month: CurrencyAmount[];
|
|
||||||
vat_month_czk: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DraftData {
|
interface DraftData {
|
||||||
form: Record<string, unknown>;
|
form: Record<string, unknown>;
|
||||||
items: Record<string, unknown>[];
|
items: Record<string, unknown>[];
|
||||||
@@ -134,8 +108,6 @@ export default function Invoices() {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1);
|
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1);
|
||||||
const [statsYear, setStatsYear] = useState(now.getFullYear());
|
const [statsYear, setStatsYear] = useState(now.getFullYear());
|
||||||
const [stats, setStats] = useState<InvoiceStats | null>(null);
|
|
||||||
const [statsLoading, setStatsLoading] = useState(true);
|
|
||||||
const hasLoadedOnce = useRef(false);
|
const hasLoadedOnce = useRef(false);
|
||||||
const slideDirection = useRef(0);
|
const slideDirection = useRef(0);
|
||||||
const blobUrlRef = useRef<string | null>(null);
|
const blobUrlRef = useRef<string | null>(null);
|
||||||
@@ -154,28 +126,15 @@ export default function Invoices() {
|
|||||||
statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear();
|
statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear();
|
||||||
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`;
|
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`;
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
const statsQuery = useQuery(invoiceStatsOptions(statsMonth, statsYear));
|
||||||
setStatsLoading(true);
|
const stats = statsQuery.data ?? null;
|
||||||
try {
|
|
||||||
const res = await apiFetch(
|
useEffect(() => {
|
||||||
`${API_BASE}/invoices/stats?month=${statsMonth}&year=${statsYear}`,
|
if (statsQuery.data) {
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
setStats(data.data);
|
|
||||||
hasLoadedOnce.current = true;
|
hasLoadedOnce.current = true;
|
||||||
setSlideKey((k) => k + 1);
|
setSlideKey((k) => k + 1);
|
||||||
}
|
}
|
||||||
} catch {
|
}, [statsQuery.data]);
|
||||||
/* ignore */
|
|
||||||
} finally {
|
|
||||||
setStatsLoading(false);
|
|
||||||
}
|
|
||||||
}, [statsMonth, statsYear]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStats();
|
|
||||||
}, [fetchStats]);
|
|
||||||
|
|
||||||
const prevMonth = () => {
|
const prevMonth = () => {
|
||||||
slideDirection.current = -1;
|
slideDirection.current = -1;
|
||||||
@@ -225,24 +184,23 @@ export default function Invoices() {
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: invoices,
|
items: invoices,
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
refetch: fetchData,
|
isPending: initialLoad,
|
||||||
} = useListData<Invoice>("invoices", {
|
isFetching: loading,
|
||||||
|
} = usePaginatedQuery<Invoice>(
|
||||||
|
invoiceListOptions({
|
||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
page,
|
page,
|
||||||
extraParams: {
|
month: statsMonth,
|
||||||
month: String(statsMonth),
|
year: statsYear,
|
||||||
year: String(statsYear),
|
status: statusFilter || undefined,
|
||||||
...(statusFilter ? { status: statusFilter } : {}),
|
}),
|
||||||
},
|
);
|
||||||
errorMsg: "Nepodařilo se načíst faktury",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasPermission("invoices.view")) return <Forbidden />;
|
if (!hasPermission("invoices.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -260,8 +218,8 @@ export default function Invoices() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, invoice: null });
|
setDeleteConfirm({ show: false, invoice: null });
|
||||||
alert.success(result.message || "Faktura byla smazána");
|
alert.success(result.message || "Faktura byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
fetchStats();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
||||||
}
|
}
|
||||||
@@ -283,8 +241,8 @@ export default function Invoices() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success("Faktura označena jako zaplacená");
|
alert.success("Faktura označena jako zaplacená");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
fetchStats();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se změnit stav");
|
alert.error(data.error || "Nepodařilo se změnit stav");
|
||||||
}
|
}
|
||||||
@@ -323,81 +281,13 @@ export default function Invoices() {
|
|||||||
|
|
||||||
if (initialLoad) {
|
if (initialLoad) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
name="invoices"
|
||||||
<div
|
loading={initialLoad}
|
||||||
className="admin-skeleton-row"
|
fixture={<InvoicesFixture />}
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-kpi-grid admin-kpi-4">
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-stat-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "60%",
|
|
||||||
height: "11px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
height: "28px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "50%", height: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "70px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "90px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "90px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,35 +418,13 @@ export default function Invoices() {
|
|||||||
>
|
>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-kpi-grid admin-kpi-4"
|
name="invoices-received-kpi"
|
||||||
style={{ marginBottom: "1.5rem" }}
|
loading={true}
|
||||||
|
fixture={<ReceivedInvoicesFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2, 3].map((i) => (
|
<div />
|
||||||
<div key={i} className="admin-stat-card">
|
</Skeleton>
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "60%",
|
|
||||||
height: "11px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
height: "28px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "50%", height: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ReceivedInvoices
|
<ReceivedInvoices
|
||||||
@@ -574,36 +442,14 @@ export default function Invoices() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
transition={{ duration: 0.25, delay: 0.1 }}
|
||||||
>
|
>
|
||||||
{!hasLoadedOnce.current && statsLoading ? (
|
{statsQuery.isPending && !hasLoadedOnce.current ? (
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-kpi-grid admin-kpi-4"
|
name="invoices-kpi"
|
||||||
style={{ marginBottom: "1.5rem" }}
|
loading={statsQuery.isPending && !hasLoadedOnce.current}
|
||||||
|
fixture={<InvoicesFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2, 3].map((i) => (
|
<div />
|
||||||
<div key={i} className="admin-stat-card">
|
</Skeleton>
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "60%",
|
|
||||||
height: "11px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
height: "28px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "50%", height: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
stats && (
|
stats && (
|
||||||
<div style={{ overflow: "hidden", marginBottom: "1.5rem" }}>
|
<div style={{ overflow: "hidden", marginBottom: "1.5rem" }}>
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
|
import {
|
||||||
|
leavePendingOptions,
|
||||||
|
leaveProcessedOptions,
|
||||||
|
} from "../lib/queries/leave";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import LeaveApprovalFixture from "../fixtures/LeaveApprovalFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -101,15 +108,24 @@ function mapLeaveRequest(raw: RawLeaveRequest): LeaveRequest {
|
|||||||
export default function LeaveApproval() {
|
export default function LeaveApproval() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
||||||
"pending",
|
"pending",
|
||||||
);
|
);
|
||||||
const [pendingRequests, setPendingRequests] = useState<LeaveRequest[]>([]);
|
const { data: pendingData, isPending: loading } = useQuery(
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
leavePendingOptions(),
|
||||||
const [processedRequests, setProcessedRequests] = useState<LeaveRequest[]>(
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
const { data: processedData } = useQuery({
|
||||||
|
...leaveProcessedOptions(),
|
||||||
|
enabled: activeTab === "processed",
|
||||||
|
});
|
||||||
|
const pendingRequests =
|
||||||
|
(pendingData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ?? [];
|
||||||
|
const pendingCount = pendingRequests.length;
|
||||||
|
const processedRequests =
|
||||||
|
(processedData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ??
|
||||||
|
[];
|
||||||
|
|
||||||
const [approveModal, setApproveModal] = useState<{
|
const [approveModal, setApproveModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
request: LeaveRequest | null;
|
request: LeaveRequest | null;
|
||||||
@@ -123,67 +139,6 @@ export default function LeaveApproval() {
|
|||||||
|
|
||||||
useModalLock(rejectModal.open);
|
useModalLock(rejectModal.open);
|
||||||
|
|
||||||
const fetchPending = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=pending`,
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const mapped = (result.data as RawLeaveRequest[]).map(mapLeaveRequest);
|
|
||||||
setPendingRequests(mapped);
|
|
||||||
setPendingCount(result.pagination?.total ?? mapped.length);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst žádosti");
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
const fetchProcessed = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=approved`,
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const resultApproved = await response.json();
|
|
||||||
|
|
||||||
const response2 = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=rejected`,
|
|
||||||
);
|
|
||||||
if (response2.status === 401) return;
|
|
||||||
const resultRejected = await response2.json();
|
|
||||||
|
|
||||||
const all = [
|
|
||||||
...(resultApproved.success
|
|
||||||
? (resultApproved.data as RawLeaveRequest[]).map(mapLeaveRequest)
|
|
||||||
: []),
|
|
||||||
...(resultRejected.success
|
|
||||||
? (resultRejected.data as RawLeaveRequest[]).map(mapLeaveRequest)
|
|
||||||
: []),
|
|
||||||
].sort(
|
|
||||||
(a: LeaveRequest, b: LeaveRequest) =>
|
|
||||||
(b.reviewed_at ? new Date(b.reviewed_at).getTime() : 0) -
|
|
||||||
(a.reviewed_at ? new Date(a.reviewed_at).getTime() : 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
setProcessedRequests(all);
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst vyřízené žádosti");
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
fetchPending().finally(() => setLoading(false));
|
|
||||||
}, [fetchPending]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab === "processed" && processedRequests.length === 0) {
|
|
||||||
fetchProcessed();
|
|
||||||
}
|
|
||||||
}, [activeTab, processedRequests.length, fetchProcessed]);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = async () => {
|
||||||
@@ -202,8 +157,7 @@ export default function LeaveApproval() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setApproveModal({ open: false, request: null });
|
setApproveModal({ open: false, request: null });
|
||||||
await fetchPending();
|
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||||
setProcessedRequests([]);
|
|
||||||
alert.success("Žádost byla schválena");
|
alert.success("Žádost byla schválena");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -240,8 +194,7 @@ export default function LeaveApproval() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setRejectModal({ open: false, request: null });
|
setRejectModal({ open: false, request: null });
|
||||||
setRejectNote("");
|
setRejectNote("");
|
||||||
await fetchPending();
|
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||||
setProcessedRequests([]);
|
|
||||||
alert.success("Žádost byla zamítnuta");
|
alert.success("Žádost byla zamítnuta");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -253,43 +206,12 @@ export default function LeaveApproval() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="leave-approval"
|
||||||
className="admin-skeleton-row"
|
loading={loading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<LeaveApprovalFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="admin-skeleton-line w-1/3 mb-2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -370,7 +292,11 @@ export default function LeaveApproval() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{pendingRequests.map((req) => (
|
{pendingRequests.map((req) => (
|
||||||
<div key={req.id} className="admin-card">
|
<div key={req.id} className="admin-card">
|
||||||
@@ -395,7 +321,8 @@ export default function LeaveApproval() {
|
|||||||
<span
|
<span
|
||||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||||
>
|
>
|
||||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
{leaveTypeLabels[req.leave_type] ||
|
||||||
|
req.leave_type}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -413,8 +340,8 @@ export default function LeaveApproval() {
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{req.total_days}{" "}
|
{req.total_days}{" "}
|
||||||
{czechPlural(req.total_days, "den", "dny", "dnů")} (
|
{czechPlural(req.total_days, "den", "dny", "dnů")}{" "}
|
||||||
{req.total_hours}h)
|
({req.total_hours}h)
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
Podáno: {formatDatetime(req.created_at)}
|
Podáno: {formatDatetime(req.created_at)}
|
||||||
@@ -515,7 +442,8 @@ export default function LeaveApproval() {
|
|||||||
<span
|
<span
|
||||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||||
>
|
>
|
||||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
{leaveTypeLabels[req.leave_type] ||
|
||||||
|
req.leave_type}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
@@ -650,5 +578,6 @@ export default function LeaveApproval() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import LeaveRequestsFixture from "../fixtures/LeaveRequestsFixture";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import { leaveRequestsOptions } from "../lib/queries/leave";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -51,33 +55,16 @@ interface LeaveRequest {
|
|||||||
export default function LeaveRequests() {
|
export default function LeaveRequests() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [requests, setRequests] = useState<LeaveRequest[]>([]);
|
const { data: requests = [], isPending } = useQuery(
|
||||||
|
leaveRequestsOptions(true),
|
||||||
|
) as { data: LeaveRequest[]; isPending: boolean };
|
||||||
const [cancelModal, setCancelModal] = useState<{
|
const [cancelModal, setCancelModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
id: number | null;
|
id: number | null;
|
||||||
}>({ open: false, id: null });
|
}>({ open: false, id: null });
|
||||||
const [cancelling, setCancelling] = useState(false);
|
const [cancelling, setCancelling] = useState(false);
|
||||||
|
|
||||||
const fetchRequests = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/leave-requests?mine=1`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setRequests(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst žádosti");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRequests();
|
|
||||||
}, [fetchRequests]);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.record")) return <Forbidden />;
|
if (!hasPermission("attendance.record")) return <Forbidden />;
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
@@ -94,7 +81,7 @@ export default function LeaveRequests() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setCancelModal({ open: false, id: null });
|
setCancelModal({ open: false, id: null });
|
||||||
await fetchRequests();
|
queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -106,48 +93,6 @@ export default function LeaveRequests() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="admin-skeleton-line w-1/3 mb-2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNoteCell(req: LeaveRequest) {
|
function renderNoteCell(req: LeaveRequest) {
|
||||||
const truncate = (text: string) =>
|
const truncate = (text: string) =>
|
||||||
text.length > 40 ? `${text.substring(0, 40)}...` : text;
|
text.length > 40 ? `${text.substring(0, 40)}...` : text;
|
||||||
@@ -176,6 +121,11 @@ export default function LeaveRequests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="leave-requests"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<LeaveRequestsFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -185,7 +135,9 @@ export default function LeaveRequests() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Moje žádosti</h1>
|
<h1 className="admin-page-title">Moje žádosti</h1>
|
||||||
<p className="admin-page-subtitle">Přehled žádostí o nepřítomnost</p>
|
<p className="admin-page-subtitle">
|
||||||
|
Přehled žádostí o nepřítomnost
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -249,7 +201,9 @@ export default function LeaveRequests() {
|
|||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{formatDate(req.date_from)}
|
{formatDate(req.date_from)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">{formatDate(req.date_to)}</td>
|
<td className="admin-mono">
|
||||||
|
{formatDate(req.date_to)}
|
||||||
|
</td>
|
||||||
<td className="admin-mono">{req.total_days}</td>
|
<td className="admin-mono">{req.total_days}</td>
|
||||||
<td className="admin-mono">{req.total_hours}h</td>
|
<td className="admin-mono">{req.total_hours}h</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -300,5 +254,6 @@ export default function LeaveRequests() {
|
|||||||
loading={cancelling}
|
loading={cancelling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
offerDetailOptions,
|
||||||
|
offerCustomersOptions,
|
||||||
|
offerTemplatesOptions,
|
||||||
|
offerNextNumberOptions,
|
||||||
|
} from "../lib/queries/offers";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -41,6 +48,9 @@ import useModalLock from "../hooks/useModalLock";
|
|||||||
import useDebounce from "../hooks/useDebounce";
|
import useDebounce from "../hooks/useDebounce";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency } from "../utils/formatters";
|
import { formatCurrency } from "../utils/formatters";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OfferDetailFixture from "../fixtures/OfferDetailFixture";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
const DRAFT_KEY = "boha_offer_draft";
|
const DRAFT_KEY = "boha_offer_draft";
|
||||||
@@ -196,9 +206,7 @@ function SortableItemRow({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
onChange={(e) =>
|
onChange={(e) => onUpdate("quantity", e.target.value)}
|
||||||
onUpdate("quantity", parseFloat(e.target.value) || 0)
|
|
||||||
}
|
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
step="1"
|
step="1"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -217,9 +225,7 @@ function SortableItemRow({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.unit_price}
|
value={item.unit_price}
|
||||||
onChange={(e) =>
|
onChange={(e) => onUpdate("unit_price", e.target.value)}
|
||||||
onUpdate("unit_price", parseFloat(e.target.value) || 0)
|
|
||||||
}
|
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -268,9 +274,10 @@ function loadOfferDraft(): {
|
|||||||
sections?: unknown[];
|
sections?: unknown[];
|
||||||
} | null {
|
} | null {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("boha_offer_draft");
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
return raw ? JSON.parse(raw) : null;
|
return raw ? JSON.parse(raw) : null;
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error("Failed to load offer draft:", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,24 +310,72 @@ export default function OfferDetail() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// ---- TanStack Query hooks ----
|
||||||
|
const offerQuery = useQuery(offerDetailOptions(id));
|
||||||
|
const { data: customersData } = useQuery({
|
||||||
|
...offerCustomersOptions(),
|
||||||
|
enabled: !isEdit,
|
||||||
|
});
|
||||||
|
const { data: templatesData } = useQuery({
|
||||||
|
...offerTemplatesOptions(),
|
||||||
|
enabled: !isEdit,
|
||||||
|
});
|
||||||
|
const { data: nextNumberData } = useQuery({
|
||||||
|
...offerNextNumberOptions(),
|
||||||
|
enabled: !isEdit,
|
||||||
|
});
|
||||||
|
// Derive typed arrays from query data
|
||||||
|
const customers = (
|
||||||
|
Array.isArray(customersData)
|
||||||
|
? customersData
|
||||||
|
: (customersData as Record<string, unknown>)?.customers
|
||||||
|
? ((customersData as Record<string, unknown>).customers as Customer[])
|
||||||
|
: []
|
||||||
|
) as Customer[];
|
||||||
|
const scopeTemplates = (
|
||||||
|
Array.isArray(templatesData) ? templatesData : []
|
||||||
|
) as Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
scope_template_sections?: Array<{
|
||||||
|
title?: string;
|
||||||
|
title_cz?: string;
|
||||||
|
content?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const loading = isEdit && offerQuery.isLoading;
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||||
const [form, setForm] = useState<OfferForm>(() => {
|
const [form, setForm] = useState<OfferForm>(() => {
|
||||||
const draft = loadOfferDraft();
|
const draft = loadOfferDraft();
|
||||||
if (draft?.form) {
|
if (draft?.form) {
|
||||||
return {
|
return {
|
||||||
...emptyForm,
|
quotation_number:
|
||||||
|
(draft.form.quotation_number as string) || emptyForm.quotation_number,
|
||||||
project_code:
|
project_code:
|
||||||
(draft.form.project_code as string) || emptyForm.project_code,
|
(draft.form.project_code as string) || emptyForm.project_code,
|
||||||
|
customer_id:
|
||||||
|
(draft.form.customer_id as number | null) ?? emptyForm.customer_id,
|
||||||
customer_name:
|
customer_name:
|
||||||
(draft.form.customer_name as string) || emptyForm.customer_name,
|
(draft.form.customer_name as string) || emptyForm.customer_name,
|
||||||
created_at: (draft.form.created_at as string) || emptyForm.created_at,
|
created_at: (draft.form.created_at as string) || emptyForm.created_at,
|
||||||
valid_until:
|
valid_until:
|
||||||
(draft.form.valid_until as string) || emptyForm.valid_until,
|
(draft.form.valid_until as string) || emptyForm.valid_until,
|
||||||
currency: (draft.form.currency as string) || emptyForm.currency,
|
currency: (draft.form.currency as string) || emptyForm.currency,
|
||||||
customer_id:
|
language: (draft.form.language as string) || emptyForm.language,
|
||||||
(draft.form.customer_id as number | null) ?? emptyForm.customer_id,
|
vat_rate: (draft.form.vat_rate as number) ?? emptyForm.vat_rate,
|
||||||
|
apply_vat: (draft.form.apply_vat as boolean) ?? emptyForm.apply_vat,
|
||||||
|
exchange_rate:
|
||||||
|
(draft.form.exchange_rate as string) || emptyForm.exchange_rate,
|
||||||
|
scope_title:
|
||||||
|
(draft.form.scope_title as string) || emptyForm.scope_title,
|
||||||
|
scope_description:
|
||||||
|
(draft.form.scope_description as string) ||
|
||||||
|
emptyForm.scope_description,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return emptyForm;
|
return emptyForm;
|
||||||
@@ -339,19 +394,6 @@ export default function OfferDetail() {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
const [scopeTemplates, setScopeTemplates] = useState<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
scope_template_sections?: Array<{
|
|
||||||
title?: string;
|
|
||||||
title_cz?: string;
|
|
||||||
content?: string;
|
|
||||||
}>;
|
|
||||||
}>
|
|
||||||
>([]);
|
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
||||||
const [customerSearch, setCustomerSearch] = useState("");
|
const [customerSearch, setCustomerSearch] = useState("");
|
||||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
||||||
const [orderInfo, setOrderInfo] = useState<OrderInfo | null>(null);
|
const [orderInfo, setOrderInfo] = useState<OrderInfo | null>(null);
|
||||||
@@ -367,12 +409,31 @@ export default function OfferDetail() {
|
|||||||
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
const [companySettings, setCompanySettings] = useState<{
|
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||||
|
| {
|
||||||
default_currency: string;
|
default_currency: string;
|
||||||
default_vat_rate: number;
|
default_vat_rate: number;
|
||||||
available_currencies: string[];
|
available_currencies: string[];
|
||||||
available_vat_rates: number[];
|
available_vat_rates: number[];
|
||||||
} | null>(null);
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (companySettings && !isEdit) {
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
currency:
|
||||||
|
prev.currency === "CZK"
|
||||||
|
? companySettings.default_currency || "CZK"
|
||||||
|
: prev.currency,
|
||||||
|
vat_rate:
|
||||||
|
prev.vat_rate === 21
|
||||||
|
? (companySettings.default_vat_rate ?? 21)
|
||||||
|
: prev.vat_rate,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [companySettings, isEdit]);
|
||||||
|
|
||||||
const [lockedBy, setLockedBy] = useState<{
|
const [lockedBy, setLockedBy] = useState<{
|
||||||
user_id: number;
|
user_id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -390,30 +451,6 @@ export default function OfferDetail() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
apiFetch(`${API_BASE}/company-settings`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
|
||||||
if (d.success) {
|
|
||||||
setCompanySettings(d.data);
|
|
||||||
if (!isEdit) {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
currency:
|
|
||||||
prev.currency === "CZK"
|
|
||||||
? d.data.default_currency || "CZK"
|
|
||||||
: prev.currency,
|
|
||||||
vat_rate:
|
|
||||||
prev.vat_rate === 21
|
|
||||||
? (d.data.default_vat_rate ?? 21)
|
|
||||||
: prev.vat_rate,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isInvalidated = offerStatus === "invalidated";
|
const isInvalidated = offerStatus === "invalidated";
|
||||||
const isLockedByOther = !!lockedBy;
|
const isLockedByOther = !!lockedBy;
|
||||||
const isExpiredNotInvalidated =
|
const isExpiredNotInvalidated =
|
||||||
@@ -423,41 +460,40 @@ export default function OfferDetail() {
|
|||||||
form.valid_until &&
|
form.valid_until &&
|
||||||
new Date(form.valid_until) < new Date(new Date().toDateString());
|
new Date(form.valid_until) < new Date(new Date().toDateString());
|
||||||
|
|
||||||
const fetchDetail = useCallback(async () => {
|
// Sync offer detail data to form state on first load (edit mode)
|
||||||
if (!id) return;
|
const formInitializedRef = useRef(false);
|
||||||
try {
|
useEffect(() => {
|
||||||
const response = await apiFetch(`${API_BASE}/offers/${id}`);
|
if (!offerQuery.data || formInitializedRef.current) return;
|
||||||
if (response.status === 401) return;
|
const d = offerQuery.data as Record<string, unknown>;
|
||||||
const result = await response.json();
|
const formData: OfferForm = {
|
||||||
if (result.success) {
|
quotation_number: (d.quotation_number as string) || "",
|
||||||
const d = result.data;
|
project_code: (d.project_code as string) || "",
|
||||||
const formData = {
|
customer_id: (d.customer_id as number | null) ?? null,
|
||||||
quotation_number: d.quotation_number || "",
|
customer_name: (d.customer_name as string) || "",
|
||||||
project_code: d.project_code || "",
|
|
||||||
customer_id: d.customer_id || null,
|
|
||||||
customer_name: d.customer_name || "",
|
|
||||||
created_at: d.created_at ? String(d.created_at).substring(0, 10) : "",
|
created_at: d.created_at ? String(d.created_at).substring(0, 10) : "",
|
||||||
valid_until: d.valid_until
|
valid_until: d.valid_until ? String(d.valid_until).substring(0, 10) : "",
|
||||||
? String(d.valid_until).substring(0, 10)
|
currency:
|
||||||
: "",
|
(d.currency as string) || companySettings?.default_currency || "CZK",
|
||||||
currency: d.currency || companySettings?.default_currency || "CZK",
|
language: (d.language as string) || "EN",
|
||||||
language: d.language || "EN",
|
vat_rate:
|
||||||
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
|
(d.vat_rate as number) ?? companySettings?.default_vat_rate ?? 21,
|
||||||
apply_vat: !!d.apply_vat,
|
apply_vat: !!d.apply_vat,
|
||||||
exchange_rate: d.exchange_rate || "",
|
exchange_rate: (d.exchange_rate as string) || "",
|
||||||
scope_title: d.scope_title || "",
|
scope_title: (d.scope_title as string) || "",
|
||||||
scope_description: d.scope_description || "",
|
scope_description: (d.scope_description as string) || "",
|
||||||
};
|
};
|
||||||
setForm(formData);
|
setForm(formData);
|
||||||
const mappedItems = d.items?.length
|
const mappedItems =
|
||||||
? d.items.map((it: any) => ({
|
Array.isArray(d.items) && d.items.length
|
||||||
|
? (d.items as any[]).map((it: any) => ({
|
||||||
...it,
|
...it,
|
||||||
_key: `item-${++itemKeyCounter.current}`,
|
_key: `item-${++itemKeyCounter.current}`,
|
||||||
}))
|
}))
|
||||||
: [emptyItem()];
|
: [emptyItem()];
|
||||||
setItems(mappedItems);
|
setItems(mappedItems);
|
||||||
const mappedSections = d.sections?.length
|
const mappedSections =
|
||||||
? d.sections.map((s: any) => ({
|
Array.isArray(d.sections) && d.sections.length
|
||||||
|
? (d.sections as any[]).map((s: any) => ({
|
||||||
title: s.title || "",
|
title: s.title || "",
|
||||||
title_cz: s.title_cz || "",
|
title_cz: s.title_cz || "",
|
||||||
content: s.content || "",
|
content: s.content || "",
|
||||||
@@ -469,9 +505,9 @@ export default function OfferDetail() {
|
|||||||
items: mappedItems,
|
items: mappedItems,
|
||||||
sections: mappedSections,
|
sections: mappedSections,
|
||||||
});
|
});
|
||||||
setOfferStatus(d.status || "");
|
setOfferStatus((d.status as string) || "");
|
||||||
setOrderInfo(d.order || null);
|
setOrderInfo((d.order as OrderInfo) ?? null);
|
||||||
setLockedBy(d.locked_by || null);
|
setLockedBy((d.locked_by as typeof lockedBy) ?? null);
|
||||||
|
|
||||||
// Try to acquire lock if not locked by someone else and not invalidated
|
// Try to acquire lock if not locked by someone else and not invalidated
|
||||||
if (
|
if (
|
||||||
@@ -483,17 +519,28 @@ export default function OfferDetail() {
|
|||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
formInitializedRef.current = true;
|
||||||
alert.error(result.error || "Nepodařilo se načíst nabídku");
|
}, [offerQuery.data, companySettings, hasPermission, id, emptyItem]);
|
||||||
|
|
||||||
|
// Redirect on offer fetch error (edit mode)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEdit && offerQuery.isError) {
|
||||||
|
alert.error("Nepodařilo se načíst nabídku");
|
||||||
navigate("/offers");
|
navigate("/offers");
|
||||||
}
|
}
|
||||||
} catch {
|
}, [isEdit, offerQuery.isError, alert, navigate]);
|
||||||
alert.error("Chyba připojení");
|
|
||||||
navigate("/offers");
|
// Sync next-number data to form (create mode)
|
||||||
} finally {
|
useEffect(() => {
|
||||||
setLoading(false);
|
if (isEdit || !nextNumberData) return;
|
||||||
|
const num =
|
||||||
|
((nextNumberData as Record<string, unknown>).next_number as string) ||
|
||||||
|
((nextNumberData as Record<string, unknown>).number as string) ||
|
||||||
|
"";
|
||||||
|
if (num) {
|
||||||
|
setForm((prev) => ({ ...prev, quotation_number: num }));
|
||||||
}
|
}
|
||||||
}, [id, alert, navigate, hasPermission, companySettings]);
|
}, [isEdit, nextNumberData]);
|
||||||
|
|
||||||
// Heartbeat to keep lock alive + cleanup on unmount
|
// Heartbeat to keep lock alive + cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -518,10 +565,6 @@ export default function OfferDetail() {
|
|||||||
};
|
};
|
||||||
}, [isEdit, id, isLockedByOther, isInvalidated]);
|
}, [isEdit, id, isLockedByOther, isInvalidated]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit) fetchDetail();
|
|
||||||
}, [isEdit, fetchDetail]);
|
|
||||||
|
|
||||||
// Capture initial snapshot after loading completes (create mode)
|
// Capture initial snapshot after loading completes (create mode)
|
||||||
if (!loading && !initialSnapshotRef.current) {
|
if (!loading && !initialSnapshotRef.current) {
|
||||||
initialSnapshotRef.current = JSON.stringify({ form, items, sections });
|
initialSnapshotRef.current = JSON.stringify({ form, items, sections });
|
||||||
@@ -544,36 +587,6 @@ export default function OfferDetail() {
|
|||||||
return () => window.removeEventListener("beforeunload", handler);
|
return () => window.removeEventListener("beforeunload", handler);
|
||||||
}, [isDirty]);
|
}, [isDirty]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadCustomers = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch(`${API_BASE}/customers`);
|
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success)
|
|
||||||
setCustomers(
|
|
||||||
Array.isArray(data.data) ? data.data : data.data?.customers || [],
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* silent */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const loadScopeTemplates = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch(`${API_BASE}/offers-templates`);
|
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success && Array.isArray(data.data)) {
|
|
||||||
setScopeTemplates(data.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* silent */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadCustomers();
|
|
||||||
loadScopeTemplates();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Close dropdown on outside click
|
// Close dropdown on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = () => setShowCustomerDropdown(false);
|
const handleClickOutside = () => setShowCustomerDropdown(false);
|
||||||
@@ -583,48 +596,33 @@ export default function OfferDetail() {
|
|||||||
}
|
}
|
||||||
}, [showCustomerDropdown]);
|
}, [showCustomerDropdown]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit) return;
|
|
||||||
const fetchNextNumber = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch(`${API_BASE}/offers/next-number`);
|
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
quotation_number: data.data?.next_number || data.data?.number || "",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* silent */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchNextNumber();
|
|
||||||
}, [isEdit]);
|
|
||||||
|
|
||||||
// Auto-save draft to localStorage (create mode only)
|
// Auto-save draft to localStorage (create mode only)
|
||||||
const draftPayload = JSON.stringify({ form, items, sections });
|
const draftPayload = JSON.stringify({ form, items, sections });
|
||||||
const debouncedDraft = useDebounce(draftPayload, 1500);
|
const debouncedDraft = useDebounce(draftPayload, 1500);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit) return;
|
if (isEdit) return;
|
||||||
try {
|
try {
|
||||||
|
const data = JSON.parse(debouncedDraft);
|
||||||
const draft = {
|
const draft = {
|
||||||
form: {
|
form: {
|
||||||
project_code: form.project_code,
|
project_code: data.form.project_code ?? "",
|
||||||
customer_id: form.customer_id,
|
customer_id: data.form.customer_id ?? null,
|
||||||
customer_name: form.customer_name,
|
customer_name: data.form.customer_name ?? "",
|
||||||
created_at: form.created_at,
|
created_at: data.form.created_at ?? "",
|
||||||
valid_until: form.valid_until,
|
valid_until: data.form.valid_until ?? "",
|
||||||
currency: form.currency,
|
currency: data.form.currency ?? "CZK",
|
||||||
|
language: data.form.language ?? "EN",
|
||||||
|
vat_rate: data.form.vat_rate ?? 21,
|
||||||
|
apply_vat: data.form.apply_vat ?? false,
|
||||||
|
exchange_rate: data.form.exchange_rate ?? "",
|
||||||
},
|
},
|
||||||
items,
|
items: data.items,
|
||||||
sections,
|
sections: data.sections,
|
||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* localStorage full or unavailable */
|
console.error("Failed to save offer draft:", e);
|
||||||
}
|
}
|
||||||
}, [debouncedDraft]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [debouncedDraft]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
@@ -679,6 +677,7 @@ export default function OfferDetail() {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
if (!form.customer_id) newErrors.customer_id = "Zákazník je povinný";
|
||||||
if (!form.created_at) newErrors.created_at = "Datum je povinné";
|
if (!form.created_at) newErrors.created_at = "Datum je povinné";
|
||||||
if (!form.valid_until) newErrors.valid_until = "Platnost je povinná";
|
if (!form.valid_until) newErrors.valid_until = "Platnost je povinná";
|
||||||
if (items.length === 0) newErrors.items = "Přidejte alespoň jednu položku";
|
if (items.length === 0) newErrors.items = "Přidejte alespoň jednu položku";
|
||||||
@@ -715,8 +714,8 @@ export default function OfferDetail() {
|
|||||||
if (!isEdit) {
|
if (!isEdit) {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(DRAFT_KEY);
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* ignore */
|
console.error("Failed to remove offer draft:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isEdit && result.data?.id) {
|
if (!isEdit && result.data?.id) {
|
||||||
@@ -728,6 +727,9 @@ export default function OfferDetail() {
|
|||||||
items,
|
items,
|
||||||
sections,
|
sections,
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit nabídku");
|
alert.error(result.error || "Nepodařilo se uložit nabídku");
|
||||||
@@ -770,6 +772,9 @@ export default function OfferDetail() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowOrderModal(false);
|
setShowOrderModal(false);
|
||||||
alert.success(result.message || "Objednávka byla vytvořena");
|
alert.success(result.message || "Objednávka byla vytvořena");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
navigate(`/orders/${result.data.order_id}`);
|
navigate(`/orders/${result.data.order_id}`);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
|
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
|
||||||
@@ -792,6 +797,9 @@ export default function OfferDetail() {
|
|||||||
setInvalidateConfirm(false);
|
setInvalidateConfirm(false);
|
||||||
setOfferStatus("invalidated");
|
setOfferStatus("invalidated");
|
||||||
alert.success(result.message || "Nabídka byla zneplatněna");
|
alert.success(result.message || "Nabídka byla zneplatněna");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
|
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
|
||||||
}
|
}
|
||||||
@@ -811,6 +819,9 @@ export default function OfferDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Nabídka byla smazána");
|
alert.success(result.message || "Nabídka byla smazána");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
navigate("/offers");
|
navigate("/offers");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
||||||
@@ -858,49 +869,12 @@ export default function OfferDetail() {
|
|||||||
const requiredPerm = getRequiredPerm();
|
const requiredPerm = getRequiredPerm();
|
||||||
if (!hasPermission(requiredPerm)) return <Forbidden />;
|
if (!hasPermission(requiredPerm)) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="offer-detail"
|
||||||
className="admin-skeleton-row"
|
loading={loading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<OfferDetailFixture />}
|
||||||
>
|
>
|
||||||
<div className="flex-row-gap">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-row gap-2">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -1098,7 +1072,7 @@ export default function OfferDetail() {
|
|||||||
readOnly={isInvalidated || isLockedByOther}
|
readOnly={isInvalidated || isLockedByOther}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Zákazník" error={errors.customer_id}>
|
<FormField label="Zákazník" error={errors.customer_id} required>
|
||||||
{form.customer_id ? (
|
{form.customer_id ? (
|
||||||
<div className="admin-customer-selected">
|
<div className="admin-customer-selected">
|
||||||
<span>{form.customer_name}</span>
|
<span>{form.customer_name}</span>
|
||||||
@@ -1189,7 +1163,11 @@ export default function OfferDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Platnost do" error={errors.valid_until} required>
|
<FormField
|
||||||
|
label="Platnost do"
|
||||||
|
error={errors.valid_until}
|
||||||
|
required
|
||||||
|
>
|
||||||
{isInvalidated || isLockedByOther ? (
|
{isInvalidated || isLockedByOther ? (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -1203,7 +1181,10 @@ export default function OfferDetail() {
|
|||||||
value={form.valid_until}
|
value={form.valid_until}
|
||||||
onChange={(val: string) => {
|
onChange={(val: string) => {
|
||||||
updateForm("valid_until", val);
|
updateForm("valid_until", val);
|
||||||
setErrors((prev) => ({ ...prev, valid_until: undefined }));
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
valid_until: undefined,
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1257,7 +1238,9 @@ export default function OfferDetail() {
|
|||||||
disabled={isInvalidated || isLockedByOther}
|
disabled={isInvalidated || isLockedByOther}
|
||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
|
companySettings?.available_vat_rates || [
|
||||||
|
0, 10, 12, 15, 21,
|
||||||
|
]
|
||||||
).map((r) => (
|
).map((r) => (
|
||||||
<option key={r} value={r}>
|
<option key={r} value={r}>
|
||||||
{r}%
|
{r}%
|
||||||
@@ -1268,7 +1251,9 @@ export default function OfferDetail() {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={form.apply_vat}
|
checked={form.apply_vat}
|
||||||
onChange={(e) => updateForm("apply_vat", e.target.checked)}
|
onChange={(e) =>
|
||||||
|
updateForm("apply_vat", e.target.checked)
|
||||||
|
}
|
||||||
disabled={isInvalidated || isLockedByOther}
|
disabled={isInvalidated || isLockedByOther}
|
||||||
/>
|
/>
|
||||||
<span>Účtovat DPH</span>
|
<span>Účtovat DPH</span>
|
||||||
@@ -1315,7 +1300,7 @@ export default function OfferDetail() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="admin-table-responsive">
|
<div className="offers-items-table">
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={dndSensors}
|
sensors={dndSensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
@@ -1348,18 +1333,42 @@ export default function OfferDetail() {
|
|||||||
<th style={{ width: "2.5rem", textAlign: "center" }}>
|
<th style={{ width: "2.5rem", textAlign: "center" }}>
|
||||||
#
|
#
|
||||||
</th>
|
</th>
|
||||||
<th>Popis</th>
|
<th className="offers-col-desc">Popis</th>
|
||||||
<th style={{ width: "5rem" }}>Množství</th>
|
<th
|
||||||
<th style={{ width: "5rem" }}>Jednotka</th>
|
className="offers-col-qty"
|
||||||
<th style={{ width: "7rem" }}>Cena/ks</th>
|
style={{ width: "5rem" }}
|
||||||
<th style={{ width: "4rem", textAlign: "center" }}>
|
>
|
||||||
|
Množství
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="offers-col-unit"
|
||||||
|
style={{ width: "5rem" }}
|
||||||
|
>
|
||||||
|
Jednotka
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="offers-col-price"
|
||||||
|
style={{ width: "7rem" }}
|
||||||
|
>
|
||||||
|
Cena/ks
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="offers-col-included"
|
||||||
|
style={{ width: "4rem", textAlign: "center" }}
|
||||||
|
>
|
||||||
V ceně
|
V ceně
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: "7rem", textAlign: "right" }}>
|
<th
|
||||||
|
className="offers-col-total"
|
||||||
|
style={{ width: "7rem", textAlign: "right" }}
|
||||||
|
>
|
||||||
Celkem
|
Celkem
|
||||||
</th>
|
</th>
|
||||||
{!isInvalidated && !isLockedByOther && (
|
{!isInvalidated && !isLockedByOther && (
|
||||||
<th style={{ width: "3rem" }} />
|
<th
|
||||||
|
className="offers-col-del"
|
||||||
|
style={{ width: "3rem" }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1416,7 +1425,11 @@ export default function OfferDetail() {
|
|||||||
<h3 className="admin-card-title">Rozsah projektu</h3>
|
<h3 className="admin-card-title">Rozsah projektu</h3>
|
||||||
{!isInvalidated && !isLockedByOther && (
|
{!isInvalidated && !isLockedByOther && (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{scopeTemplates.length > 0 && (
|
{scopeTemplates.length > 0 && (
|
||||||
<select
|
<select
|
||||||
@@ -1514,7 +1527,9 @@ export default function OfferDetail() {
|
|||||||
{(form.language === "CZ"
|
{(form.language === "CZ"
|
||||||
? section.title_cz
|
? section.title_cz
|
||||||
: section.title) && (
|
: section.title) && (
|
||||||
<span style={{ fontWeight: 400, marginLeft: "0.5rem" }}>
|
<span
|
||||||
|
style={{ fontWeight: 400, marginLeft: "0.5rem" }}
|
||||||
|
>
|
||||||
—{" "}
|
—{" "}
|
||||||
{form.language === "CZ"
|
{form.language === "CZ"
|
||||||
? section.title_cz || section.title
|
? section.title_cz || section.title
|
||||||
@@ -1825,5 +1840,6 @@ export default function OfferDetail() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OffersFixture from "../fixtures/OffersFixture";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import { offerListOptions } from "../lib/queries/offers";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
@@ -95,19 +99,15 @@ export default function Offers() {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: quotations,
|
items: quotations,
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
refetch: fetchData,
|
isPending,
|
||||||
} = useListData("offers", {
|
isFetching,
|
||||||
search,
|
} = usePaginatedQuery<Quotation>(
|
||||||
sort,
|
offerListOptions({ search, sort, order, page }),
|
||||||
order,
|
);
|
||||||
page,
|
|
||||||
errorMsg: "Nepodařilo se načíst nabídky",
|
|
||||||
});
|
|
||||||
|
|
||||||
const discardDraft = () => {
|
const discardDraft = () => {
|
||||||
try {
|
try {
|
||||||
@@ -139,7 +139,7 @@ export default function Offers() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Nabídka byla duplikována");
|
alert.success(result.message || "Nabídka byla duplikována");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se duplikovat nabídku");
|
alert.error(result.error || "Nepodařilo se duplikovat nabídku");
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,9 @@ export default function Offers() {
|
|||||||
setOrderModal({ show: false, quotation: null });
|
setOrderModal({ show: false, quotation: null });
|
||||||
alert.success(result.message || "Objednávka byla vytvořena");
|
alert.success(result.message || "Objednávka byla vytvořena");
|
||||||
navigate(`/orders/${result.data.order_id}`);
|
navigate(`/orders/${result.data.order_id}`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
|
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
|
||||||
}
|
}
|
||||||
@@ -193,7 +196,9 @@ export default function Offers() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, quotation: null });
|
setDeleteConfirm({ show: false, quotation: null });
|
||||||
alert.success(result.message || "Nabídka byla smazána");
|
alert.success(result.message || "Nabídka byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
||||||
}
|
}
|
||||||
@@ -218,7 +223,9 @@ export default function Offers() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setInvalidateConfirm({ show: false, quotation: null });
|
setInvalidateConfirm({ show: false, quotation: null });
|
||||||
alert.success(result.message || "Nabídka byla zneplatněna");
|
alert.success(result.message || "Nabídka byla zneplatněna");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
|
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
|
||||||
}
|
}
|
||||||
@@ -260,64 +267,8 @@ export default function Offers() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (initialLoad) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
borderRadius: "8px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton name="offers" loading={isPending} fixture={<OffersFixture />}>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -381,7 +332,7 @@ export default function Offers() {
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
style={{ opacity: loading ? 0.6 : 1, transition: "opacity 0.2s" }}
|
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<div className="admin-search-bar mb-4">
|
<div className="admin-search-bar mb-4">
|
||||||
@@ -418,7 +369,10 @@ export default function Offers() {
|
|||||||
</div>
|
</div>
|
||||||
<p>Zatím nejsou žádné nabídky.</p>
|
<p>Zatím nejsou žádné nabídky.</p>
|
||||||
{hasPermission("offers.create") && (
|
{hasPermission("offers.create") && (
|
||||||
<Link to="/offers/new" className="admin-btn admin-btn-primary">
|
<Link
|
||||||
|
to="/offers/new"
|
||||||
|
className="admin-btn admin-btn-primary"
|
||||||
|
>
|
||||||
Vytvořit první nabídku
|
Vytvořit první nabídku
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -578,7 +532,10 @@ export default function Offers() {
|
|||||||
className={getRowClass(isInvalidated, !!isExpired)}
|
className={getRowClass(isInvalidated, !!isExpired)}
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<Link to={`/offers/${q.id}`} className="link-accent">
|
<Link
|
||||||
|
to={`/offers/${q.id}`}
|
||||||
|
className="link-accent"
|
||||||
|
>
|
||||||
{q.quotation_number}
|
{q.quotation_number}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -699,7 +656,10 @@ export default function Offers() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCustomerOrderNumber("");
|
setCustomerOrderNumber("");
|
||||||
setOrderAttachment(null);
|
setOrderAttachment(null);
|
||||||
setOrderModal({ show: true, quotation: q });
|
setOrderModal({
|
||||||
|
show: true,
|
||||||
|
quotation: q,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="admin-btn-icon"
|
className="admin-btn-icon"
|
||||||
title="Vytvořit objednávku"
|
title="Vytvořit objednávku"
|
||||||
@@ -799,7 +759,10 @@ export default function Offers() {
|
|||||||
{hasPermission("offers.delete") && (
|
{hasPermission("offers.delete") && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setDeleteConfirm({ show: true, quotation: q })
|
setDeleteConfirm({
|
||||||
|
show: true,
|
||||||
|
quotation: q,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="admin-btn-icon danger"
|
className="admin-btn-icon danger"
|
||||||
title="Smazat"
|
title="Smazat"
|
||||||
@@ -1017,5 +980,6 @@ export default function Offers() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -6,6 +7,9 @@ import ConfirmModal from "../components/ConfirmModal";
|
|||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
|
import { offerCustomersOptions } from "../lib/queries/offers";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OffersCustomersFixture from "../fixtures/OffersCustomersFixture";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
|
||||||
@@ -61,8 +65,10 @@ interface CustomerForm {
|
|||||||
export default function OffersCustomers() {
|
export default function OffersCustomers() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
const { data: customers = [], isPending } = useQuery(
|
||||||
|
offerCustomersOptions(),
|
||||||
|
) as { data: Customer[]; isPending: boolean };
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
@@ -135,26 +141,7 @@ export default function OffersCustomers() {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// Data fetching moved to useQuery(offerCustomersOptions()) above
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/customers`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setCustomers(Array.isArray(result.data) ? result.data : []);
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se načíst zákazníky");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
setEditingCustomer(null);
|
setEditingCustomer(null);
|
||||||
@@ -244,7 +231,7 @@ export default function OffersCustomers() {
|
|||||||
? "Zákazník byl aktualizován"
|
? "Zákazník byl aktualizován"
|
||||||
: "Zákazník byl vytvořen"),
|
: "Zákazník byl vytvořen"),
|
||||||
);
|
);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-customers"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit zákazníka");
|
alert.error(result.error || "Nepodařilo se uložit zákazníka");
|
||||||
}
|
}
|
||||||
@@ -272,7 +259,7 @@ export default function OffersCustomers() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, customer: null });
|
setDeleteConfirm({ show: false, customer: null });
|
||||||
alert.success(result.message || "Zákazník byl smazán");
|
alert.success(result.message || "Zákazník byl smazán");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-customers"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat zákazníka");
|
alert.error(result.error || "Nepodařilo se smazat zákazníka");
|
||||||
}
|
}
|
||||||
@@ -294,60 +281,14 @@ export default function OffersCustomers() {
|
|||||||
)
|
)
|
||||||
: customers;
|
: customers;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "160px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
borderRadius: "8px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullFieldOrder = getFullFieldOrder();
|
const fullFieldOrder = getFullFieldOrder();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="offers-customers"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<OffersCustomersFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -562,7 +503,10 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.street}
|
value={form.street}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({ ...prev, street: e.target.value }))
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
street: e.target.value,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -573,7 +517,10 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.city}
|
value={form.city}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({ ...prev, city: e.target.value }))
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
city: e.target.value,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -701,7 +648,9 @@ export default function OffersCustomers() {
|
|||||||
.filter((k) => k !== key)
|
.filter((k) => k !== key)
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
if (k.startsWith("custom_")) {
|
if (k.startsWith("custom_")) {
|
||||||
const ki = parseInt(k.split("_")[1]);
|
const ki = parseInt(
|
||||||
|
k.split("_")[1],
|
||||||
|
);
|
||||||
if (ki > idx)
|
if (ki > idx)
|
||||||
return `custom_${ki - 1}`;
|
return `custom_${ki - 1}`;
|
||||||
}
|
}
|
||||||
@@ -894,5 +843,6 @@ export default function OffersCustomers() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import {
|
import { useState, useRef, type ReactNode } from "react";
|
||||||
useState,
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -13,6 +8,9 @@ import FormField from "../components/FormField";
|
|||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import RichEditor from "../components/RichEditor";
|
import RichEditor from "../components/RichEditor";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
|
import { offerTemplatesOptions } from "../lib/queries/offers";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OffersTemplatesFixture from "../fixtures/OffersTemplatesFixture";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
|
||||||
@@ -73,7 +71,7 @@ export default function OffersTemplates() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="admin-tabs">
|
<div className="admin-tabs mb-4">
|
||||||
<button
|
<button
|
||||||
className={`admin-tab ${activeTab === "items" ? "active" : ""}`}
|
className={`admin-tab ${activeTab === "items" ? "active" : ""}`}
|
||||||
onClick={() => setActiveTab("items")}
|
onClick={() => setActiveTab("items")}
|
||||||
@@ -97,8 +95,10 @@ export default function OffersTemplates() {
|
|||||||
|
|
||||||
function ItemTemplatesTab() {
|
function ItemTemplatesTab() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [templates, setTemplates] = useState<ItemTemplate[]>([]);
|
const { data: templates = [], isPending } = useQuery(
|
||||||
|
offerTemplatesOptions("items"),
|
||||||
|
) as { data: ItemTemplate[]; isPending: boolean };
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingTemplate, setEditingTemplate] = useState<ItemTemplate | null>(
|
const [editingTemplate, setEditingTemplate] = useState<ItemTemplate | null>(
|
||||||
null,
|
null,
|
||||||
@@ -118,27 +118,6 @@ function ItemTemplatesTab() {
|
|||||||
|
|
||||||
useModalLock(showModal);
|
useModalLock(showModal);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/offers-templates?action=items`,
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setTemplates(Array.isArray(result.data) ? result.data : []);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst šablony");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
setForm({ name: "", description: "", default_price: 0, category: "" });
|
setForm({ name: "", description: "", default_price: 0, category: "" });
|
||||||
@@ -177,7 +156,7 @@ function ItemTemplatesTab() {
|
|||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
}
|
}
|
||||||
@@ -200,7 +179,7 @@ function ItemTemplatesTab() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, template: null });
|
setDeleteConfirm({ show: false, template: null });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
}
|
}
|
||||||
@@ -211,32 +190,12 @@ function ItemTemplatesTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="offers-templates"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<OffersTemplatesFixture />}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
@@ -368,7 +327,9 @@ function ItemTemplatesTab() {
|
|||||||
>
|
>
|
||||||
<div className="admin-modal-header">
|
<div className="admin-modal-header">
|
||||||
<h2 className="admin-modal-title">
|
<h2 className="admin-modal-title">
|
||||||
{editingTemplate ? "Upravit šablonu" : "Nová šablona položky"}
|
{editingTemplate
|
||||||
|
? "Upravit šablonu"
|
||||||
|
: "Nová šablona položky"}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-modal-body">
|
<div className="admin-modal-body">
|
||||||
@@ -387,7 +348,10 @@ function ItemTemplatesTab() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((p) => ({ ...p, description: e.target.value }))
|
setForm((p) => ({
|
||||||
|
...p,
|
||||||
|
description: e.target.value,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
rows={2}
|
rows={2}
|
||||||
@@ -401,7 +365,7 @@ function ItemTemplatesTab() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((p) => ({
|
setForm((p) => ({
|
||||||
...p,
|
...p,
|
||||||
default_price: parseFloat(e.target.value) || 0,
|
default_price: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
@@ -462,6 +426,7 @@ function ItemTemplatesTab() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +434,10 @@ function ItemTemplatesTab() {
|
|||||||
|
|
||||||
function ScopeTemplatesTab() {
|
function ScopeTemplatesTab() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [templates, setTemplates] = useState<ScopeTemplate[]>([]);
|
const { data: templates = [], isPending } = useQuery(
|
||||||
|
offerTemplatesOptions(),
|
||||||
|
) as { data: ScopeTemplate[]; isPending: boolean };
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingTemplate, setEditingTemplate] = useState<ScopeTemplate | null>(
|
const [editingTemplate, setEditingTemplate] = useState<ScopeTemplate | null>(
|
||||||
null,
|
null,
|
||||||
@@ -486,25 +453,6 @@ function ScopeTemplatesTab() {
|
|||||||
|
|
||||||
useModalLock(showModal);
|
useModalLock(showModal);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/offers-templates`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setTemplates(Array.isArray(result.data) ? result.data : []);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst šablony");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
setForm({
|
setForm({
|
||||||
@@ -625,7 +573,7 @@ function ScopeTemplatesTab() {
|
|||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
}
|
}
|
||||||
@@ -648,7 +596,7 @@ function ScopeTemplatesTab() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, template: null });
|
setDeleteConfirm({ show: false, template: null });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
}
|
}
|
||||||
@@ -659,32 +607,12 @@ function ScopeTemplatesTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="offers-templates"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<OffersTemplatesFixture />}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
@@ -828,7 +756,10 @@ function ScopeTemplatesTab() {
|
|||||||
<label className="admin-form-label mb-2">Sekce</label>
|
<label className="admin-form-label mb-2">Sekce</label>
|
||||||
<div className="admin-scope-list">
|
<div className="admin-scope-list">
|
||||||
{form.sections.map((section, index) => (
|
{form.sections.map((section, index) => (
|
||||||
<div key={section._key} className="admin-scope-section">
|
<div
|
||||||
|
key={section._key}
|
||||||
|
className="admin-scope-section"
|
||||||
|
>
|
||||||
<div className="admin-scope-section-header">
|
<div className="admin-scope-section-header">
|
||||||
<span className="admin-scope-number">
|
<span className="admin-scope-number">
|
||||||
{index + 1}.
|
{index + 1}.
|
||||||
@@ -1018,5 +949,6 @@ function ScopeTemplatesTab() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import {
|
import { useState, useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { orderDetailOptions } from "../lib/queries/orders";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
@@ -15,6 +10,8 @@ import ConfirmModal from "../components/ConfirmModal";
|
|||||||
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OrderDetailFixture from "../fixtures/OrderDetailFixture";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency, formatDate } from "../utils/formatters";
|
import { formatCurrency, formatDate } from "../utils/formatters";
|
||||||
@@ -105,8 +102,11 @@ export default function OrderDetail() {
|
|||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [order, setOrder] = useState<OrderData | null>(null);
|
const orderQuery = useQuery(orderDetailOptions(id));
|
||||||
|
const order = orderQuery.data as OrderData | undefined;
|
||||||
|
const loading = orderQuery.isPending;
|
||||||
|
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [statusChanging, setStatusChanging] = useState<string | null>(null);
|
const [statusChanging, setStatusChanging] = useState<string | null>(null);
|
||||||
@@ -121,39 +121,38 @@ export default function OrderDetail() {
|
|||||||
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
|
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
|
||||||
const [confirmationLoading, setConfirmationLoading] = useState(false);
|
const [confirmationLoading, setConfirmationLoading] = useState(false);
|
||||||
const initialNotesRef = useRef<string | null>(null);
|
const initialNotesRef = useRef<string | null>(null);
|
||||||
|
const formInitializedRef = useRef(false);
|
||||||
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
|
|
||||||
|
// Reset form sync when navigating to a different order
|
||||||
|
useEffect(() => {
|
||||||
|
formInitializedRef.current = false;
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// Sync order data to local form state on first load
|
||||||
|
useEffect(() => {
|
||||||
|
if (orderQuery.data && !formInitializedRef.current) {
|
||||||
|
const orderData = orderQuery.data as OrderData;
|
||||||
|
setNotes(orderData.notes || "");
|
||||||
|
initialNotesRef.current = orderData.notes || "";
|
||||||
|
formInitializedRef.current = true;
|
||||||
|
}
|
||||||
|
}, [orderQuery.data]);
|
||||||
|
|
||||||
|
// Navigate away on fetch error
|
||||||
|
useEffect(() => {
|
||||||
|
if (orderQuery.error) {
|
||||||
|
alert.error("Nepodařilo se načíst objednávku");
|
||||||
|
navigate("/orders");
|
||||||
|
}
|
||||||
|
}, [orderQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
blobTimeoutsRef.current.forEach(clearTimeout);
|
blobTimeoutsRef.current.forEach(clearTimeout);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDetail = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/orders/${id}`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setOrder(result.data);
|
|
||||||
setNotes(result.data.notes || "");
|
|
||||||
initialNotesRef.current = result.data.notes || "";
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se načíst objednávku");
|
|
||||||
navigate("/orders");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
navigate("/orders");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [id, alert, navigate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDetail();
|
|
||||||
}, [fetchDetail]);
|
|
||||||
|
|
||||||
const isDirty = useMemo(() => {
|
const isDirty = useMemo(() => {
|
||||||
if (!initialNotesRef.current) return false;
|
if (!initialNotesRef.current) return false;
|
||||||
return notes !== initialNotesRef.current;
|
return notes !== initialNotesRef.current;
|
||||||
@@ -200,7 +199,9 @@ export default function OrderDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Stav byl změněn");
|
alert.success(result.message || "Stav byl změněn");
|
||||||
fetchDetail();
|
queryClient.invalidateQueries({ queryKey: ["orders", id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
alert.error(result.error || "Nepodařilo se změnit stav");
|
||||||
}
|
}
|
||||||
@@ -223,6 +224,9 @@ export default function OrderDetail() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success("Poznámky byly uloženy");
|
alert.success("Poznámky byly uloženy");
|
||||||
initialNotesRef.current = notes;
|
initialNotesRef.current = notes;
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders", id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit poznámky");
|
alert.error(result.error || "Nepodařilo se uložit poznámky");
|
||||||
}
|
}
|
||||||
@@ -311,6 +315,9 @@ export default function OrderDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Objednávka byla smazána");
|
alert.success(result.message || "Objednávka byla smazána");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
navigate("/orders");
|
navigate("/orders");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
||||||
@@ -323,51 +330,14 @@ export default function OrderDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div className="flex-row-gap">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-row gap-2">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!order) return null;
|
if (!order) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="order-detail"
|
||||||
|
loading={loading}
|
||||||
|
fixture={<OrderDetailFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -465,8 +435,8 @@ export default function OrderDetail() {
|
|||||||
Potvrzení objednávky
|
Potvrzení objednávky
|
||||||
</button>
|
</button>
|
||||||
{hasPermission("orders.edit") &&
|
{hasPermission("orders.edit") &&
|
||||||
order.valid_transitions?.filter((s) => s !== "stornovana").length! >
|
order.valid_transitions?.filter((s) => s !== "stornovana")
|
||||||
0 &&
|
.length! > 0 &&
|
||||||
order
|
order
|
||||||
.valid_transitions!.filter((s) => s !== "stornovana")
|
.valid_transitions!.filter((s) => s !== "stornovana")
|
||||||
.map((status) => (
|
.map((status) => (
|
||||||
@@ -608,7 +578,9 @@ export default function OrderDetail() {
|
|||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: "2.5rem", textAlign: "center" }}>#</th>
|
<th style={{ width: "2.5rem", textAlign: "center" }}>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
<th>Popis</th>
|
<th>Popis</th>
|
||||||
<th style={{ width: "5.5rem", textAlign: "center" }}>
|
<th style={{ width: "5.5rem", textAlign: "center" }}>
|
||||||
Množství
|
Množství
|
||||||
@@ -671,7 +643,9 @@ export default function OrderDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ textAlign: "center" }}>{item.quantity}</td>
|
<td style={{ textAlign: "center" }}>
|
||||||
|
{item.quantity}
|
||||||
|
</td>
|
||||||
<td style={{ textAlign: "center" }}>
|
<td style={{ textAlign: "center" }}>
|
||||||
{item.unit || "—"}
|
{item.unit || "—"}
|
||||||
</td>
|
</td>
|
||||||
@@ -713,7 +687,9 @@ export default function OrderDetail() {
|
|||||||
{Number(order.apply_vat) > 0 && (
|
{Number(order.apply_vat) > 0 && (
|
||||||
<div className="admin-totals-row">
|
<div className="admin-totals-row">
|
||||||
<span>DPH ({order.vat_rate}%):</span>
|
<span>DPH ({order.vat_rate}%):</span>
|
||||||
<span>{formatCurrency(totals.vatAmount, order.currency)}</span>
|
<span>
|
||||||
|
{formatCurrency(totals.vatAmount, order.currency)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="admin-totals-row admin-totals-total">
|
<div className="admin-totals-row admin-totals-total">
|
||||||
@@ -741,7 +717,10 @@ export default function OrderDetail() {
|
|||||||
)}
|
)}
|
||||||
{order.scope_description && (
|
{order.scope_description && (
|
||||||
<div
|
<div
|
||||||
style={{ color: "var(--text-secondary)", marginBottom: "1rem" }}
|
style={{
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{order.scope_description}
|
{order.scope_description}
|
||||||
</div>
|
</div>
|
||||||
@@ -879,5 +858,6 @@ export default function OrderDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useAlert } from "../context/AlertContext";
|
|||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OrdersFixture from "../fixtures/OrdersFixture";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
|
||||||
@@ -10,7 +12,9 @@ import apiFetch from "../utils/api";
|
|||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import { orderListOptions } from "../lib/queries/orders";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -57,19 +61,13 @@ export default function Orders() {
|
|||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: orders,
|
items: orders,
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
refetch: fetchData,
|
isPending,
|
||||||
} = useListData("orders", {
|
isFetching,
|
||||||
search,
|
} = usePaginatedQuery<Order>(orderListOptions({ search, sort, order, page }));
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
page,
|
|
||||||
errorMsg: "Nepodařilo se načíst objednávky",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasPermission("orders.view")) return <Forbidden />;
|
if (!hasPermission("orders.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -90,7 +88,9 @@ export default function Orders() {
|
|||||||
setDeleteConfirm({ show: false, order: null });
|
setDeleteConfirm({ show: false, order: null });
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
alert.success(result.message || "Objednávka byla smazána");
|
alert.success(result.message || "Objednávka byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
||||||
}
|
}
|
||||||
@@ -101,52 +101,8 @@ export default function Orders() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (initialLoad) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton name="orders" loading={isPending} fixture={<OrdersFixture />}>
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -173,7 +129,7 @@ export default function Orders() {
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
style={{ opacity: loading ? 0.6 : 1, transition: "opacity 0.2s" }}
|
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<div className="admin-search-bar mb-4">
|
<div className="admin-search-bar mb-4">
|
||||||
@@ -281,7 +237,9 @@ export default function Orders() {
|
|||||||
{STATUS_LABELS[o.status] || o.status}
|
{STATUS_LABELS[o.status] || o.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">{formatDate(o.created_at)}</td>
|
<td className="admin-mono">
|
||||||
|
{formatDate(o.created_at)}
|
||||||
|
</td>
|
||||||
<td className="admin-mono text-right fw-500">
|
<td className="admin-mono text-right fw-500">
|
||||||
{formatCurrency(o.total, o.currency)}
|
{formatCurrency(o.total, o.currency)}
|
||||||
</td>
|
</td>
|
||||||
@@ -403,8 +361,8 @@ export default function Orders() {
|
|||||||
message={
|
message={
|
||||||
<>
|
<>
|
||||||
Opravdu chcete smazat objednávku "
|
Opravdu chcete smazat objednávku "
|
||||||
{deleteConfirm.order?.order_number}"? Bude smazán i přidružený
|
{deleteConfirm.order?.order_number}"? Bude smazán i
|
||||||
projekt. Tato akce je nevratná.
|
přidružený projekt. Tato akce je nevratná.
|
||||||
<label
|
<label
|
||||||
className="admin-form-checkbox"
|
className="admin-form-checkbox"
|
||||||
style={{ marginTop: "1rem", display: "flex" }}
|
style={{ marginTop: "1rem", display: "flex" }}
|
||||||
@@ -424,5 +382,6 @@ export default function Orders() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,381 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import { useNavigate, Link } from "react-router-dom";
|
|
||||||
import { useAlert } from "../context/AlertContext";
|
|
||||||
import { useAuth } from "../context/AuthContext";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import FormField from "../components/FormField";
|
|
||||||
import Forbidden from "../components/Forbidden";
|
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
|
||||||
|
|
||||||
interface Customer {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
company_id?: string;
|
|
||||||
city?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectForm {
|
|
||||||
project_number: string;
|
|
||||||
name: string;
|
|
||||||
customer_id: number | null;
|
|
||||||
customer_name: string;
|
|
||||||
start_date: string;
|
|
||||||
responsible_user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectCreate() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const alert = useAlert();
|
|
||||||
const { hasPermission } = useAuth();
|
|
||||||
|
|
||||||
const [form, setForm] = useState<ProjectForm>({
|
|
||||||
project_number: "",
|
|
||||||
name: "",
|
|
||||||
customer_id: null,
|
|
||||||
customer_name: "",
|
|
||||||
start_date: new Date().toISOString().split("T")[0],
|
|
||||||
responsible_user_id: "",
|
|
||||||
});
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
|
||||||
const [loadingNumber, setLoadingNumber] = useState(true);
|
|
||||||
|
|
||||||
// Customer selector state
|
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
||||||
const [customerSearch, setCustomerSearch] = useState("");
|
|
||||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
useEffect(() => {
|
|
||||||
const load = async () => {
|
|
||||||
try {
|
|
||||||
const [numRes, custRes, usersRes] = await Promise.all([
|
|
||||||
apiFetch(`${API_BASE}/projects/next-number`),
|
|
||||||
apiFetch(`${API_BASE}/customers`),
|
|
||||||
apiFetch(`${API_BASE}/users`),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const numData = await numRes.json();
|
|
||||||
if (numData.success) {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
project_number:
|
|
||||||
numData.data?.next_number || numData.data?.number || "",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const custData = await custRes.json();
|
|
||||||
if (custData.success) {
|
|
||||||
setCustomers(
|
|
||||||
Array.isArray(custData.data)
|
|
||||||
? custData.data
|
|
||||||
: custData.data?.items || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersData = await usersRes.json();
|
|
||||||
if (usersData.success) {
|
|
||||||
const rawUsers = Array.isArray(usersData.data)
|
|
||||||
? usersData.data
|
|
||||||
: usersData.data?.items || [];
|
|
||||||
setUsers(
|
|
||||||
rawUsers.map((u: any) => ({
|
|
||||||
id: u.id,
|
|
||||||
name:
|
|
||||||
`${u.first_name || ""} ${u.last_name || ""}`.trim() ||
|
|
||||||
u.username,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba při načítání dat");
|
|
||||||
} finally {
|
|
||||||
setLoadingNumber(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
load();
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
// Customer filtering
|
|
||||||
const filteredCustomers = useMemo(() => {
|
|
||||||
if (!customerSearch) return customers;
|
|
||||||
const q = customerSearch.toLowerCase();
|
|
||||||
return customers.filter(
|
|
||||||
(c) =>
|
|
||||||
(c.name || "").toLowerCase().includes(q) ||
|
|
||||||
(c.company_id || "").includes(customerSearch) ||
|
|
||||||
(c.city || "").toLowerCase().includes(q),
|
|
||||||
);
|
|
||||||
}, [customers, customerSearch]);
|
|
||||||
|
|
||||||
// Close dropdown on outside click
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = () => setShowCustomerDropdown(false);
|
|
||||||
if (showCustomerDropdown) {
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("click", handleClickOutside);
|
|
||||||
}
|
|
||||||
}, [showCustomerDropdown]);
|
|
||||||
|
|
||||||
if (!hasPermission("projects.create")) return <Forbidden />;
|
|
||||||
|
|
||||||
const selectCustomer = (customer: Customer) => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
customer_id: customer.id,
|
|
||||||
customer_name: customer.name,
|
|
||||||
}));
|
|
||||||
setErrors((prev) => ({ ...prev, customer_id: undefined }));
|
|
||||||
setCustomerSearch("");
|
|
||||||
setShowCustomerDropdown(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCustomer = () => {
|
|
||||||
setForm((prev) => ({ ...prev, customer_id: null, customer_name: "" }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateForm = (field: keyof ProjectForm, value: unknown) => {
|
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
|
||||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const newErrors: Record<string, string> = {};
|
|
||||||
if (!form.name.trim()) newErrors.name = "Název projektu je povinný";
|
|
||||||
if (!form.customer_id) newErrors.customer_id = "Vyberte zákazníka";
|
|
||||||
setErrors(newErrors);
|
|
||||||
if (Object.keys(newErrors).length > 0) return;
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
const body = {
|
|
||||||
name: form.name.trim(),
|
|
||||||
customer_id: form.customer_id,
|
|
||||||
start_date: form.start_date,
|
|
||||||
responsible_user_id: form.responsible_user_id || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await apiFetch(`${API_BASE}/projects`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert.success("Projekt byl vytvořen");
|
|
||||||
navigate(`/projects/${data.data.id}`);
|
|
||||||
} else {
|
|
||||||
alert.error(data.error || "Nepodařilo se vytvořit projekt");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingNumber) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div className="admin-skeleton-line h-8" style={{ width: "200px" }} />
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<motion.div
|
|
||||||
className="admin-page-header"
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
>
|
|
||||||
<div className="flex-row gap-4">
|
|
||||||
<Link
|
|
||||||
to="/projects"
|
|
||||||
className="admin-btn-icon"
|
|
||||||
title="Zpět"
|
|
||||||
aria-label="Zpět"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nový projekt</h1>
|
|
||||||
<p className="admin-page-subtitle">Ruční vytvoření projektu</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
{saving ? "Ukládám..." : "Uložit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="admin-card"
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
|
||||||
style={{ overflow: "visible" }}
|
|
||||||
>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Základní údaje</h3>
|
|
||||||
<div className="admin-form">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<FormField label="Číslo projektu">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.project_number}
|
|
||||||
readOnly
|
|
||||||
className="admin-form-input"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "var(--bg-secondary)",
|
|
||||||
cursor: "default",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Název" error={errors.name} required>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.name}
|
|
||||||
onChange={(e) => updateForm("name", e.target.value)}
|
|
||||||
className="admin-form-input"
|
|
||||||
placeholder="Název projektu"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<FormField label="Zákazník" error={errors.customer_id} required>
|
|
||||||
{form.customer_id ? (
|
|
||||||
<div className="admin-customer-selected">
|
|
||||||
<span>{form.customer_name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={clearCustomer}
|
|
||||||
className="admin-btn-icon"
|
|
||||||
title="Odebrat zákazníka"
|
|
||||||
aria-label="Odebrat zákazníka"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="admin-customer-select"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customerSearch}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomerSearch(e.target.value);
|
|
||||||
setShowCustomerDropdown(true);
|
|
||||||
}}
|
|
||||||
onFocus={() => setShowCustomerDropdown(true)}
|
|
||||||
className="admin-form-input"
|
|
||||||
placeholder="Hledat zákazníka..."
|
|
||||||
/>
|
|
||||||
{showCustomerDropdown && (
|
|
||||||
<div className="admin-customer-dropdown">
|
|
||||||
{filteredCustomers.length === 0 ? (
|
|
||||||
<div className="admin-customer-dropdown-empty">
|
|
||||||
Žádní zákazníci
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredCustomers.slice(0, 20).map((c) => (
|
|
||||||
<div
|
|
||||||
key={c.id}
|
|
||||||
className="admin-customer-dropdown-item"
|
|
||||||
onMouseDown={() => selectCustomer(c)}
|
|
||||||
>
|
|
||||||
<div>{c.name}</div>
|
|
||||||
{c.city && <div>{c.city}</div>}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Datum zahájení">
|
|
||||||
<AdminDatePicker
|
|
||||||
mode="date"
|
|
||||||
value={form.start_date}
|
|
||||||
onChange={(val: string) => updateForm("start_date", val)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<FormField label="Zodpovědná osoba">
|
|
||||||
<select
|
|
||||||
value={form.responsible_user_id}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateForm("responsible_user_id", e.target.value)
|
|
||||||
}
|
|
||||||
className="admin-form-select"
|
|
||||||
>
|
|
||||||
<option value="">— Nevybráno —</option>
|
|
||||||
{users.map((u) => (
|
|
||||||
<option key={u.id} value={u.id}>
|
|
||||||
{u.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { projectDetailOptions } from "../lib/queries/projects";
|
||||||
|
import { userListOptions } from "../lib/queries/users";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import ProjectDetailFixture from "../fixtures/ProjectDetailFixture";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
@@ -74,9 +79,7 @@ export default function ProjectDetail() {
|
|||||||
const { hasPermission, isAdmin } = useAuth();
|
const { hasPermission, isAdmin } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [project, setProject] = useState<ProjectData | null>(null);
|
|
||||||
const [form, setForm] = useState<ProjectForm>({
|
const [form, setForm] = useState<ProjectForm>({
|
||||||
name: "",
|
name: "",
|
||||||
status: "aktivni",
|
status: "aktivni",
|
||||||
@@ -84,88 +87,62 @@ export default function ProjectDetail() {
|
|||||||
end_date: "",
|
end_date: "",
|
||||||
responsible_user_id: "",
|
responsible_user_id: "",
|
||||||
});
|
});
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
// Dynamic notes
|
// Dynamic notes
|
||||||
const [notes, setNotes] = useState<Note[]>([]);
|
|
||||||
const [notesLoading, setNotesLoading] = useState(true);
|
|
||||||
const [newNote, setNewNote] = useState("");
|
const [newNote, setNewNote] = useState("");
|
||||||
const [addingNote, setAddingNote] = useState(false);
|
const [addingNote, setAddingNote] = useState(false);
|
||||||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||||||
|
|
||||||
const fetchNotes = async () => {
|
const queryClient = useQueryClient();
|
||||||
try {
|
const projectQuery = useQuery(projectDetailOptions(id));
|
||||||
const response = await apiFetch(`${API_BASE}/projects/${id}`);
|
const project = projectQuery.data as
|
||||||
if (response.status === 401) return;
|
| (ProjectData & { project_notes?: Note[] })
|
||||||
const result = await response.json();
|
| undefined;
|
||||||
if (result.success) {
|
const isPending = projectQuery.isPending;
|
||||||
setNotes(result.data.project_notes || []);
|
const notes: Note[] = project?.project_notes || [];
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent - notes are supplementary
|
|
||||||
} finally {
|
|
||||||
setNotesLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: usersData } = useQuery(userListOptions());
|
||||||
const fetchDetail = async () => {
|
const rawUsers = ((usersData as Record<string, unknown>)?.items ??
|
||||||
try {
|
usersData ??
|
||||||
const response = await apiFetch(`${API_BASE}/projects/${id}`);
|
[]) as any[];
|
||||||
if (response.status === 401) return;
|
const users: User[] = Array.isArray(rawUsers)
|
||||||
const result = await response.json();
|
? rawUsers.map((u: any) => ({
|
||||||
if (result.success) {
|
|
||||||
const p = result.data;
|
|
||||||
setProject(p);
|
|
||||||
setForm({
|
|
||||||
name: p.name || "",
|
|
||||||
status: p.status || "aktivni",
|
|
||||||
start_date: (p.start_date || "").substring(0, 10),
|
|
||||||
end_date: (p.end_date || "").substring(0, 10),
|
|
||||||
responsible_user_id: p.responsible_user_id || "",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se načíst projekt");
|
|
||||||
navigate("/projects");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
navigate("/projects");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch(`${API_BASE}/users`);
|
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
const raw = Array.isArray(data.data)
|
|
||||||
? data.data
|
|
||||||
: data.data?.items || [];
|
|
||||||
setUsers(
|
|
||||||
raw.map((u: any) => ({
|
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name:
|
name: `${u.first_name || ""} ${u.last_name || ""}`.trim() || u.username,
|
||||||
`${u.first_name || ""} ${u.last_name || ""}`.trim() ||
|
}))
|
||||||
u.username,
|
: [];
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDetail();
|
// Reset form sync when navigating to a different project
|
||||||
fetchNotes();
|
const formInitialized = useRef(false);
|
||||||
fetchUsers();
|
useEffect(() => {
|
||||||
}, [id, alert, navigate]); // eslint-disable-line react-hooks/exhaustive-deps
|
formInitialized.current = false;
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// Sync project data to local form state on first load
|
||||||
|
useEffect(() => {
|
||||||
|
if (project && !formInitialized.current) {
|
||||||
|
setForm({
|
||||||
|
name: project.name || "",
|
||||||
|
status: project.status || "aktivni",
|
||||||
|
start_date: (project.start_date || "").substring(0, 10),
|
||||||
|
end_date: (project.end_date || "").substring(0, 10),
|
||||||
|
responsible_user_id: project.responsible_user_id || "",
|
||||||
|
});
|
||||||
|
formInitialized.current = true;
|
||||||
|
}
|
||||||
|
}, [project]);
|
||||||
|
|
||||||
|
// Navigate away on fetch error
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectQuery.error) {
|
||||||
|
alert.error("Nepodařilo se načíst projekt");
|
||||||
|
navigate("/projects");
|
||||||
|
}
|
||||||
|
}, [projectQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (!hasPermission("projects.view")) return <Forbidden />;
|
if (!hasPermission("projects.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -194,6 +171,9 @@ export default function ProjectDetail() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Projekt byl aktualizován");
|
alert.success(result.message || "Projekt byl aktualizován");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit projekt");
|
alert.error(result.error || "Nepodařilo se uložit projekt");
|
||||||
}
|
}
|
||||||
@@ -214,6 +194,9 @@ export default function ProjectDetail() {
|
|||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
navigate("/projects");
|
navigate("/projects");
|
||||||
setTimeout(() => alert.success("Projekt byl smazán"), 300);
|
setTimeout(() => alert.success("Projekt byl smazán"), 300);
|
||||||
} else {
|
} else {
|
||||||
@@ -238,9 +221,9 @@ export default function ProjectDetail() {
|
|||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setNotes((prev) => [result.data.note, ...prev]);
|
|
||||||
setNewNote("");
|
setNewNote("");
|
||||||
alert.success("Poznámka byla přidána");
|
alert.success("Poznámka byla přidána");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects", id] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se přidat poznámku");
|
alert.error(result.error || "Nepodařilo se přidat poznámku");
|
||||||
}
|
}
|
||||||
@@ -262,8 +245,8 @@ export default function ProjectDetail() {
|
|||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setNotes((prev) => prev.filter((n) => n.id !== noteId));
|
|
||||||
alert.success("Poznámka byla smazána");
|
alert.success("Poznámka byla smazána");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects", id] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat poznámku");
|
alert.error(result.error || "Nepodařilo se smazat poznámku");
|
||||||
}
|
}
|
||||||
@@ -274,53 +257,16 @@ export default function ProjectDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div className="flex-row-gap">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-row" style={{ gap: "0.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!project) return null;
|
if (!project) return null;
|
||||||
|
|
||||||
const canEdit = hasPermission("projects.edit");
|
const canEdit = hasPermission("projects.edit");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton
|
||||||
|
name="project-detail"
|
||||||
|
loading={isPending}
|
||||||
|
fixture={<ProjectDetailFixture />}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -548,18 +494,7 @@ export default function ProjectDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notes list */}
|
{/* Notes list */}
|
||||||
{notesLoading && (
|
{notes.length === 0 && !project.notes && (
|
||||||
<div className="admin-skeleton" style={{ gap: "0.75rem" }}>
|
|
||||||
{[0, 1, 2].map((i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ height: "52px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!notesLoading && notes.length === 0 && !project.notes && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: "var(--text-tertiary)",
|
color: "var(--text-tertiary)",
|
||||||
@@ -571,7 +506,7 @@ export default function ProjectDetail() {
|
|||||||
Zatím žádné poznámky
|
Zatím žádné poznámky
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!notesLoading && (notes.length > 0 || project.notes) && (
|
{(notes.length > 0 || project.notes) && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -606,7 +541,9 @@ export default function ProjectDetail() {
|
|||||||
marginBottom: "0.25rem",
|
marginBottom: "0.25rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontWeight: 600, fontSize: "0.85rem" }}>
|
<span
|
||||||
|
style={{ fontWeight: 600, fontSize: "0.85rem" }}
|
||||||
|
>
|
||||||
{note.user_name}
|
{note.user_name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -769,5 +706,6 @@ export default function ProjectDetail() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user