v1.5.6: boneyard-js skeleton migration, TanStack Query refactor, rate-limit config
- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones - Remove skeleton.css and @keyframes shimmer from base.css - Add <Skeleton> wrappers with fixtures to all 25+ page components - Generate 20 bone captures via boneyard CLI (CDP auth-gated capture) - Refactor data fetching from useEffect+useState to TanStack Query - Extract query hooks into src/admin/lib/queries/ and apiAdapter - Add usePaginatedQuery hook replacing useApiCall/useListData - Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs - Fix customer_id mandatory validation on offer creation - Fix leave-requests comma-separated status filter (Prisma enum in: []) - Add cross-entity cache invalidation for orders/offers/invoices/projects - Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.) - Add boneyard.config.json with routes and breakpoints Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
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",
|
||||
"version": "1.5.3",
|
||||
"version": "1.5.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app-ts",
|
||||
"version": "1.5.3",
|
||||
"version": "1.5.5",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -19,8 +19,10 @@
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@tanstack/react-query": "^5.100.5",
|
||||
"@types/jsdom": "^28.0.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"boneyard-js": "^1.8.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "^17.3.1",
|
||||
@@ -148,6 +150,13 @@
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
@@ -353,7 +362,6 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -365,7 +373,6 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -376,7 +383,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -390,7 +396,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -407,7 +412,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -424,7 +428,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -441,7 +444,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -458,7 +460,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -475,7 +476,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -492,7 +492,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -509,7 +508,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -526,7 +524,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -543,7 +540,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -560,7 +556,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -577,7 +572,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -594,7 +588,6 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -611,7 +604,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -628,7 +620,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -645,7 +636,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -662,7 +652,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -679,7 +668,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -696,7 +684,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -713,7 +700,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -730,7 +716,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -747,7 +732,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -764,7 +748,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -781,7 +764,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -798,7 +780,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -815,7 +796,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1195,7 +1175,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1224,7 +1203,7 @@
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
||||
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -1234,7 +1213,7 @@
|
||||
"version": "0.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
||||
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
@@ -1385,7 +1364,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1402,7 +1380,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1419,7 +1396,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1436,7 +1412,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1453,7 +1428,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1470,7 +1444,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1487,7 +1460,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1504,7 +1476,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1521,7 +1492,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1538,7 +1508,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1555,7 +1524,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1572,7 +1540,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1589,7 +1556,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1606,7 +1572,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1623,7 +1588,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1646,6 +1610,32 @@
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"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": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
@@ -1662,7 +1652,6 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2332,6 +2321,53 @@
|
||||
"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": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
@@ -2889,7 +2925,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3087,7 +3123,7 @@
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3444,7 +3480,7 @@
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -3568,7 +3604,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -3656,7 +3691,7 @@
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
@@ -4135,7 +4170,7 @@
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -4168,7 +4203,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4189,7 +4223,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4210,7 +4243,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4231,7 +4263,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4252,7 +4283,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4273,7 +4303,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4294,7 +4323,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4315,7 +4343,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4336,7 +4363,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4357,7 +4383,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4378,7 +4403,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4618,7 +4642,7 @@
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4931,7 +4955,7 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4988,6 +5012,50 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
@@ -5001,7 +5069,7 @@
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5521,7 +5589,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
@@ -5556,7 +5624,7 @@
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.115.0",
|
||||
@@ -5590,7 +5658,7 @@
|
||||
"version": "1.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
@@ -6159,7 +6227,7 @@
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -6279,7 +6347,7 @@
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
@@ -6334,7 +6402,7 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
||||
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/runtime": "0.115.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-ts",
|
||||
"version": "1.5.5",
|
||||
"version": "1.5.6",
|
||||
"description": "",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
@@ -17,7 +17,8 @@
|
||||
"db:push": "prisma db push",
|
||||
"db:studio": "prisma studio",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"bones": "boneyard-js build http://localhost:3000"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -34,8 +35,10 @@
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@tanstack/react-query": "^5.100.5",
|
||||
"@types/jsdom": "^28.0.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"boneyard-js": "^1.8.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "^17.3.1",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AuthProvider } from "./context/AuthContext";
|
||||
import { AlertProvider } from "./context/AlertContext";
|
||||
import { queryClient } from "./lib/queryClient";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import AdminLayout from "./components/AdminLayout";
|
||||
import AlertContainer from "./components/AlertContainer";
|
||||
@@ -14,8 +16,8 @@ import "./buttons.css";
|
||||
import "./layout.css";
|
||||
import "./components.css";
|
||||
import "./tables.css";
|
||||
import "./skeleton.css";
|
||||
import "./datepicker.css";
|
||||
import "./bones/registry";
|
||||
import "./filemanager.css";
|
||||
import "./pagination.css";
|
||||
import "./responsive.css";
|
||||
@@ -57,63 +59,80 @@ export default function AdminApp() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AlertProvider>
|
||||
<AlertContainer />
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="admin-loading">
|
||||
<div className="admin-spinner" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route element={<AdminLayout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="attendance" element={<Attendance />} />
|
||||
<Route
|
||||
path="attendance/history"
|
||||
element={<AttendanceHistory />}
|
||||
/>
|
||||
<Route path="attendance/admin" element={<AttendanceAdmin />} />
|
||||
<Route
|
||||
path="attendance/balances"
|
||||
element={<AttendanceBalances />}
|
||||
/>
|
||||
<Route path="attendance/requests" element={<LeaveRequests />} />
|
||||
<Route path="attendance/approval" element={<LeaveApproval />} />
|
||||
<Route
|
||||
path="attendance/create"
|
||||
element={<AttendanceCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/location/:id"
|
||||
element={<AttendanceLocation />}
|
||||
/>
|
||||
<Route path="trips" element={<Trips />} />
|
||||
<Route path="trips/history" element={<TripsHistory />} />
|
||||
<Route path="trips/admin" element={<TripsAdmin />} />
|
||||
<Route path="vehicles" element={<Vehicles />} />
|
||||
<Route path="offers" element={<Offers />} />
|
||||
<Route path="offers/new" element={<OfferDetail />} />
|
||||
<Route path="offers/:id" element={<OfferDetail />} />
|
||||
<Route path="offers/customers" element={<OffersCustomers />} />
|
||||
<Route path="offers/templates" element={<OffersTemplates />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="orders/:id" element={<OrderDetail />} />
|
||||
<Route path="projects" element={<Projects />} />
|
||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
||||
<Route path="invoices" element={<Invoices />} />
|
||||
<Route path="invoices/new" element={<InvoiceDetail />} />
|
||||
<Route path="invoices/:id" element={<InvoiceDetail />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="audit-log" element={<AuditLog />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AlertContainer />
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="admin-loading">
|
||||
<div className="admin-spinner" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route element={<AdminLayout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="attendance" element={<Attendance />} />
|
||||
<Route
|
||||
path="attendance/history"
|
||||
element={<AttendanceHistory />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/admin"
|
||||
element={<AttendanceAdmin />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/balances"
|
||||
element={<AttendanceBalances />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/requests"
|
||||
element={<LeaveRequests />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/approval"
|
||||
element={<LeaveApproval />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/create"
|
||||
element={<AttendanceCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="attendance/location/:id"
|
||||
element={<AttendanceLocation />}
|
||||
/>
|
||||
<Route path="trips" element={<Trips />} />
|
||||
<Route path="trips/history" element={<TripsHistory />} />
|
||||
<Route path="trips/admin" element={<TripsAdmin />} />
|
||||
<Route path="vehicles" element={<Vehicles />} />
|
||||
<Route path="offers" element={<Offers />} />
|
||||
<Route path="offers/new" element={<OfferDetail />} />
|
||||
<Route path="offers/:id" element={<OfferDetail />} />
|
||||
<Route
|
||||
path="offers/customers"
|
||||
element={<OffersCustomers />}
|
||||
/>
|
||||
<Route
|
||||
path="offers/templates"
|
||||
element={<OffersTemplates />}
|
||||
/>
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="orders/:id" element={<OrderDetail />} />
|
||||
<Route path="projects" element={<Projects />} />
|
||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
||||
<Route path="invoices" element={<Invoices />} />
|
||||
<Route path="invoices/new" element={<InvoiceDetail />} />
|
||||
<Route path="invoices/:id" element={<InvoiceDetail />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="audit-log" element={<AuditLog />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</AlertProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
@@ -330,15 +330,6 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Additional Utilities ─────────────────────────────────────────── */
|
||||
|
||||
/* 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 ConfirmModal from "./ConfirmModal";
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import ProjectFileManagerFixture from "../fixtures/ProjectFileManagerFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -196,14 +200,11 @@ export default function ProjectFileManager({
|
||||
hasNasFolder,
|
||||
}: ProjectFileManagerProps) {
|
||||
const alert = useAlert();
|
||||
const queryClient = useQueryClient();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const isCancelling = useRef(false);
|
||||
|
||||
const [items, setItems] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPath, setCurrentPath] = useState("");
|
||||
const [breadcrumb, setBreadcrumb] = useState<string[]>([""]);
|
||||
const [fullPath, setFullPath] = useState("");
|
||||
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@@ -217,59 +218,25 @@ export default function ProjectFileManager({
|
||||
|
||||
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const canManage = hasPermission("projects.files");
|
||||
|
||||
const fetchFiles = useCallback(
|
||||
async (path = "", options: { ignore?: boolean } = {}) => {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
const params = new URLSearchParams({ project_id: String(projectId) });
|
||||
if (path) {
|
||||
params.set("path", path);
|
||||
}
|
||||
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
|
||||
if (options.ignore) return;
|
||||
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 {
|
||||
data: filesData,
|
||||
isPending: filesLoading,
|
||||
error: filesError,
|
||||
} = useQuery(projectFilesOptions(projectId, currentPath));
|
||||
const items = filesData?.items ?? [];
|
||||
const breadcrumb = filesData?.breadcrumb ?? [""];
|
||||
const fullPath = filesData?.full_path ?? "";
|
||||
const errorMessage = filesError
|
||||
? filesError.message || "Nepodařilo se načíst soubory"
|
||||
: null;
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
setNewFolderMode(false);
|
||||
setRenamingItem(null);
|
||||
fetchFiles(path);
|
||||
setCurrentPath(path);
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = (index: number) => {
|
||||
@@ -332,7 +299,9 @@ export default function ProjectFileManager({
|
||||
? "Soubor byl nahrán"
|
||||
: `Nahráno ${successCount} souborů`;
|
||||
alert.success(msg);
|
||||
fetchFiles(currentPath);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["projects", String(projectId), "files"],
|
||||
});
|
||||
}
|
||||
if (errorMsg) {
|
||||
alert.error(errorMsg);
|
||||
@@ -383,7 +352,9 @@ export default function ProjectFileManager({
|
||||
alert.success("Složka byla vytvořena");
|
||||
setNewFolderMode(false);
|
||||
setNewFolderName("");
|
||||
fetchFiles(currentPath);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["projects", String(projectId), "files"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se vytvořit složku");
|
||||
}
|
||||
@@ -444,7 +415,9 @@ export default function ProjectFileManager({
|
||||
? "Složka byla smazána"
|
||||
: "Soubor byl smazán",
|
||||
);
|
||||
fetchFiles(currentPath);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["projects", String(projectId), "files"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se smazat");
|
||||
}
|
||||
@@ -479,7 +452,9 @@ export default function ProjectFileManager({
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert.success("Přejmenováno");
|
||||
fetchFiles(currentPath);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["projects", String(projectId), "files"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se přejmenovat");
|
||||
}
|
||||
@@ -495,32 +470,15 @@ export default function ProjectFileManager({
|
||||
setRenameValue(item.name);
|
||||
};
|
||||
|
||||
if (loading && items.length === 0 && !errorMessage) {
|
||||
if (filesLoading && items.length === 0 && !errorMessage) {
|
||||
return (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-body">
|
||||
<h3 className="admin-card-title">Soubory</h3>
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "0.5rem" }}>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div
|
||||
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>
|
||||
<Skeleton
|
||||
name="project-file-manager"
|
||||
loading={filesLoading && items.length === 0}
|
||||
fixture={<ProjectFileManagerFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -710,7 +668,7 @@ export default function ProjectFileManager({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length === 0 && !loading ? (
|
||||
{items.length === 0 && !filesLoading ? (
|
||||
<div className="fm-empty">
|
||||
<svg
|
||||
width="32"
|
||||
|
||||
@@ -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 { useAlert } from "../../context/AlertContext";
|
||||
import ConfirmModal from "../ConfirmModal";
|
||||
import useModalLock from "../../hooks/useModalLock";
|
||||
import apiFetch from "../../utils/api";
|
||||
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";
|
||||
|
||||
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 {
|
||||
isOpen: boolean;
|
||||
session: Session | null;
|
||||
@@ -77,9 +67,10 @@ function getDeviceIcon(iconType?: string) {
|
||||
|
||||
export default function DashSessions() {
|
||||
const alert = useAlert();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true);
|
||||
const { data: sessions = [], isPending: sessionsLoading } =
|
||||
useQuery(sessionsOptions());
|
||||
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({
|
||||
isOpen: false,
|
||||
session: null,
|
||||
@@ -89,26 +80,6 @@ export default function DashSessions() {
|
||||
|
||||
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 () => {
|
||||
if (!deleteModal.session) {
|
||||
return;
|
||||
@@ -122,7 +93,7 @@ export default function DashSessions() {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setDeleteModal({ isOpen: false, session: null });
|
||||
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
||||
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||
alert.success("Relace byla ukončena");
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se ukončit relaci");
|
||||
@@ -143,7 +114,7 @@ export default function DashSessions() {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setDeleteAllModal(false);
|
||||
setSessions((prev) => prev.filter((s) => s.is_current));
|
||||
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||
alert.success(data.message || "Ostatní relace byly ukončeny");
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se ukončit relace");
|
||||
@@ -183,98 +154,84 @@ export default function DashSessions() {
|
||||
)}
|
||||
</div>
|
||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||
{sessionsLoading && (
|
||||
<div
|
||||
className="admin-skeleton"
|
||||
style={{ padding: "1rem", gap: "1rem" }}
|
||||
>
|
||||
{[0, 1, 2].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/2"
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line w-1/3"
|
||||
style={{ height: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!sessionsLoading && sessions.length === 0 && (
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
textAlign: "center",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
Žádné aktivní relace
|
||||
</div>
|
||||
)}
|
||||
{!sessionsLoading && sessions.length > 0 && (
|
||||
<div className="dash-sessions-list">
|
||||
{sessions.map((session) => (
|
||||
<Skeleton
|
||||
name="dash-sessions"
|
||||
loading={sessionsLoading}
|
||||
fixture={<DashSessionsFixture />}
|
||||
>
|
||||
<>
|
||||
{sessions.length === 0 && (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`dash-session-item ${session.is_current ? "dash-session-item-current" : ""}`}
|
||||
className="text-secondary"
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
textAlign: "center",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
<div className="dash-session-icon">
|
||||
{getDeviceIcon(session.device_info?.icon)}
|
||||
</div>
|
||||
<div className="dash-session-info">
|
||||
<div className="dash-session-device">
|
||||
{session.device_info?.browser} na{" "}
|
||||
{session.device_info?.os}
|
||||
{session.is_current && (
|
||||
<span
|
||||
className="admin-badge admin-badge-success"
|
||||
style={{ marginLeft: "0.5rem" }}
|
||||
>
|
||||
Aktuální
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="dash-session-meta">
|
||||
<span>{session.ip_address}</span>
|
||||
<span className="dash-session-meta-separator">|</span>
|
||||
<span>{formatSessionDate(session.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dash-session-actions">
|
||||
{!session.is_current && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteModal({ isOpen: true, session })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Ukončit relaci"
|
||||
aria-label="Ukončit relaci"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
Žádné aktivní relace
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{sessions.length > 0 && (
|
||||
<div className="dash-sessions-list">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`dash-session-item ${session.is_current ? "dash-session-item-current" : ""}`}
|
||||
>
|
||||
<div className="dash-session-icon">
|
||||
{getDeviceIcon(session.device_info?.icon)}
|
||||
</div>
|
||||
<div className="dash-session-info">
|
||||
<div className="dash-session-device">
|
||||
{session.device_info?.browser} na{" "}
|
||||
{session.device_info?.os}
|
||||
{session.is_current && (
|
||||
<span
|
||||
className="admin-badge admin-badge-success"
|
||||
style={{ marginLeft: "0.5rem" }}
|
||||
>
|
||||
Aktuální
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="dash-session-meta">
|
||||
<span>{session.ip_address}</span>
|
||||
<span className="dash-session-meta-separator">|</span>
|
||||
<span>{formatSessionDate(session.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dash-session-actions">
|
||||
{!session.is_current && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteModal({ isOpen: true, session })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Ukončit relaci"
|
||||
aria-label="Ukončit relaci"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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 { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -14,6 +15,9 @@ import {
|
||||
import FormField from "../components/FormField";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
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";
|
||||
|
||||
@@ -92,22 +96,20 @@ function getFundBarBackground(fund: MonthlyFund) {
|
||||
export default function Attendance() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [data, setData] = useState<AttendanceData>({
|
||||
ongoing_shift: null,
|
||||
today_shifts: [],
|
||||
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 queryClient = useQueryClient();
|
||||
|
||||
const statusQuery = useQuery({
|
||||
queryKey: ["attendance", "status"],
|
||||
queryFn: () => jsonQuery<AttendanceData>("/api/admin/attendance/status"),
|
||||
});
|
||||
|
||||
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 [leaveForm, setLeaveForm] = useState({
|
||||
leave_type: "vacation",
|
||||
@@ -117,10 +119,7 @@ export default function Attendance() {
|
||||
});
|
||||
const [requestSubmitting, setRequestSubmitting] = useState(false);
|
||||
const [notes, setNotes] = useState("");
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [switchingProject, setSwitchingProject] = useState(false);
|
||||
const [projectLogs, setProjectLogs] = useState<ProjectLog[]>([]);
|
||||
const [activeProjectId, setActiveProjectId] = useState<number | null>(null);
|
||||
const [gpsConfirm, setGpsConfirm] = useState<{
|
||||
isOpen: boolean;
|
||||
action: string | null;
|
||||
@@ -139,45 +138,12 @@ export default function Attendance() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
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);
|
||||
// Sync notes from query data when the shift changes
|
||||
useEffect(() => {
|
||||
if (statusQuery.data) {
|
||||
setNotes(statusQuery.data.ongoing_shift?.notes || "");
|
||||
}
|
||||
}, [alert]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
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 {
|
||||
// silent - projects are supplementary
|
||||
}
|
||||
};
|
||||
loadProjects();
|
||||
}, []);
|
||||
}, [statusQuery.data]);
|
||||
|
||||
useModalLock(isLeaveModalOpen);
|
||||
|
||||
@@ -277,7 +243,9 @@ export default function Attendance() {
|
||||
setSubmitting(false);
|
||||
|
||||
if (result.success) {
|
||||
await fetchData();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["attendance", "status"],
|
||||
});
|
||||
punchTimeoutRef.current = setTimeout(() => {
|
||||
alert.success(result.data?.message || result.message || "Uloženo");
|
||||
}, 300);
|
||||
@@ -302,7 +270,9 @@ export default function Attendance() {
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
await fetchData();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["attendance", "status"],
|
||||
});
|
||||
alert.success(
|
||||
result.data?.message || result.message || "Přestávka zaznamenána",
|
||||
);
|
||||
@@ -348,7 +318,9 @@ export default function Attendance() {
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
await fetchData();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["attendance", "status"],
|
||||
});
|
||||
alert.success(
|
||||
result.data?.message || result.message || "Projekt přepnut",
|
||||
);
|
||||
@@ -390,7 +362,9 @@ export default function Attendance() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setIsLeaveModalOpen(false);
|
||||
await fetchData();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["attendance", "status"],
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
alert.success(
|
||||
result.data?.message || result.message || "Žádost odeslána",
|
||||
@@ -411,103 +385,15 @@ export default function Attendance() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (!statusQuery.data) {
|
||||
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>
|
||||
<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>
|
||||
<Skeleton
|
||||
name="attendance"
|
||||
loading={statusQuery.isPending}
|
||||
fixture={<AttendanceFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -515,7 +401,11 @@ export default function Attendance() {
|
||||
ongoing_shift: ongoingShift,
|
||||
today_shifts: todayShifts,
|
||||
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 completedToday = todayShifts.filter((s) => s.departure_time);
|
||||
const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8);
|
||||
|
||||
@@ -11,6 +11,8 @@ import useModalLock from "../hooks/useModalLock";
|
||||
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
||||
import FormField from "../components/FormField";
|
||||
import { formatMinutes } from "../utils/attendanceHelpers";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import AttendanceAdminFixture from "../fixtures/AttendanceAdminFixture";
|
||||
|
||||
interface UserTotalData {
|
||||
name: string;
|
||||
@@ -95,84 +97,13 @@ export default function AttendanceAdmin() {
|
||||
|
||||
if (isInitialLoad) {
|
||||
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>
|
||||
<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>
|
||||
<Skeleton
|
||||
name="attendance-admin"
|
||||
loading={isInitialLoad}
|
||||
fixture={<AttendanceAdminFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
@@ -6,8 +6,16 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
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 { Skeleton } from "boneyard-js/react";
|
||||
import AttendanceBalancesFixture from "../fixtures/AttendanceBalancesFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface BalanceEntry {
|
||||
@@ -134,23 +142,20 @@ const getProgressBackground = (
|
||||
export default function AttendanceBalances() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const [year, setYear] = useState(new Date().getFullYear());
|
||||
const [data, setData] = useState<BalancesData>({
|
||||
users: [],
|
||||
balances: {},
|
||||
});
|
||||
|
||||
const [fundLoading, setFundLoading] = useState(true);
|
||||
const [fundData, setFundData] = useState<FundData>({
|
||||
months: {},
|
||||
holidays: [],
|
||||
users: [],
|
||||
balances: {},
|
||||
});
|
||||
|
||||
const [projectLoading, setProjectLoading] = useState(true);
|
||||
const [projectData, setProjectData] = useState<ProjectData>({ months: {} });
|
||||
const { data: balancesRaw, isPending: balancesPending } = useQuery(
|
||||
attendanceBalancesOptions(year),
|
||||
);
|
||||
const { data: fundRaw, isPending: fundPending } = useQuery(
|
||||
attendanceWorkFundOptions(year),
|
||||
);
|
||||
const { data: projectRaw, isPending: projectPending } = useQuery(
|
||||
attendanceProjectReportOptions(year),
|
||||
);
|
||||
const balancesData = balancesRaw as BalancesData | undefined;
|
||||
const fundData = fundRaw as FundData | undefined;
|
||||
const projectData = projectRaw as ProjectData | undefined;
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<{
|
||||
@@ -169,67 +174,6 @@ export default function AttendanceBalances() {
|
||||
userName: string;
|
||||
}>({ 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);
|
||||
|
||||
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
||||
@@ -265,8 +209,7 @@ export default function AttendanceBalances() {
|
||||
|
||||
if (result.success) {
|
||||
setShowEditModal(false);
|
||||
await fetchData(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -297,7 +240,7 @@ export default function AttendanceBalances() {
|
||||
|
||||
if (result.success) {
|
||||
setResetConfirm({ show: false, userId: null, userName: "" });
|
||||
await fetchData(false);
|
||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -315,7 +258,7 @@ export default function AttendanceBalances() {
|
||||
}
|
||||
|
||||
const getYearFundTotals = (userId: string) => {
|
||||
if (!fundData.months || Object.keys(fundData.months).length === 0)
|
||||
if (!fundData?.months || Object.keys(fundData.months).length === 0)
|
||||
return null;
|
||||
let totalFund = 0;
|
||||
let totalWorked = 0;
|
||||
@@ -380,132 +323,137 @@ export default function AttendanceBalances() {
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<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>
|
||||
)}
|
||||
{!loading && Object.keys(data.balances).length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádní uživatelé k zobrazení.</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && Object.keys(data.balances).length > 0 && (
|
||||
<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>Fond roku</th>
|
||||
<th>Pokryto</th>
|
||||
<th>+/−</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(data.balances).map(([userId, balance]) => {
|
||||
const yf = getYearFundTotals(userId);
|
||||
return (
|
||||
<tr key={userId}>
|
||||
<td className="fw-500">{balance.name}</td>
|
||||
<td className="admin-mono">{balance.vacation_total}</td>
|
||||
<td className="admin-mono">
|
||||
{balance.vacation_used.toFixed(1)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span
|
||||
className={getVacationClass(
|
||||
balance.vacation_remaining,
|
||||
)}
|
||||
>
|
||||
{balance.vacation_remaining.toFixed(1)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{balance.sick_used.toFixed(1)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? `${yf.fund}h` : "—"}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? `${yf.covered}h` : "—"}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? renderFundDiff(yf) : "—"}
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button
|
||||
onClick={() => openEditModal(userId, balance)}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setResetConfirm({
|
||||
show: true,
|
||||
userId,
|
||||
userName: balance.name,
|
||||
})
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Resetovat"
|
||||
aria-label="Resetovat"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Skeleton
|
||||
name="attendance-balances"
|
||||
loading={balancesPending}
|
||||
fixture={<AttendanceBalancesFixture />}
|
||||
>
|
||||
<>
|
||||
{balancesData &&
|
||||
Object.keys(balancesData.balances).length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádní uživatelé k zobrazení.</p>
|
||||
</div>
|
||||
)}
|
||||
{balancesData &&
|
||||
Object.keys(balancesData.balances).length > 0 && (
|
||||
<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>Fond roku</th>
|
||||
<th>Pokryto</th>
|
||||
<th>+/−</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(balancesData.balances).map(
|
||||
([userId, balance]) => {
|
||||
const yf = getYearFundTotals(userId);
|
||||
return (
|
||||
<tr key={userId}>
|
||||
<td className="fw-500">{balance.name}</td>
|
||||
<td className="admin-mono">
|
||||
{balance.vacation_total}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{balance.vacation_used.toFixed(1)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span
|
||||
className={getVacationClass(
|
||||
balance.vacation_remaining,
|
||||
)}
|
||||
>
|
||||
{balance.vacation_remaining.toFixed(1)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{balance.sick_used.toFixed(1)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? `${yf.fund}h` : "—"}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? `${yf.covered}h` : "—"}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{yf ? renderFundDiff(yf) : "—"}
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button
|
||||
onClick={() =>
|
||||
openEditModal(userId, balance)
|
||||
}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setResetConfirm({
|
||||
show: true,
|
||||
userId,
|
||||
userName: balance.name,
|
||||
})
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Resetovat"
|
||||
aria-label="Resetovat"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Monthly Fund Overview */}
|
||||
{!fundLoading &&
|
||||
fundData.months &&
|
||||
{!fundPending &&
|
||||
fundData?.months &&
|
||||
Object.keys(fundData.months).length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
@@ -587,7 +535,7 @@ export default function AttendanceBalances() {
|
||||
gap: "0.375rem",
|
||||
}}
|
||||
>
|
||||
{fundData.users &&
|
||||
{fundData?.users &&
|
||||
fundData.users.map((user) => {
|
||||
const us = monthData.users?.[String(user.id)];
|
||||
if (!us) return null;
|
||||
@@ -668,23 +616,19 @@ export default function AttendanceBalances() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{fundLoading && (
|
||||
<div className="mt-6">
|
||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
||||
{[0, 1, 2].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>
|
||||
{fundPending && (
|
||||
<Skeleton
|
||||
name="attendance-balances-fund"
|
||||
loading={fundPending}
|
||||
fixture={<AttendanceBalancesFixture />}
|
||||
>
|
||||
<div className="mt-6" />
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* Monthly Project Overview */}
|
||||
{!projectLoading &&
|
||||
projectData.months &&
|
||||
{!projectPending &&
|
||||
projectData?.months &&
|
||||
Object.keys(projectData.months).length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
@@ -876,18 +820,14 @@ export default function AttendanceBalances() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{projectLoading && (
|
||||
<div className="mt-6">
|
||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
||||
{[0, 1, 2].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>
|
||||
{projectPending && (
|
||||
<Skeleton
|
||||
name="attendance-balances-projects"
|
||||
loading={projectPending}
|
||||
fixture={<AttendanceBalancesFixture />}
|
||||
>
|
||||
<div className="mt-6" />
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* 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 { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
@@ -8,6 +10,8 @@ import { motion } from "framer-motion";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import FormField from "../components/FormField";
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import AttendanceCreateFixture from "../fixtures/AttendanceCreateFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface User {
|
||||
@@ -35,9 +39,9 @@ export default function AttendanceCreate() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
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 [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
const [form, setForm] = useState<CreateForm>(() => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -125,247 +109,223 @@ export default function AttendanceCreate() {
|
||||
|
||||
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 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>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Přidat záznam docházky</h1>
|
||||
</div>
|
||||
<div className="admin-page-actions">
|
||||
<Link
|
||||
to="/attendance/admin"
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
← Zpět na správu
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Skeleton
|
||||
name="attendance-create"
|
||||
loading={loading}
|
||||
fixture={<AttendanceCreateFixture />}
|
||||
>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Přidat záznam docházky</h1>
|
||||
</div>
|
||||
<div className="admin-page-actions">
|
||||
<Link
|
||||
to="/attendance/admin"
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
← Zpět na správu
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
style={{ maxWidth: "600px" }}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<form onSubmit={handleSubmit} className="admin-form">
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Zaměstnanec" required>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
style={{ maxWidth: "600px" }}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<form onSubmit={handleSubmit} className="admin-form">
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Zaměstnanec" required>
|
||||
<select
|
||||
value={form.user_id}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, user_id: e.target.value })
|
||||
}
|
||||
className="admin-form-select"
|
||||
required
|
||||
>
|
||||
<option value="">Vyberte zaměstnance</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Datum směny" required>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.shift_date}
|
||||
onChange={(val: string) => handleShiftDateChange(val)}
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Typ záznamu" required>
|
||||
<select
|
||||
value={form.user_id}
|
||||
value={form.leave_type}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, user_id: e.target.value })
|
||||
setForm({ ...form, leave_type: e.target.value })
|
||||
}
|
||||
className="admin-form-select"
|
||||
required
|
||||
>
|
||||
<option value="">Vyberte zaměstnance</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name}
|
||||
</option>
|
||||
))}
|
||||
<option value="work">Práce</option>
|
||||
<option value="vacation">Dovolená</option>
|
||||
<option value="sick">Nemoc</option>
|
||||
<option value="holiday">Svátek</option>
|
||||
<option value="unpaid">Neplacené volno</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Datum směny" required>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.shift_date}
|
||||
onChange={(val: string) => handleShiftDateChange(val)}
|
||||
required
|
||||
|
||||
{!isWorkType && (
|
||||
<FormField label="Počet hodin">
|
||||
<input
|
||||
type="number"
|
||||
value={form.leave_hours}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
leave_hours: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
min="0.5"
|
||||
max="24"
|
||||
step="0.5"
|
||||
className="admin-form-input"
|
||||
/>
|
||||
<small className="admin-form-hint">
|
||||
Výchozí 8 hodin pro celý den
|
||||
</small>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{isWorkType && (
|
||||
<>
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Příchod - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.arrival_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, arrival_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Příchod - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.arrival_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, arrival_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Začátek pauzy - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_start_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_start_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Začátek pauzy - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_start_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_start_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Konec pauzy - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_end_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_end_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Konec pauzy - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_end_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_end_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Odchod - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.departure_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, departure_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Odchod - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.departure_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, departure_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField label="Poznámka">
|
||||
<textarea
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
||||
className="admin-form-textarea"
|
||||
rows={3}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Typ záznamu" required>
|
||||
<select
|
||||
value={form.leave_type}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, leave_type: e.target.value })
|
||||
}
|
||||
className="admin-form-select"
|
||||
>
|
||||
<option value="work">Práce</option>
|
||||
<option value="vacation">Dovolená</option>
|
||||
<option value="sick">Nemoc</option>
|
||||
<option value="holiday">Svátek</option>
|
||||
<option value="unpaid">Neplacené volno</option>
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
{!isWorkType && (
|
||||
<FormField label="Počet hodin">
|
||||
<input
|
||||
type="number"
|
||||
value={form.leave_hours}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
leave_hours: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
min="0.5"
|
||||
max="24"
|
||||
step="0.5"
|
||||
className="admin-form-input"
|
||||
/>
|
||||
<small className="admin-form-hint">
|
||||
Výchozí 8 hodin pro celý den
|
||||
</small>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{isWorkType && (
|
||||
<>
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Příchod - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.arrival_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, arrival_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Příchod - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.arrival_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, arrival_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Začátek pauzy - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_start_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_start_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Začátek pauzy - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_start_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_start_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Konec pauzy - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_end_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_end_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Konec pauzy - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_end_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, break_end_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Odchod - datum">
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.departure_date}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, departure_date: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Odchod - čas">
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.departure_time}
|
||||
onChange={(val: string) =>
|
||||
setForm({ ...form, departure_time: val })
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField label="Poznámka">
|
||||
<textarea
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
||||
className="admin-form-textarea"
|
||||
rows={3}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<Link
|
||||
to="/attendance/admin"
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
Zrušit
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
{submitting ? "Ukládám..." : "Uložit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="admin-form-actions">
|
||||
<Link
|
||||
to="/attendance/admin"
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
Zrušit
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
{submitting ? "Ukládám..." : "Uložit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { motion } from "framer-motion";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
import { attendanceHistoryOptions } from "../lib/queries/attendance";
|
||||
import {
|
||||
formatDate,
|
||||
formatDatetime,
|
||||
@@ -16,10 +18,8 @@ import {
|
||||
formatTimeOrDatetimePrint,
|
||||
} from "../utils/attendanceHelpers";
|
||||
import FormField from "../components/FormField";
|
||||
import apiFetch from "../utils/api";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import AttendanceHistoryFixture from "../fixtures/AttendanceHistoryFixture";
|
||||
interface ProjectLog {
|
||||
id?: number;
|
||||
project_id?: number;
|
||||
@@ -193,48 +193,21 @@ const renderProjectCell = (record: AttendanceRecord) => {
|
||||
};
|
||||
|
||||
export default function AttendanceHistory() {
|
||||
const alert = useAlert();
|
||||
const { user, hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||
const companyName =
|
||||
((companySettings as Record<string, unknown> | undefined)
|
||||
?.company_name as string) || "";
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
const [month, setMonth] = useState(() => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
});
|
||||
const [records, setRecords] = useState<AttendanceRecord[]>([]);
|
||||
|
||||
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 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 { data, isPending } = useQuery(
|
||||
attendanceHistoryOptions({ month, userId: user?.id }),
|
||||
);
|
||||
const records = (data as AttendanceRecord[] | undefined) ?? [];
|
||||
|
||||
const computed = useMemo(() => {
|
||||
const [yearStr, monthStr] = month.split("-");
|
||||
@@ -459,144 +432,123 @@ export default function AttendanceHistory() {
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<div className="admin-skeleton" style={{ gap: "0.5rem" }}>
|
||||
<div className="admin-skeleton-row" style={{ gap: "1rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{
|
||||
width: "48px",
|
||||
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
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "1rem",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<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: "200px" }}>
|
||||
<Skeleton
|
||||
name="attendance-history-fund"
|
||||
loading={isPending}
|
||||
fixture={<AttendanceHistoryFixture />}
|
||||
>
|
||||
<>
|
||||
{computed.monthlyFund && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
marginBottom: "0.375rem",
|
||||
alignItems: "center",
|
||||
gap: "1rem",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
>
|
||||
Fond: {computed.monthlyFund.worked}h /{" "}
|
||||
{computed.monthlyFund.fund}h
|
||||
</span>
|
||||
<span
|
||||
className="text-secondary"
|
||||
style={{ fontSize: "0.8125rem" }}
|
||||
>
|
||||
{computed.monthlyFund.business_days} prac. dnů
|
||||
</span>
|
||||
</div>
|
||||
<div className="attendance-balance-bar">
|
||||
<div
|
||||
className="attendance-balance-progress"
|
||||
style={{
|
||||
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`,
|
||||
background:
|
||||
computed.monthlyFund.covered >=
|
||||
computed.monthlyFund.fund
|
||||
? "linear-gradient(135deg, var(--success), #059669)"
|
||||
: "var(--gradient)",
|
||||
}}
|
||||
/>
|
||||
<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: "200px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
marginBottom: "0.375rem",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
>
|
||||
Fond: {computed.monthlyFund.worked}h /{" "}
|
||||
{computed.monthlyFund.fund}h
|
||||
</span>
|
||||
<span
|
||||
className="text-secondary"
|
||||
style={{ fontSize: "0.8125rem" }}
|
||||
>
|
||||
{computed.monthlyFund.business_days} prac. dnů
|
||||
</span>
|
||||
</div>
|
||||
<div className="attendance-balance-bar">
|
||||
<div
|
||||
className="attendance-balance-progress"
|
||||
style={{
|
||||
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`,
|
||||
background:
|
||||
computed.monthlyFund.covered >=
|
||||
computed.monthlyFund.fund
|
||||
? "linear-gradient(135deg, var(--success), #059669)"
|
||||
: "var(--gradient)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-muted"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "0.75rem",
|
||||
marginTop: "0.375rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{"Pokryto: "}
|
||||
{computed.monthlyFund.covered}h (práce{" "}
|
||||
{computed.monthlyFund.worked}h
|
||||
{computed.vacationHours > 0 &&
|
||||
` + dovolená ${computed.vacationHours}h`}
|
||||
{computed.sickHours > 0 &&
|
||||
` + nemoc ${computed.sickHours}h`}
|
||||
{computed.holidayHours > 0 &&
|
||||
` + svátek ${computed.holidayHours}h`}
|
||||
{computed.unpaidHours > 0 &&
|
||||
` + neplacené ${computed.unpaidHours}h`}
|
||||
)
|
||||
</span>
|
||||
{computed.monthlyFund.overtime > 0 ? (
|
||||
<span className="text-warning fw-600">
|
||||
Přesčas: +{computed.monthlyFund.overtime}h
|
||||
</span>
|
||||
) : (
|
||||
<span>Zbývá: {computed.monthlyFund.remaining}h</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!computed.monthlyFund && (
|
||||
<div
|
||||
className="text-muted"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "0.75rem",
|
||||
marginTop: "0.375rem",
|
||||
fontSize: "0.875rem",
|
||||
textAlign: "center",
|
||||
padding: "0.5rem 0",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{"Pokryto: "}
|
||||
{computed.monthlyFund.covered}h (práce{" "}
|
||||
{computed.monthlyFund.worked}h
|
||||
{computed.vacationHours > 0 &&
|
||||
` + dovolená ${computed.vacationHours}h`}
|
||||
{computed.sickHours > 0 &&
|
||||
` + nemoc ${computed.sickHours}h`}
|
||||
{computed.holidayHours > 0 &&
|
||||
` + svátek ${computed.holidayHours}h`}
|
||||
{computed.unpaidHours > 0 &&
|
||||
` + neplacené ${computed.unpaidHours}h`}
|
||||
)
|
||||
</span>
|
||||
{computed.monthlyFund.overtime > 0 ? (
|
||||
<span className="text-warning fw-600">
|
||||
Přesčas: +{computed.monthlyFund.overtime}h
|
||||
</span>
|
||||
) : (
|
||||
<span>Zbývá: {computed.monthlyFund.remaining}h</span>
|
||||
)}
|
||||
Fond měsíce není k dispozici
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !computed.monthlyFund && (
|
||||
<div
|
||||
className="text-muted"
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
textAlign: "center",
|
||||
padding: "0.5rem 0",
|
||||
}}
|
||||
>
|
||||
Fond měsíce není k dispozici
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -608,91 +560,90 @@ export default function AttendanceHistory() {
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<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" />
|
||||
<Skeleton
|
||||
name="attendance-history-table"
|
||||
loading={isPending}
|
||||
fixture={<AttendanceHistoryFixture />}
|
||||
>
|
||||
<>
|
||||
{records.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!loading && records.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && records.length > 0 && (
|
||||
<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>
|
||||
<th>Projekty</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((record) => {
|
||||
const leaveType = record.leave_type || "work";
|
||||
const isLeave = leaveType !== "work";
|
||||
const workMinutes = isLeave
|
||||
? (Number(record.leave_hours) || 8) * 60
|
||||
: calculateWorkMinutes(record);
|
||||
|
||||
return (
|
||||
<tr key={record.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(record.shift_date)}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}
|
||||
>
|
||||
{getLeaveTypeName(leaveType)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave ? "—" : formatDatetime(record.arrival_time)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave ? "—" : formatBreakRange(record)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatDatetime(record.departure_time)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{workMinutes > 0
|
||||
? formatMinutes(workMinutes, true)
|
||||
: "—"}
|
||||
</td>
|
||||
<td>{renderProjectCell(record)}</td>
|
||||
<td
|
||||
style={{
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{record.notes || ""}
|
||||
</td>
|
||||
)}
|
||||
{records.length > 0 && (
|
||||
<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>
|
||||
<th>Projekty</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((record) => {
|
||||
const leaveType = record.leave_type || "work";
|
||||
const isLeave = leaveType !== "work";
|
||||
const workMinutes = isLeave
|
||||
? (Number(record.leave_hours) || 8) * 60
|
||||
: calculateWorkMinutes(record);
|
||||
|
||||
return (
|
||||
<tr key={record.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(record.shift_date)}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${getLeaveTypeBadgeClass(leaveType)}`}
|
||||
>
|
||||
{getLeaveTypeName(leaveType)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatDatetime(record.arrival_time)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave ? "—" : formatBreakRange(record)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{isLeave
|
||||
? "—"
|
||||
: formatDatetime(record.departure_time)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{workMinutes > 0
|
||||
? formatMinutes(workMinutes, true)
|
||||
: "—"}
|
||||
</td>
|
||||
<td>{renderProjectCell(record)}</td>
|
||||
<td
|
||||
style={{
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{record.notes || ""}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</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 { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
@@ -9,65 +10,35 @@ import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
||||
import apiFetch from "../utils/api";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
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;
|
||||
}
|
||||
import {
|
||||
attendanceLocationOptions,
|
||||
type LocationRecord,
|
||||
} from "../lib/queries/attendance";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import AttendanceLocationFixture from "../fixtures/AttendanceLocationFixture";
|
||||
|
||||
export default function AttendanceLocation() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [record, setRecord] = useState<LocationRecord | null>(null);
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
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");
|
||||
navigate("/attendance/admin");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const locationQuery = useQuery(attendanceLocationOptions(id));
|
||||
const record = locationQuery.data ?? null;
|
||||
const isPending = locationQuery.isPending;
|
||||
|
||||
fetchData();
|
||||
}, [id, alert, navigate]);
|
||||
// Navigate away on fetch error
|
||||
useEffect(() => {
|
||||
if (locationQuery.error) {
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
navigate("/attendance/admin");
|
||||
}
|
||||
}, [locationQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (!record || loading) return;
|
||||
if (!record || isPending) return;
|
||||
|
||||
const hasArrivalLocation = record.arrival_lat && record.arrival_lng;
|
||||
const hasDepartureLocation = record.departure_lat && record.departure_lng;
|
||||
@@ -175,7 +146,7 @@ export default function AttendanceLocation() {
|
||||
mapInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [record, loading]);
|
||||
}, [record, isPending]);
|
||||
|
||||
const formatDatetimeLocal = (datetime: string | null | undefined): string => {
|
||||
if (!datetime) return "—";
|
||||
@@ -185,56 +156,6 @@ export default function AttendanceLocation() {
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -248,102 +169,70 @@ export default function AttendanceLocation() {
|
||||
const month = shiftDateStr.substring(0, 7);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Poloha záznamu</h1>
|
||||
</div>
|
||||
<div className="admin-page-actions">
|
||||
<Link
|
||||
to={`/attendance/admin?month=${month}`}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
← Zpět na správu
|
||||
</Link>
|
||||
</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 }}
|
||||
>
|
||||
<div className="admin-card-header">
|
||||
<h2 className="admin-card-title">
|
||||
{record.user_name} — {formatDate(record.shift_date)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{hasAnyLocation && (
|
||||
<div ref={mapRef} className="attendance-location-map" />
|
||||
)}
|
||||
|
||||
<div className="attendance-location-grid">
|
||||
{/* Arrival */}
|
||||
<div
|
||||
className={`attendance-location-card ${!hasArrivalLocation ? "empty" : ""}`}
|
||||
<Skeleton
|
||||
name="attendance-location"
|
||||
loading={isPending}
|
||||
fixture={<AttendanceLocationFixture />}
|
||||
>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Poloha záznamu</h1>
|
||||
</div>
|
||||
<div className="admin-page-actions">
|
||||
<Link
|
||||
to={`/attendance/admin?month=${month}`}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
<h3 className="attendance-location-title">Příchod</h3>
|
||||
<div className="attendance-location-time">
|
||||
{record.arrival_time
|
||||
? formatDatetimeLocal(record.arrival_time)
|
||||
: "—"}
|
||||
</div>
|
||||
{hasArrivalLocation ? (
|
||||
<>
|
||||
<div className="attendance-location-address">
|
||||
{record.arrival_address || <em>Adresa nezjištěna</em>}
|
||||
</div>
|
||||
<div className="attendance-location-coords">
|
||||
GPS: {record.arrival_lat}, {record.arrival_lng}
|
||||
{record.arrival_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
|
||||
>
|
||||
Otevřít v Google Maps
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<div className="attendance-location-address">
|
||||
<em>Poloha nebyla zaznamenána</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
← Zpět na správu
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Departure */}
|
||||
{(hasDepartureLocation || record.departure_time) && (
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-card-header">
|
||||
<h2 className="admin-card-title">
|
||||
{record.user_name} — {formatDate(record.shift_date)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{hasAnyLocation && (
|
||||
<div ref={mapRef} className="attendance-location-map" />
|
||||
)}
|
||||
|
||||
<div className="attendance-location-grid">
|
||||
{/* Arrival */}
|
||||
<div
|
||||
className={`attendance-location-card ${!hasDepartureLocation ? "empty" : ""}`}
|
||||
className={`attendance-location-card ${!hasArrivalLocation ? "empty" : ""}`}
|
||||
>
|
||||
<h3 className="attendance-location-title">Odchod</h3>
|
||||
<h3 className="attendance-location-title">Příchod</h3>
|
||||
<div className="attendance-location-time">
|
||||
{record.departure_time
|
||||
? formatDatetimeLocal(record.departure_time)
|
||||
{record.arrival_time
|
||||
? formatDatetimeLocal(record.arrival_time)
|
||||
: "—"}
|
||||
</div>
|
||||
{hasDepartureLocation ? (
|
||||
{hasArrivalLocation ? (
|
||||
<>
|
||||
<div className="attendance-location-address">
|
||||
{record.departure_address || <em>Adresa nezjištěna</em>}
|
||||
{record.arrival_address || <em>Adresa nezjištěna</em>}
|
||||
</div>
|
||||
<div className="attendance-location-coords">
|
||||
GPS: {record.departure_lat}, {record.departure_lng}
|
||||
{record.departure_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
||||
GPS: {record.arrival_lat}, {record.arrival_lng}
|
||||
{record.arrival_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
|
||||
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
|
||||
@@ -357,10 +246,48 @@ export default function AttendanceLocation() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Departure */}
|
||||
{(hasDepartureLocation || record.departure_time) && (
|
||||
<div
|
||||
className={`attendance-location-card ${!hasDepartureLocation ? "empty" : ""}`}
|
||||
>
|
||||
<h3 className="attendance-location-title">Odchod</h3>
|
||||
<div className="attendance-location-time">
|
||||
{record.departure_time
|
||||
? formatDatetimeLocal(record.departure_time)
|
||||
: "—"}
|
||||
</div>
|
||||
{hasDepartureLocation ? (
|
||||
<>
|
||||
<div className="attendance-location-address">
|
||||
{record.departure_address || <em>Adresa nezjištěna</em>}
|
||||
</div>
|
||||
<div className="attendance-location-coords">
|
||||
GPS: {record.departure_lat}, {record.departure_lng}
|
||||
{record.departure_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
|
||||
>
|
||||
Otevřít v Google Maps
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<div className="attendance-location-address">
|
||||
<em>Poloha nebyla zaznamenána</em>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.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 { useAuth } from "../context/AuthContext";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
@@ -8,6 +9,8 @@ import FormField from "../components/FormField";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import { czechPlural } from "../utils/formatters";
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import AuditLogFixture from "../fixtures/AuditLogFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -77,13 +80,6 @@ interface AuditLogEntry {
|
||||
user_ip: string | null;
|
||||
}
|
||||
|
||||
interface PaginationData {
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
interface Filters {
|
||||
search: string;
|
||||
action: string;
|
||||
@@ -95,9 +91,7 @@ interface Filters {
|
||||
export default function AuditLog() {
|
||||
const { hasPermission } = useAuth();
|
||||
const alert = useAlert();
|
||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState<PaginationData | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const [filters, setFilters] = useState<Filters>({
|
||||
search: "",
|
||||
action: "",
|
||||
@@ -105,53 +99,57 @@ export default function AuditLog() {
|
||||
date_from: "",
|
||||
date_to: "",
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(50);
|
||||
const [showCleanup, setShowCleanup] = useState(false);
|
||||
const [cleanupDays, setCleanupDays] = useState(90);
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
|
||||
const fetchLogs = useCallback(
|
||||
async (page = 1, perPage = 50) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
});
|
||||
const { data: logsData, isPending } = useQuery({
|
||||
queryKey: [
|
||||
"audit-log",
|
||||
{
|
||||
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({
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
});
|
||||
if (filters.search) params.set("search", filters.search);
|
||||
if (filters.action) params.set("action", filters.action);
|
||||
if (filters.entity_type) params.set("entity_type", filters.entity_type);
|
||||
if (filters.date_from) params.set("date_from", filters.date_from);
|
||||
if (filters.date_to) params.set("date_to", filters.date_to);
|
||||
|
||||
if (filters.search) params.set("search", filters.search);
|
||||
if (filters.action) params.set("action", filters.action);
|
||||
if (filters.entity_type) params.set("entity_type", filters.entity_type);
|
||||
if (filters.date_from) params.set("date_from", filters.date_from);
|
||||
if (filters.date_to) params.set("date_to", filters.date_to);
|
||||
|
||||
const response = await apiFetch(
|
||||
`${API_BASE}/audit-log?${params.toString()}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setLogs(Array.isArray(data.data) ? data.data : []);
|
||||
setPagination({
|
||||
total: data.pagination?.total ?? 0,
|
||||
page: data.pagination?.page ?? 1,
|
||||
per_page: data.pagination?.limit ?? 50,
|
||||
total_pages: data.pagination?.total_pages ?? 1,
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se načíst audit log");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const response = await apiFetch(
|
||||
`${API_BASE}/audit-log?${params.toString()}`,
|
||||
);
|
||||
if (response.status === 401) throw new Error("Unauthorized");
|
||||
const result = await response.json();
|
||||
if (!result.success)
|
||||
throw new Error(result.error || "Nepodařilo se načíst audit log");
|
||||
return {
|
||||
data: Array.isArray(result.data) ? result.data : [],
|
||||
pagination: {
|
||||
total: result.pagination?.total ?? 0,
|
||||
page: result.pagination?.page ?? 1,
|
||||
per_page: result.pagination?.limit ?? perPage,
|
||||
total_pages: result.pagination?.total_pages ?? 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
[filters, alert],
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [fetchLogs]);
|
||||
const logs = logsData?.data ?? [];
|
||||
const pagination = logsData?.pagination ?? null;
|
||||
|
||||
if (!hasPermission("settings.audit")) {
|
||||
return <Forbidden />;
|
||||
@@ -159,14 +157,16 @@ export default function AuditLog() {
|
||||
|
||||
const handleFilterChange = (key: keyof Filters, value: string) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
fetchLogs(newPage, pagination?.per_page || 50);
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
fetchLogs(1, newPerPage);
|
||||
setPage(1);
|
||||
setPerPage(newPerPage);
|
||||
};
|
||||
|
||||
const handleCleanup = async () => {
|
||||
@@ -181,7 +181,7 @@ export default function AuditLog() {
|
||||
if (data.success) {
|
||||
alert.success(data.message);
|
||||
setShowCleanup(false);
|
||||
fetchLogs();
|
||||
queryClient.invalidateQueries({ queryKey: ["audit-log"] });
|
||||
} else {
|
||||
alert.error(data.error);
|
||||
}
|
||||
@@ -197,66 +197,15 @@ export default function AuditLog() {
|
||||
return new Date(dateString).toLocaleString("cs-CZ");
|
||||
};
|
||||
|
||||
if (loading && logs.length === 0) {
|
||||
if (isPending && logs.length === 0) {
|
||||
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: "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>
|
||||
<Skeleton
|
||||
name="audit-log"
|
||||
loading={isPending && logs.length === 0}
|
||||
fixture={<AuditLogFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -454,98 +403,123 @@ export default function AuditLog() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading &&
|
||||
Array.from({ length: 10 }, (_, i) => (
|
||||
<tr key={`skeleton-${i}`}>
|
||||
<td>
|
||||
<Skeleton
|
||||
name="audit-log-rows"
|
||||
loading={isPending}
|
||||
fixture={
|
||||
<div style={{ padding: "1rem" }}>
|
||||
{Array.from({ length: 10 }, (_, i) => (
|
||||
<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"
|
||||
key={i}
|
||||
style={{
|
||||
width: "70px",
|
||||
height: "22px",
|
||||
borderRadius: "10px",
|
||||
display: "flex",
|
||||
gap: "1rem",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "80px", height: "14px" }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "60%", height: "14px" }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "90px", height: "14px" }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!loading && logs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
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" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Žádné záznamy k zobrazení</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!loading &&
|
||||
logs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDatetime(log.created_at)}
|
||||
</td>
|
||||
<td className="fw-500">{log.username || "-"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${ACTION_BADGE_CLASS[log.action] || "admin-badge-secondary"}`}
|
||||
>
|
||||
{ACTION_LABELS[log.action] || log.action}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{ENTITY_TYPE_LABELS[log.entity_type || ""] ||
|
||||
log.entity_type ||
|
||||
"-"}
|
||||
</td>
|
||||
<td>{log.description || "-"}</td>
|
||||
<td className="admin-mono">{log.user_ip || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
width: 110,
|
||||
height: 14,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 80,
|
||||
height: 14,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 70,
|
||||
height: 22,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 10,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 80,
|
||||
height: 14,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 14,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: 90,
|
||||
height: 14,
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{logs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
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" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Žádné záznamy k zobrazení</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{logs.length > 0 &&
|
||||
logs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDatetime(log.created_at)}
|
||||
</td>
|
||||
<td className="fw-500">{log.username || "-"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${ACTION_BADGE_CLASS[log.action] || "admin-badge-secondary"}`}
|
||||
>
|
||||
{ACTION_LABELS[log.action] || log.action}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{ENTITY_TYPE_LABELS[log.entity_type || ""] ||
|
||||
log.entity_type ||
|
||||
"-"}
|
||||
</td>
|
||||
<td>{log.description || "-"}</td>
|
||||
<td className="admin-mono">{log.user_ip || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
</Skeleton>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import FormField from "../components/FormField";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
import { bankAccountsOptions } from "../lib/queries/common";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import CompanySettingsFixture from "../fixtures/CompanySettingsFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
const DEFAULT_FIELD_ORDER = [
|
||||
@@ -68,7 +73,7 @@ export default function CompanySettings({
|
||||
}: { embedded?: boolean } = {}) {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||
const [uploadingLogoDark, setUploadingLogoDark] = useState(false);
|
||||
@@ -89,14 +94,12 @@ export default function CompanySettings({
|
||||
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
||||
...DEFAULT_FIELD_ORDER,
|
||||
]);
|
||||
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
||||
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
|
||||
"CZK",
|
||||
"EUR",
|
||||
"USD",
|
||||
"GBP",
|
||||
]);
|
||||
const [bankLoading, setBankLoading] = useState(true);
|
||||
const [bankSaving, setBankSaving] = useState(false);
|
||||
const [editingBank, setEditingBank] = useState<number | null>(null);
|
||||
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
|
||||
@@ -182,84 +185,63 @@ export default function CompanySettings({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/company-settings`);
|
||||
if (response.status === 401) return;
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const d = result.data;
|
||||
setForm({
|
||||
company_name: d.company_name || "",
|
||||
street: d.street || "",
|
||||
city: d.city || "",
|
||||
postal_code: d.postal_code || "",
|
||||
country: d.country || "",
|
||||
company_id: d.company_id || "",
|
||||
vat_id: d.vat_id || "",
|
||||
});
|
||||
const cf =
|
||||
Array.isArray(d.custom_fields) && d.custom_fields.length > 0
|
||||
? d.custom_fields.map(
|
||||
(
|
||||
f: {
|
||||
name: string;
|
||||
value: string;
|
||||
showLabel?: boolean;
|
||||
_key?: string;
|
||||
},
|
||||
i: number,
|
||||
) => ({
|
||||
...f,
|
||||
_key: f._key || `cf-${Date.now()}-${i}`,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
setCustomFields(cf);
|
||||
if (
|
||||
Array.isArray(d.supplier_field_order) &&
|
||||
d.supplier_field_order.length > 0
|
||||
) {
|
||||
setFieldOrder(d.supplier_field_order);
|
||||
} else {
|
||||
setFieldOrder([...DEFAULT_FIELD_ORDER]);
|
||||
}
|
||||
if (
|
||||
Array.isArray(d.available_currencies) &&
|
||||
d.available_currencies.length > 0
|
||||
) {
|
||||
setAvailableCurrencies(d.available_currencies);
|
||||
}
|
||||
if (d.has_logo) {
|
||||
fetchLogo("light");
|
||||
}
|
||||
if (d.has_logo_dark) {
|
||||
fetchLogo("dark");
|
||||
}
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se načíst nastavení");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [alert, fetchLogo]);
|
||||
// ── TanStack Query: company settings ──
|
||||
const { data: settingsData, isPending: settingsLoading } = useQuery(
|
||||
companySettingsOptions(),
|
||||
);
|
||||
|
||||
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);
|
||||
// ── TanStack Query: bank accounts ──
|
||||
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({
|
||||
company_name: (d.company_name as string) || "",
|
||||
street: (d.street as string) || "",
|
||||
city: (d.city as string) || "",
|
||||
postal_code: (d.postal_code as string) || "",
|
||||
country: (d.country as string) || "",
|
||||
company_id: (d.company_id as string) || "",
|
||||
vat_id: (d.vat_id as string) || "",
|
||||
});
|
||||
const cf: CustomField[] =
|
||||
Array.isArray(d.custom_fields) && d.custom_fields.length > 0
|
||||
? (d.custom_fields as CustomField[]).map((f, i) => ({
|
||||
...f,
|
||||
showLabel: f.showLabel !== false,
|
||||
_key: f._key || `cf-${Date.now()}-${i}`,
|
||||
}))
|
||||
: [];
|
||||
setCustomFields(cf);
|
||||
if (
|
||||
Array.isArray(d.supplier_field_order) &&
|
||||
d.supplier_field_order.length > 0
|
||||
) {
|
||||
setFieldOrder(d.supplier_field_order as string[]);
|
||||
} else {
|
||||
setFieldOrder([...DEFAULT_FIELD_ORDER]);
|
||||
}
|
||||
}, []);
|
||||
if (
|
||||
Array.isArray(d.available_currencies) &&
|
||||
d.available_currencies.length > 0
|
||||
) {
|
||||
setAvailableCurrencies(d.available_currencies as string[]);
|
||||
}
|
||||
if (d.has_logo) {
|
||||
fetchLogo("light");
|
||||
}
|
||||
if (d.has_logo_dark) {
|
||||
fetchLogo("dark");
|
||||
}
|
||||
}, [settingsData, fetchLogo]);
|
||||
|
||||
const resetBankForm = () => {
|
||||
setEditingBank(null);
|
||||
@@ -294,7 +276,7 @@ export default function CompanySettings({
|
||||
if (result.success) {
|
||||
alert.success(result.message);
|
||||
resetBankForm();
|
||||
fetchBankAccounts();
|
||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||
} else {
|
||||
alert.error(result.error || "Chyba při ukládání");
|
||||
}
|
||||
@@ -322,7 +304,7 @@ export default function CompanySettings({
|
||||
if (result.success) {
|
||||
alert.success(result.message);
|
||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||
fetchBankAccounts();
|
||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||
} else {
|
||||
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
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -377,6 +354,7 @@ export default function CompanySettings({
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Nastavení bylo uloženo");
|
||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
@@ -411,6 +389,7 @@ export default function CompanySettings({
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Logo bylo nahráno");
|
||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||
fetchLogo(variant);
|
||||
} else {
|
||||
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 (loading) {
|
||||
if (settingsLoading) {
|
||||
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: "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>
|
||||
<Skeleton
|
||||
name="company-settings"
|
||||
loading={settingsLoading}
|
||||
fixture={<CompanySettingsFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -774,18 +718,16 @@ export default function CompanySettings({
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{bankLoading ? (
|
||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line w-1/3" />
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton
|
||||
name="company-settings-bank"
|
||||
loading={bankLoading}
|
||||
fixture={<CompanySettingsFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
) : (
|
||||
<>
|
||||
{bankAccounts.length > 0 && (
|
||||
{bankAccountsList.length > 0 && (
|
||||
<div className="admin-table-responsive mb-4">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
@@ -801,7 +743,7 @@ export default function CompanySettings({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bankAccounts.map((acc) => (
|
||||
{bankAccountsList.map((acc) => (
|
||||
<tr
|
||||
key={acc.id}
|
||||
style={
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import apiFetch from "../utils/api";
|
||||
import { dashboardOptions } from "../lib/queries/dashboard";
|
||||
import { require2FAOptions } from "../lib/queries/settings";
|
||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||
@@ -12,6 +15,8 @@ import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
||||
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
||||
import DashProfile from "../components/dashboard/DashProfile";
|
||||
import DashSessions from "../components/dashboard/DashSessions";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import DashboardFixture from "../fixtures/DashboardFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -69,13 +74,17 @@ export default function Dashboard() {
|
||||
const { user, updateUser, hasPermission } = useAuth();
|
||||
const alert = useAlert();
|
||||
|
||||
const [dashData, setDashData] = useState<DashData | null>(null);
|
||||
const [dashLoading, setDashLoading] = useState(true);
|
||||
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
|
||||
const [totpEnabled, setTotpEnabled] = useState(false);
|
||||
const [totpLoading, setTotpLoading] = useState(true);
|
||||
const [show2FASetup, setShow2FASetup] = useState(false);
|
||||
const [show2FADisable, setShow2FADisable] = useState(false);
|
||||
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
||||
@@ -88,46 +97,6 @@ export default function Dashboard() {
|
||||
useModalLock(show2FASetup);
|
||||
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
|
||||
const handleQuickPunch = useCallback(() => {
|
||||
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
||||
@@ -143,7 +112,7 @@ export default function Dashboard() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
||||
fetchDashboard();
|
||||
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
||||
} else {
|
||||
alert.error(result.error || "Chyba při záznamu docházky");
|
||||
}
|
||||
@@ -167,7 +136,7 @@ export default function Dashboard() {
|
||||
() => submitPunch({}),
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||
);
|
||||
}, [dashData, alert, fetchDashboard]);
|
||||
}, [dashData, alert, queryClient]);
|
||||
|
||||
// 2FA handlery
|
||||
const handleStart2FASetup = async () => {
|
||||
@@ -202,7 +171,7 @@ export default function Dashboard() {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(true);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
setBackupCodes(data.data?.backup_codes || null);
|
||||
setTotpSecret(null);
|
||||
setTotpQrUri(null);
|
||||
@@ -230,7 +199,7 @@ export default function Dashboard() {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
setShow2FADisable(false);
|
||||
setDisableCode("");
|
||||
updateUser({ totpEnabled: false });
|
||||
@@ -337,62 +306,13 @@ export default function Dashboard() {
|
||||
|
||||
{/* Skeleton loading */}
|
||||
{dashLoading && (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.25rem" }}>
|
||||
<div className="admin-kpi-grid admin-kpi-4">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div
|
||||
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
|
||||
className="admin-skeleton-line"
|
||||
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>
|
||||
<Skeleton
|
||||
name="dashboard"
|
||||
loading={dashLoading}
|
||||
fixture={<DashboardFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
lazy,
|
||||
Suspense,
|
||||
} from "react";
|
||||
import { useState, useEffect, useRef, lazy, Suspense } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
@@ -17,8 +11,18 @@ import apiFetch from "../utils/api";
|
||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||
import SortIcon from "../components/SortIcon";
|
||||
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 { Skeleton } from "boneyard-js/react";
|
||||
import InvoicesFixture from "../fixtures/InvoicesFixture";
|
||||
import ReceivedInvoicesFixture from "../fixtures/ReceivedInvoicesFixture";
|
||||
|
||||
const ReceivedInvoices = lazy(() => import("./ReceivedInvoices"));
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -39,11 +43,6 @@ const MONTH_NAMES = [
|
||||
"prosinec",
|
||||
];
|
||||
|
||||
interface CurrencyAmount {
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
function formatMultiCurrency(amounts: CurrencyAmount[]): string {
|
||||
if (!Array.isArray(amounts) || amounts.length === 0) return "0 Kč";
|
||||
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
|
||||
@@ -84,31 +83,6 @@ const STATUS_FILTERS = [
|
||||
{ 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 {
|
||||
form: Record<string, unknown>;
|
||||
items: Record<string, unknown>[];
|
||||
@@ -134,8 +108,6 @@ export default function Invoices() {
|
||||
const now = new Date();
|
||||
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1);
|
||||
const [statsYear, setStatsYear] = useState(now.getFullYear());
|
||||
const [stats, setStats] = useState<InvoiceStats | null>(null);
|
||||
const [statsLoading, setStatsLoading] = useState(true);
|
||||
const hasLoadedOnce = useRef(false);
|
||||
const slideDirection = useRef(0);
|
||||
const blobUrlRef = useRef<string | null>(null);
|
||||
@@ -154,28 +126,15 @@ export default function Invoices() {
|
||||
statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear();
|
||||
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`;
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
try {
|
||||
const res = await apiFetch(
|
||||
`${API_BASE}/invoices/stats?month=${statsMonth}&year=${statsYear}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setStats(data.data);
|
||||
hasLoadedOnce.current = true;
|
||||
setSlideKey((k) => k + 1);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
setStatsLoading(false);
|
||||
}
|
||||
}, [statsMonth, statsYear]);
|
||||
const statsQuery = useQuery(invoiceStatsOptions(statsMonth, statsYear));
|
||||
const stats = statsQuery.data ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [fetchStats]);
|
||||
if (statsQuery.data) {
|
||||
hasLoadedOnce.current = true;
|
||||
setSlideKey((k) => k + 1);
|
||||
}
|
||||
}, [statsQuery.data]);
|
||||
|
||||
const prevMonth = () => {
|
||||
slideDirection.current = -1;
|
||||
@@ -225,24 +184,23 @@ export default function Invoices() {
|
||||
setDraft(null);
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
items: invoices,
|
||||
loading,
|
||||
initialLoad,
|
||||
pagination,
|
||||
refetch: fetchData,
|
||||
} = useListData<Invoice>("invoices", {
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
page,
|
||||
extraParams: {
|
||||
month: String(statsMonth),
|
||||
year: String(statsYear),
|
||||
...(statusFilter ? { status: statusFilter } : {}),
|
||||
},
|
||||
errorMsg: "Nepodařilo se načíst faktury",
|
||||
});
|
||||
isPending: initialLoad,
|
||||
isFetching: loading,
|
||||
} = usePaginatedQuery<Invoice>(
|
||||
invoiceListOptions({
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
page,
|
||||
month: statsMonth,
|
||||
year: statsYear,
|
||||
status: statusFilter || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!hasPermission("invoices.view")) return <Forbidden />;
|
||||
|
||||
@@ -260,8 +218,8 @@ export default function Invoices() {
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, invoice: null });
|
||||
alert.success(result.message || "Faktura byla smazána");
|
||||
fetchData();
|
||||
fetchStats();
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
||||
}
|
||||
@@ -283,8 +241,8 @@ export default function Invoices() {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert.success("Faktura označena jako zaplacená");
|
||||
fetchData();
|
||||
fetchStats();
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se změnit stav");
|
||||
}
|
||||
@@ -323,81 +281,13 @@ export default function Invoices() {
|
||||
|
||||
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-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>
|
||||
<Skeleton
|
||||
name="invoices"
|
||||
loading={initialLoad}
|
||||
fixture={<InvoicesFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,35 +418,13 @@ export default function Invoices() {
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className="admin-kpi-grid admin-kpi-4"
|
||||
style={{ marginBottom: "1.5rem" }}
|
||||
<Skeleton
|
||||
name="invoices-received-kpi"
|
||||
loading={true}
|
||||
fixture={<ReceivedInvoicesFixture />}
|
||||
>
|
||||
{[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 />
|
||||
</Skeleton>
|
||||
}
|
||||
>
|
||||
<ReceivedInvoices
|
||||
@@ -574,36 +442,14 @@ export default function Invoices() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.1 }}
|
||||
>
|
||||
{!hasLoadedOnce.current && statsLoading ? (
|
||||
<div
|
||||
className="admin-kpi-grid admin-kpi-4"
|
||||
style={{ marginBottom: "1.5rem" }}
|
||||
{statsQuery.isPending && !hasLoadedOnce.current ? (
|
||||
<Skeleton
|
||||
name="invoices-kpi"
|
||||
loading={statsQuery.isPending && !hasLoadedOnce.current}
|
||||
fixture={<InvoicesFixture />}
|
||||
>
|
||||
{[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 />
|
||||
</Skeleton>
|
||||
) : (
|
||||
stats && (
|
||||
<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 { useAlert } from "../context/AlertContext";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||
import apiFetch from "../utils/api";
|
||||
import { czechPlural } from "../utils/formatters";
|
||||
import {
|
||||
leavePendingOptions,
|
||||
leaveProcessedOptions,
|
||||
} from "../lib/queries/leave";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import FormField from "../components/FormField";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import LeaveApprovalFixture from "../fixtures/LeaveApprovalFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -101,15 +108,24 @@ function mapLeaveRequest(raw: RawLeaveRequest): LeaveRequest {
|
||||
export default function LeaveApproval() {
|
||||
const { hasPermission } = useAuth();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
||||
"pending",
|
||||
);
|
||||
const [pendingRequests, setPendingRequests] = useState<LeaveRequest[]>([]);
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [processedRequests, setProcessedRequests] = useState<LeaveRequest[]>(
|
||||
[],
|
||||
const { data: pendingData, isPending: loading } = useQuery(
|
||||
leavePendingOptions(),
|
||||
);
|
||||
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<{
|
||||
open: boolean;
|
||||
request: LeaveRequest | null;
|
||||
@@ -123,67 +139,6 @@ export default function LeaveApproval() {
|
||||
|
||||
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 />;
|
||||
|
||||
const handleApprove = async () => {
|
||||
@@ -202,8 +157,7 @@ export default function LeaveApproval() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setApproveModal({ open: false, request: null });
|
||||
await fetchPending();
|
||||
setProcessedRequests([]);
|
||||
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||
alert.success("Žádost byla schválena");
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -240,8 +194,7 @@ export default function LeaveApproval() {
|
||||
if (result.success) {
|
||||
setRejectModal({ open: false, request: null });
|
||||
setRejectNote("");
|
||||
await fetchPending();
|
||||
setProcessedRequests([]);
|
||||
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||
alert.success("Žádost byla zamítnuta");
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -253,402 +206,378 @@ export default function LeaveApproval() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-row"
|
||||
style={{ justifyContent: "space-between" }}
|
||||
return (
|
||||
<Skeleton
|
||||
name="leave-approval"
|
||||
loading={loading}
|
||||
fixture={<LeaveApprovalFixture />}
|
||||
>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="admin-skeleton-line h-8"
|
||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
||||
/>
|
||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
||||
<h1 className="admin-page-title">Schvalování nepřítomnosti</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pendingCount > 0
|
||||
? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení`
|
||||
: "Žádné čekající žádosti"}
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
</motion.div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Schvalování nepřítomnosti</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pendingCount > 0
|
||||
? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení`
|
||||
: "Žádné čekající žádosti"}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Tabs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-tabs mb-6">
|
||||
<button
|
||||
className={`admin-tab ${activeTab === "pending" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("pending")}
|
||||
>
|
||||
Ke schválení
|
||||
{pendingCount > 0 && (
|
||||
<span
|
||||
className="admin-badge badge-pending"
|
||||
style={{
|
||||
marginLeft: "0.5rem",
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
}}
|
||||
>
|
||||
{pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab ${activeTab === "processed" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("processed")}
|
||||
>
|
||||
Vyřízené
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Pending Tab */}
|
||||
{activeTab === "pending" && (
|
||||
{/* Tabs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
{pendingRequests.length === 0 ? (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-empty-state">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-muted mb-4"
|
||||
>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
<p>Žádné čekající žádosti</p>
|
||||
<div className="admin-tabs mb-6">
|
||||
<button
|
||||
className={`admin-tab ${activeTab === "pending" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("pending")}
|
||||
>
|
||||
Ke schválení
|
||||
{pendingCount > 0 && (
|
||||
<span
|
||||
className="admin-badge badge-pending"
|
||||
style={{
|
||||
marginLeft: "0.5rem",
|
||||
fontSize: "0.7rem",
|
||||
padding: "0.15rem 0.5rem",
|
||||
}}
|
||||
>
|
||||
{pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab ${activeTab === "processed" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("processed")}
|
||||
>
|
||||
Vyřízené
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Pending Tab */}
|
||||
{activeTab === "pending" && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
{pendingRequests.length === 0 ? (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-empty-state">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-muted mb-4"
|
||||
>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
<p>Žádné čekající žádosti</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
||||
>
|
||||
{pendingRequests.map((req) => (
|
||||
<div key={req.id} className="admin-card">
|
||||
<div
|
||||
className="admin-card-body"
|
||||
style={{ padding: "1.25rem" }}
|
||||
>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{pendingRequests.map((req) => (
|
||||
<div key={req.id} className="admin-card">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
gap: "1rem",
|
||||
}}
|
||||
className="admin-card-body"
|
||||
style={{ padding: "1.25rem" }}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex-row-gap mb-2">
|
||||
<strong style={{ fontSize: "1rem" }}>
|
||||
{req.employee_name}
|
||||
</strong>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1.5rem",
|
||||
flexWrap: "wrap",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<strong>{formatDate(req.date_from)}</strong> —{" "}
|
||||
<strong>{formatDate(req.date_to)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
{req.total_days}{" "}
|
||||
{czechPlural(req.total_days, "den", "dny", "dnů")} (
|
||||
{req.total_hours}h)
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
Podáno: {formatDatetime(req.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{req.notes && (
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
marginTop: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{req.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
flexShrink: 0,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
setApproveModal({ open: true, request: req })
|
||||
}
|
||||
className="admin-btn admin-btn-sm"
|
||||
<div className="flex-1">
|
||||
<div className="flex-row-gap mb-2">
|
||||
<strong style={{ fontSize: "1rem" }}>
|
||||
{req.employee_name}
|
||||
</strong>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] ||
|
||||
req.leave_type}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1.5rem",
|
||||
flexWrap: "wrap",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<strong>{formatDate(req.date_from)}</strong> —{" "}
|
||||
<strong>{formatDate(req.date_to)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
{req.total_days}{" "}
|
||||
{czechPlural(req.total_days, "den", "dny", "dnů")}{" "}
|
||||
({req.total_hours}h)
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
Podáno: {formatDatetime(req.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{req.notes && (
|
||||
<div
|
||||
className="text-secondary"
|
||||
style={{
|
||||
marginTop: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{req.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "var(--success-light)",
|
||||
color: "var(--success)",
|
||||
border: "none",
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Schválit
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRejectModal({ open: true, request: req })
|
||||
}
|
||||
className="admin-btn admin-btn-sm"
|
||||
style={{
|
||||
background: "var(--danger-light)",
|
||||
color: "var(--danger)",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
Zamítnout
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setApproveModal({ open: true, request: req })
|
||||
}
|
||||
className="admin-btn admin-btn-sm"
|
||||
style={{
|
||||
background: "var(--success-light)",
|
||||
color: "var(--success)",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
Schválit
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRejectModal({ open: true, request: req })
|
||||
}
|
||||
className="admin-btn admin-btn-sm"
|
||||
style={{
|
||||
background: "var(--danger-light)",
|
||||
color: "var(--danger)",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
Zamítnout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Processed Tab */}
|
||||
{activeTab === "processed" && (
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{processedRequests.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<p>Zatím žádné vyřízené žádosti</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zaměstnanec</th>
|
||||
<th>Typ</th>
|
||||
<th>Od</th>
|
||||
<th>Do</th>
|
||||
<th>Dny</th>
|
||||
<th>Stav</th>
|
||||
<th>Schválil</th>
|
||||
<th>Poznámka</th>
|
||||
<th>Vyřízeno</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedRequests.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td>
|
||||
<strong>{req.employee_name}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_from)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_to)}
|
||||
</td>
|
||||
<td className="admin-mono">{req.total_days}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${statusClasses[req.status] || ""}`}
|
||||
>
|
||||
{statusLabels[req.status] || req.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>{req.reviewer_name || "—"}</td>
|
||||
<td style={{ maxWidth: "200px" }}>
|
||||
{req.reviewer_note ? (
|
||||
<span title={req.reviewer_note}>
|
||||
{req.reviewer_note.length > 40
|
||||
? `${req.reviewer_note.substring(0, 40)}...`
|
||||
: req.reviewer_note}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className="admin-mono"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{formatDatetime(req.reviewed_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Approve Confirmation */}
|
||||
<ConfirmModal
|
||||
isOpen={approveModal.open}
|
||||
onClose={() => setApproveModal({ open: false, request: null })}
|
||||
onConfirm={handleApprove}
|
||||
title="Schválit žádost"
|
||||
message={
|
||||
approveModal.request
|
||||
? `Schválit ${approveModal.request.total_days} ${czechPlural(approveModal.request.total_days, "den", "dny", "dnů")} ${leaveTypeLabels[approveModal.request.leave_type]?.toLowerCase() || ""} pro ${approveModal.request.employee_name}?`
|
||||
: ""
|
||||
}
|
||||
confirmText="Schválit"
|
||||
type="info"
|
||||
loading={processing}
|
||||
/>
|
||||
|
||||
{/* Reject Modal */}
|
||||
<AnimatePresence>
|
||||
{rejectModal.open && (
|
||||
<motion.div
|
||||
className="admin-modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
onClick={() => {
|
||||
setRejectModal({ open: false, request: null });
|
||||
setRejectNote("");
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="admin-modal"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="admin-modal-header">
|
||||
<h2 className="admin-modal-title">Zamítnout žádost</h2>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
{rejectModal.request && (
|
||||
<p className="text-secondary mb-4">
|
||||
{rejectModal.request.employee_name} —{" "}
|
||||
{leaveTypeLabels[rejectModal.request.leave_type]},{" "}
|
||||
{formatDate(rejectModal.request.date_from)} —{" "}
|
||||
{formatDate(rejectModal.request.date_to)} (
|
||||
{rejectModal.request.total_days} dnů)
|
||||
</p>
|
||||
)}
|
||||
<FormField label="Důvod zamítnutí" required>
|
||||
<textarea
|
||||
value={rejectNote}
|
||||
onChange={(e) => setRejectNote(e.target.value)}
|
||||
placeholder="Uveďte důvod zamítnutí..."
|
||||
className="admin-form-textarea"
|
||||
rows={3}
|
||||
autoFocus
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="admin-modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRejectModal({ open: false, request: null });
|
||||
setRejectNote("");
|
||||
}}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
disabled={processing}
|
||||
>
|
||||
Zrušit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReject}
|
||||
disabled={processing || !rejectNote.trim()}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
{processing ? "Zpracování..." : "Zamítnout"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Processed Tab */}
|
||||
{activeTab === "processed" && (
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.08 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{processedRequests.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<p>Zatím žádné vyřízené žádosti</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zaměstnanec</th>
|
||||
<th>Typ</th>
|
||||
<th>Od</th>
|
||||
<th>Do</th>
|
||||
<th>Dny</th>
|
||||
<th>Stav</th>
|
||||
<th>Schválil</th>
|
||||
<th>Poznámka</th>
|
||||
<th>Vyřízeno</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedRequests.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td>
|
||||
<strong>{req.employee_name}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] ||
|
||||
req.leave_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_from)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_to)}
|
||||
</td>
|
||||
<td className="admin-mono">{req.total_days}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${statusClasses[req.status] || ""}`}
|
||||
>
|
||||
{statusLabels[req.status] || req.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>{req.reviewer_name || "—"}</td>
|
||||
<td style={{ maxWidth: "200px" }}>
|
||||
{req.reviewer_note ? (
|
||||
<span title={req.reviewer_note}>
|
||||
{req.reviewer_note.length > 40
|
||||
? `${req.reviewer_note.substring(0, 40)}...`
|
||||
: req.reviewer_note}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className="admin-mono"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{formatDatetime(req.reviewed_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Approve Confirmation */}
|
||||
<ConfirmModal
|
||||
isOpen={approveModal.open}
|
||||
onClose={() => setApproveModal({ open: false, request: null })}
|
||||
onConfirm={handleApprove}
|
||||
title="Schválit žádost"
|
||||
message={
|
||||
approveModal.request
|
||||
? `Schválit ${approveModal.request.total_days} ${czechPlural(approveModal.request.total_days, "den", "dny", "dnů")} ${leaveTypeLabels[approveModal.request.leave_type]?.toLowerCase() || ""} pro ${approveModal.request.employee_name}?`
|
||||
: ""
|
||||
}
|
||||
confirmText="Schválit"
|
||||
type="info"
|
||||
loading={processing}
|
||||
/>
|
||||
|
||||
{/* Reject Modal */}
|
||||
<AnimatePresence>
|
||||
{rejectModal.open && (
|
||||
<motion.div
|
||||
className="admin-modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
onClick={() => {
|
||||
setRejectModal({ open: false, request: null });
|
||||
setRejectNote("");
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="admin-modal"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="admin-modal-header">
|
||||
<h2 className="admin-modal-title">Zamítnout žádost</h2>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
{rejectModal.request && (
|
||||
<p className="text-secondary mb-4">
|
||||
{rejectModal.request.employee_name} —{" "}
|
||||
{leaveTypeLabels[rejectModal.request.leave_type]},{" "}
|
||||
{formatDate(rejectModal.request.date_from)} —{" "}
|
||||
{formatDate(rejectModal.request.date_to)} (
|
||||
{rejectModal.request.total_days} dnů)
|
||||
</p>
|
||||
)}
|
||||
<FormField label="Důvod zamítnutí" required>
|
||||
<textarea
|
||||
value={rejectNote}
|
||||
onChange={(e) => setRejectNote(e.target.value)}
|
||||
placeholder="Uveďte důvod zamítnutí..."
|
||||
className="admin-form-textarea"
|
||||
rows={3}
|
||||
autoFocus
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="admin-modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRejectModal({ open: false, request: null });
|
||||
setRejectNote("");
|
||||
}}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
disabled={processing}
|
||||
>
|
||||
Zrušit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReject}
|
||||
disabled={processing || !rejectNote.trim()}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
{processing ? "Zpracování..." : "Zamítnout"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</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 { useAuth } from "../context/AuthContext";
|
||||
import { motion } from "framer-motion";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import LeaveRequestsFixture from "../fixtures/LeaveRequestsFixture";
|
||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||
import apiFetch from "../utils/api";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import { leaveRequestsOptions } from "../lib/queries/leave";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -51,33 +55,16 @@ interface LeaveRequest {
|
||||
export default function LeaveRequests() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [requests, setRequests] = useState<LeaveRequest[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: requests = [], isPending } = useQuery(
|
||||
leaveRequestsOptions(true),
|
||||
) as { data: LeaveRequest[]; isPending: boolean };
|
||||
const [cancelModal, setCancelModal] = useState<{
|
||||
open: boolean;
|
||||
id: number | null;
|
||||
}>({ open: false, id: null });
|
||||
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 />;
|
||||
|
||||
const handleCancel = async () => {
|
||||
@@ -94,7 +81,7 @@ export default function LeaveRequests() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setCancelModal({ open: false, id: null });
|
||||
await fetchRequests();
|
||||
queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
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) {
|
||||
const truncate = (text: string) =>
|
||||
text.length > 40 ? `${text.substring(0, 40)}...` : text;
|
||||
@@ -176,129 +121,139 @@ export default function LeaveRequests() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Moje žádosti</h1>
|
||||
<p className="admin-page-subtitle">Přehled žádostí o nepřítomnost</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Skeleton
|
||||
name="leave-requests"
|
||||
loading={isPending}
|
||||
fixture={<LeaveRequestsFixture />}
|
||||
>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Moje žádosti</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
Přehled žádostí o nepřítomnost
|
||||
</p>
|
||||
</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 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{requests.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<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>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{requests.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<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>
|
||||
<p>Zatím nemáte žádné žádosti</p>
|
||||
<p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
|
||||
Novou žádost můžete podat na stránce Docházka
|
||||
</p>
|
||||
</div>
|
||||
<p>Zatím nemáte žádné žádosti</p>
|
||||
<p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
|
||||
Novou žádost můžete podat na stránce Docházka
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Typ</th>
|
||||
<th>Od</th>
|
||||
<th>Do</th>
|
||||
<th>Dny</th>
|
||||
<th>Hodiny</th>
|
||||
<th>Stav</th>
|
||||
<th>Poznámka</th>
|
||||
<th>Podáno</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requests.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_from)}
|
||||
</td>
|
||||
<td className="admin-mono">{formatDate(req.date_to)}</td>
|
||||
<td className="admin-mono">{req.total_days}</td>
|
||||
<td className="admin-mono">{req.total_hours}h</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${statusClasses[req.status] || ""}`}
|
||||
>
|
||||
{statusLabels[req.status] || req.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ maxWidth: "200px" }}>
|
||||
{renderNoteCell(req)}
|
||||
</td>
|
||||
<td
|
||||
className="admin-mono"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{formatDatetime(req.created_at)}
|
||||
</td>
|
||||
<td>
|
||||
{req.status === "pending" && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setCancelModal({ open: true, id: req.id })
|
||||
}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
>
|
||||
Zrušit
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Typ</th>
|
||||
<th>Od</th>
|
||||
<th>Do</th>
|
||||
<th>Dny</th>
|
||||
<th>Hodiny</th>
|
||||
<th>Stav</th>
|
||||
<th>Poznámka</th>
|
||||
<th>Podáno</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requests.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td>
|
||||
<span
|
||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||
>
|
||||
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_from)}
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(req.date_to)}
|
||||
</td>
|
||||
<td className="admin-mono">{req.total_days}</td>
|
||||
<td className="admin-mono">{req.total_hours}h</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${statusClasses[req.status] || ""}`}
|
||||
>
|
||||
{statusLabels[req.status] || req.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ maxWidth: "200px" }}>
|
||||
{renderNoteCell(req)}
|
||||
</td>
|
||||
<td
|
||||
className="admin-mono"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{formatDatetime(req.created_at)}
|
||||
</td>
|
||||
<td>
|
||||
{req.status === "pending" && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setCancelModal({ open: true, id: req.id })
|
||||
}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
>
|
||||
Zrušit
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={cancelModal.open}
|
||||
onClose={() => setCancelModal({ open: false, id: null })}
|
||||
onConfirm={handleCancel}
|
||||
title="Zrušit žádost"
|
||||
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
||||
confirmText="Zrušit žádost"
|
||||
type="warning"
|
||||
loading={cancelling}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={cancelModal.open}
|
||||
onClose={() => setCancelModal({ open: false, id: null })}
|
||||
onConfirm={handleCancel}
|
||||
title="Zrušit žádost"
|
||||
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
||||
confirmText="Zrušit žádost"
|
||||
type="warning"
|
||||
loading={cancelling}
|
||||
/>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import OrdersFixture from "../fixtures/OrdersFixture";
|
||||
import { motion } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
|
||||
@@ -10,7 +12,9 @@ import apiFetch from "../utils/api";
|
||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||
import SortIcon from "../components/SortIcon";
|
||||
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";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -57,19 +61,13 @@ export default function Orders() {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
items: orders,
|
||||
loading,
|
||||
initialLoad,
|
||||
pagination,
|
||||
refetch: fetchData,
|
||||
} = useListData("orders", {
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
page,
|
||||
errorMsg: "Nepodařilo se načíst objednávky",
|
||||
});
|
||||
isPending,
|
||||
isFetching,
|
||||
} = usePaginatedQuery<Order>(orderListOptions({ search, sort, order, page }));
|
||||
|
||||
if (!hasPermission("orders.view")) return <Forbidden />;
|
||||
|
||||
@@ -90,7 +88,9 @@ export default function Orders() {
|
||||
setDeleteConfirm({ show: false, order: null });
|
||||
setDeleteFiles(false);
|
||||
alert.success(result.message || "Objednávka byla smazána");
|
||||
fetchData();
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
||||
}
|
||||
@@ -101,216 +101,155 @@ 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 (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Objednávky</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pagination?.total ?? orders.length}{" "}
|
||||
{czechPlural(
|
||||
pagination?.total ?? orders.length,
|
||||
"objednávka",
|
||||
"objednávky",
|
||||
"objednávek",
|
||||
)}
|
||||
</p>
|
||||
</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={{ opacity: loading ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-search-bar mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
|
||||
/>
|
||||
<Skeleton name="orders" loading={isPending} fixture={<OrdersFixture />}>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Objednávky</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pagination?.total ?? orders.length}{" "}
|
||||
{czechPlural(
|
||||
pagination?.total ?? orders.length,
|
||||
"objednávka",
|
||||
"objednávky",
|
||||
"objednávek",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{orders.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<path d="M16 10a4 4 0 0 1-8 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Zatím nejsou žádné objednávky.</p>
|
||||
<p className="text-tertiary" style={{ fontSize: "0.875rem" }}>
|
||||
Objednávky se vytvářejí z nabídek.
|
||||
</p>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-search-bar mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("order_number")}
|
||||
>
|
||||
Číslo{" "}
|
||||
<SortIcon
|
||||
column="order_number"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Nabídka</th>
|
||||
<th>Zákazník</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
Stav{" "}
|
||||
<SortIcon
|
||||
column="status"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("created_at")}
|
||||
>
|
||||
Datum{" "}
|
||||
<SortIcon
|
||||
column="created_at"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th className="text-right">Celkem</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(orders as Order[]).map((o) => (
|
||||
<tr key={o.id}>
|
||||
<td className="admin-mono">
|
||||
<Link to={`/orders/${o.id}`} className="link-accent">
|
||||
{o.order_number}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
to={`/offers/${o.quotation_id}`}
|
||||
className="text-secondary"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
{o.quotation_number}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{o.customer_name || "—"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${STATUS_CLASSES[o.status] || ""}`}
|
||||
>
|
||||
{STATUS_LABELS[o.status] || o.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">{formatDate(o.created_at)}</td>
|
||||
<td className="admin-mono text-right fw-500">
|
||||
{formatCurrency(o.total, o.currency)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<Link
|
||||
to={`/orders/${o.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Detail"
|
||||
aria-label="Detail"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
||||
{orders.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<path d="M16 10a4 4 0 0 1-8 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Zatím nejsou žádné objednávky.</p>
|
||||
<p className="text-tertiary" style={{ fontSize: "0.875rem" }}>
|
||||
Objednávky se vytvářejí z nabídek.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("order_number")}
|
||||
>
|
||||
Číslo{" "}
|
||||
<SortIcon
|
||||
column="order_number"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Nabídka</th>
|
||||
<th>Zákazník</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
Stav{" "}
|
||||
<SortIcon
|
||||
column="status"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("created_at")}
|
||||
>
|
||||
Datum{" "}
|
||||
<SortIcon
|
||||
column="created_at"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th className="text-right">Celkem</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(orders as Order[]).map((o) => (
|
||||
<tr key={o.id}>
|
||||
<td className="admin-mono">
|
||||
<Link to={`/orders/${o.id}`} className="link-accent">
|
||||
{o.order_number}
|
||||
</Link>
|
||||
{o.invoice_id ? (
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
to={`/offers/${o.quotation_id}`}
|
||||
className="text-secondary"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
{o.quotation_number}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{o.customer_name || "—"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${STATUS_CLASSES[o.status] || ""}`}
|
||||
>
|
||||
{STATUS_LABELS[o.status] || o.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(o.created_at)}
|
||||
</td>
|
||||
<td className="admin-mono text-right fw-500">
|
||||
{formatCurrency(o.total, o.currency)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<Link
|
||||
to={`/invoices/${o.invoice_id}`}
|
||||
className="admin-btn-icon accent"
|
||||
title="Zobrazit fakturu"
|
||||
aria-label="Zobrazit fakturu"
|
||||
to={`/orders/${o.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Detail"
|
||||
aria-label="Detail"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
@@ -320,28 +259,16 @@ export default function Orders() {
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<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" />
|
||||
<text
|
||||
x="12"
|
||||
y="16.5"
|
||||
textAnchor="middle"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
fontSize="9"
|
||||
fontWeight="700"
|
||||
>
|
||||
F
|
||||
</text>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</Link>
|
||||
) : (
|
||||
hasPermission("invoices.create") && (
|
||||
{o.invoice_id ? (
|
||||
<Link
|
||||
to={`/invoices/new?fromOrder=${o.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Vytvořit fakturu"
|
||||
aria-label="Vytvořit fakturu"
|
||||
to={`/invoices/${o.invoice_id}`}
|
||||
className="admin-btn-icon accent"
|
||||
title="Zobrazit fakturu"
|
||||
aria-label="Zobrazit fakturu"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
@@ -353,76 +280,108 @@ export default function Orders() {
|
||||
>
|
||||
<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" />
|
||||
<line x1="12" y1="11" x2="12" y2="17" />
|
||||
<line x1="9" y1="14" x2="15" y2="14" />
|
||||
<text
|
||||
x="12"
|
||||
y="16.5"
|
||||
textAnchor="middle"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
fontSize="9"
|
||||
fontWeight="700"
|
||||
>
|
||||
F
|
||||
</text>
|
||||
</svg>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
{hasPermission("orders.delete") && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteConfirm({ show: true, order: o })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
) : (
|
||||
hasPermission("invoices.create") && (
|
||||
<Link
|
||||
to={`/invoices/new?fromOrder=${o.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Vytvořit fakturu"
|
||||
aria-label="Vytvořit fakturu"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<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" />
|
||||
<line x1="12" y1="11" x2="12" y2="17" />
|
||||
<line x1="9" y1="14" x2="15" y2="14" />
|
||||
</svg>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
{hasPermission("orders.delete") && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteConfirm({ show: true, order: o })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||
</div>
|
||||
</motion.div>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteConfirm.show}
|
||||
onClose={() => {
|
||||
setDeleteConfirm({ show: false, order: null });
|
||||
setDeleteFiles(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat objednávku"
|
||||
message={
|
||||
<>
|
||||
Opravdu chcete smazat objednávku "
|
||||
{deleteConfirm.order?.order_number}"? Bude smazán i přidružený
|
||||
projekt. Tato akce je nevratná.
|
||||
<label
|
||||
className="admin-form-checkbox"
|
||||
style={{ marginTop: "1rem", display: "flex" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteFiles}
|
||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||
/>
|
||||
<span>Smazat i soubory projektu na disku</span>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
confirmText="Smazat"
|
||||
cancelText="Zrušit"
|
||||
type="danger"
|
||||
loading={deleting}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={deleteConfirm.show}
|
||||
onClose={() => {
|
||||
setDeleteConfirm({ show: false, order: null });
|
||||
setDeleteFiles(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat objednávku"
|
||||
message={
|
||||
<>
|
||||
Opravdu chcete smazat objednávku "
|
||||
{deleteConfirm.order?.order_number}"? Bude smazán i
|
||||
přidružený projekt. Tato akce je nevratná.
|
||||
<label
|
||||
className="admin-form-checkbox"
|
||||
style={{ marginTop: "1rem", display: "flex" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteFiles}
|
||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||
/>
|
||||
<span>Smazat i soubory projektu na disku</span>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
confirmText="Smazat"
|
||||
cancelText="Zrušit"
|
||||
type="danger"
|
||||
loading={deleting}
|
||||
/>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import ProjectsFixture from "../fixtures/ProjectsFixture";
|
||||
import { motion } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
|
||||
@@ -10,7 +12,9 @@ import apiFetch from "../utils/api";
|
||||
import { formatDate, czechPlural } from "../utils/formatters";
|
||||
import SortIcon from "../components/SortIcon";
|
||||
import useTableSort from "../hooks/useTableSort";
|
||||
import useListData from "../hooks/useListData";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||
import { projectListOptions } from "../lib/queries/projects";
|
||||
import Pagination from "../components/Pagination";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
@@ -52,19 +56,15 @@ export default function Projects() {
|
||||
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
items: projects,
|
||||
setItems: setProjects,
|
||||
loading,
|
||||
initialLoad,
|
||||
pagination,
|
||||
} = useListData<Project>("projects", {
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
page,
|
||||
errorMsg: "Nepodařilo se načíst projekty",
|
||||
});
|
||||
isPending,
|
||||
isFetching,
|
||||
} = usePaginatedQuery<Project>(
|
||||
projectListOptions({ search, sort, order, page }),
|
||||
);
|
||||
|
||||
if (!hasPermission("projects.view")) return <Forbidden />;
|
||||
|
||||
@@ -80,9 +80,9 @@ export default function Projects() {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert.success(data.message || "Projekt byl smazán");
|
||||
setProjects((prev: Project[]) =>
|
||||
prev.filter((p) => p.id !== deleteTarget.id),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se smazat projekt");
|
||||
}
|
||||
@@ -95,298 +95,268 @@ export default function Projects() {
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Projekty</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pagination?.total ?? projects.length}{" "}
|
||||
{czechPlural(
|
||||
pagination?.total ?? projects.length,
|
||||
"projekt",
|
||||
"projekty",
|
||||
"projektů",
|
||||
)}
|
||||
</p>
|
||||
</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={{ opacity: loading ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-search-bar mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat podle čísla, názvu nebo zákazníka..."
|
||||
/>
|
||||
<Skeleton name="projects" loading={isPending} fixture={<ProjectsFixture />}>
|
||||
<div>
|
||||
<motion.div
|
||||
className="admin-page-header"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="admin-page-title">Projekty</h1>
|
||||
<p className="admin-page-subtitle">
|
||||
{pagination?.total ?? projects.length}{" "}
|
||||
{czechPlural(
|
||||
pagination?.total ?? projects.length,
|
||||
"projekt",
|
||||
"projekty",
|
||||
"projektů",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Zatím nejsou žádné projekty.</p>
|
||||
<p
|
||||
style={{ color: "var(--text-tertiary)", fontSize: "0.875rem" }}
|
||||
>
|
||||
Vytvořte první projekt tlačítkem výše nebo automaticky při
|
||||
vytvoření objednávky.
|
||||
</p>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-search-bar mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat podle čísla, názvu nebo zákazníka..."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("project_number")}
|
||||
>
|
||||
Číslo{" "}
|
||||
<SortIcon
|
||||
column="project_number"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
Název{" "}
|
||||
<SortIcon column="name" sort={activeSort} order={order} />
|
||||
</th>
|
||||
<th>Zákazník</th>
|
||||
<th>Zodpovědná osoba</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
Stav{" "}
|
||||
<SortIcon
|
||||
column="status"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("start_date")}
|
||||
>
|
||||
Začátek{" "}
|
||||
<SortIcon
|
||||
column="start_date"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("end_date")}
|
||||
>
|
||||
Konec{" "}
|
||||
<SortIcon
|
||||
column="end_date"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Objednávka</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(projects as Project[]).map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td className="admin-mono">
|
||||
<Link to={`/projects/${p.id}`} className="link-accent">
|
||||
{p.project_number}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="fw-500">{p.name || "—"}</td>
|
||||
<td>{p.customer_name || "—"}</td>
|
||||
<td>{p.responsible_user_name || "—"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${STATUS_CLASSES[p.status] || ""}`}
|
||||
>
|
||||
{STATUS_LABELS[p.status] || p.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
||||
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
||||
<td>
|
||||
{p.order_id ? (
|
||||
<Link
|
||||
to={`/orders/${p.order_id}`}
|
||||
className="text-secondary"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
{p.order_number}
|
||||
</Link>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Zatím nejsou žádné projekty.</p>
|
||||
<p
|
||||
style={{
|
||||
color: "var(--text-tertiary)",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
Vytvořte první projekt tlačítkem výše nebo automaticky při
|
||||
vytvoření objednávky.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("project_number")}
|
||||
>
|
||||
Číslo{" "}
|
||||
<SortIcon
|
||||
column="project_number"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
Název{" "}
|
||||
<SortIcon
|
||||
column="name"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Zákazník</th>
|
||||
<th>Zodpovědná osoba</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
Stav{" "}
|
||||
<SortIcon
|
||||
column="status"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("start_date")}
|
||||
>
|
||||
Začátek{" "}
|
||||
<SortIcon
|
||||
column="start_date"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("end_date")}
|
||||
>
|
||||
Konec{" "}
|
||||
<SortIcon
|
||||
column="end_date"
|
||||
sort={activeSort}
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Objednávka</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(projects as Project[]).map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td className="admin-mono">
|
||||
<Link
|
||||
to={`/projects/${p.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
className="link-accent"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
{p.project_number}
|
||||
</Link>
|
||||
{!p.order_id && hasPermission("projects.create") && (
|
||||
<button
|
||||
onClick={() => setDeleteTarget(p)}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat projekt"
|
||||
disabled={deletingId === p.id}
|
||||
</td>
|
||||
<td className="fw-500">{p.name || "—"}</td>
|
||||
<td>{p.customer_name || "—"}</td>
|
||||
<td>{p.responsible_user_name || "—"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${STATUS_CLASSES[p.status] || ""}`}
|
||||
>
|
||||
{STATUS_LABELS[p.status] || p.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
{formatDate(p.start_date)}
|
||||
</td>
|
||||
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
||||
<td>
|
||||
{p.order_id ? (
|
||||
<Link
|
||||
to={`/orders/${p.order_id}`}
|
||||
className="text-secondary"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
{deletingId === p.id ? (
|
||||
<div className="admin-spinner admin-spinner-sm" />
|
||||
) : (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{p.order_number}
|
||||
</Link>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<Link
|
||||
to={`/projects/${p.id}`}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</Link>
|
||||
{!p.order_id &&
|
||||
hasPermission("projects.create") && (
|
||||
<button
|
||||
onClick={() => setDeleteTarget(p)}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat projekt"
|
||||
disabled={deletingId === p.id}
|
||||
>
|
||||
{deletingId === p.id ? (
|
||||
<div className="admin-spinner admin-spinner-sm" />
|
||||
) : (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTarget}
|
||||
onClose={() => {
|
||||
setDeleteTarget(null);
|
||||
setDeleteFiles(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat projekt"
|
||||
message={
|
||||
<>
|
||||
Opravdu chcete smazat projekt {deleteTarget?.project_number}?
|
||||
<label
|
||||
className="admin-form-checkbox"
|
||||
style={{ marginTop: "1rem", display: "flex" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteFiles}
|
||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||
/>
|
||||
<span>Smazat i soubory na disku</span>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
confirmText="Smazat"
|
||||
type="danger"
|
||||
loading={!!deletingId}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTarget}
|
||||
onClose={() => {
|
||||
setDeleteTarget(null);
|
||||
setDeleteFiles(false);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
title="Smazat projekt"
|
||||
message={
|
||||
<>
|
||||
Opravdu chcete smazat projekt {deleteTarget?.project_number}?
|
||||
<label
|
||||
className="admin-form-checkbox"
|
||||
style={{ marginTop: "1rem", display: "flex" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteFiles}
|
||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||
/>
|
||||
<span>Smazat i soubory na disku</span>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
confirmText="Smazat"
|
||||
type="danger"
|
||||
loading={!!deletingId}
|
||||
/>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { useAuth } from "../context/AuthContext";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
@@ -10,6 +11,14 @@ import SortIcon from "../components/SortIcon";
|
||||
import useTableSort from "../hooks/useTableSort";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
import { supplierListOptions } from "../lib/queries/common";
|
||||
import {
|
||||
receivedInvoiceListOptions,
|
||||
receivedInvoiceStatsOptions,
|
||||
} from "../lib/queries/invoices";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import ReceivedInvoicesFixture from "../fixtures/ReceivedInvoicesFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -131,7 +140,7 @@ interface CompanySettings {
|
||||
available_vat_rates: number[];
|
||||
}
|
||||
|
||||
function emptyMeta(settings: CompanySettings | null): UploadMeta {
|
||||
function emptyMeta(settings: CompanySettings | null | undefined): UploadMeta {
|
||||
return {
|
||||
supplier_name: "",
|
||||
invoice_number: "",
|
||||
@@ -155,10 +164,16 @@ export default function ReceivedInvoices({
|
||||
const { sort, order, handleSort, activeSort } = useTableSort("created_at");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [invoices, setInvoices] = useState<ReceivedInvoice[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState<ReceivedStats | null>(null);
|
||||
const [statsLoading, setStatsLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editInvoice, setEditInvoice] = useState<EditInvoice | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
show: boolean;
|
||||
invoice: ReceivedInvoice | null;
|
||||
}>({ show: false, invoice: null });
|
||||
const hasLoadedOnce = useRef(false);
|
||||
const slideDirection = useRef(0);
|
||||
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
@@ -166,18 +181,42 @@ export default function ReceivedInvoices({
|
||||
const prevMonth = useRef(statsMonth);
|
||||
const prevYear = useRef(statsYear);
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editInvoice, setEditInvoice] = useState<EditInvoice | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
show: boolean;
|
||||
invoice: ReceivedInvoice | null;
|
||||
}>({ show: false, invoice: null });
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { data: supplierNames = [] } = useQuery(supplierListOptions());
|
||||
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||
| CompanySettings
|
||||
| undefined;
|
||||
|
||||
const [supplierNames, setSupplierNames] = useState<string[]>([]);
|
||||
const [companySettings, setCompanySettings] =
|
||||
useState<CompanySettings | null>(null);
|
||||
// List query — auto-refetches when filters change
|
||||
const listQuery = useQuery(
|
||||
receivedInvoiceListOptions({
|
||||
month: statsMonth,
|
||||
year: statsYear,
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
}),
|
||||
);
|
||||
|
||||
// Stats query — auto-refetches when month/year change
|
||||
const statsQuery = useQuery(
|
||||
receivedInvoiceStatsOptions(statsMonth, statsYear),
|
||||
);
|
||||
|
||||
// Derive list data from query (paginatedJsonQuery returns { data, pagination })
|
||||
const invoices = (listQuery.data?.data ?? []) as ReceivedInvoice[];
|
||||
if (listQuery.data || statsQuery.data) hasLoadedOnce.current = true;
|
||||
|
||||
// Derive stats from query
|
||||
const stats = (statsQuery.data as unknown as ReceivedStats) ?? null;
|
||||
|
||||
// Trigger slide animation when stats data changes
|
||||
useEffect(() => {
|
||||
if (statsQuery.data) {
|
||||
setSlideKey((k) => k + 1);
|
||||
}
|
||||
}, [statsQuery.data]);
|
||||
|
||||
const showListSkeleton = listQuery.isPending && !hasLoadedOnce.current;
|
||||
|
||||
const [uploadFiles, setUploadFiles] = useState<File[]>([]);
|
||||
const [uploadMeta, setUploadMeta] = useState<UploadMeta[]>([]);
|
||||
@@ -201,57 +240,6 @@ export default function ReceivedInvoices({
|
||||
prevMonth.current = statsMonth;
|
||||
prevYear.current = statsYear;
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
if (!hasLoadedOnce.current) setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
month: String(statsMonth),
|
||||
year: String(statsYear),
|
||||
});
|
||||
if (search) {
|
||||
params.set("search", search);
|
||||
}
|
||||
if (sort) {
|
||||
params.set("sort", sort);
|
||||
}
|
||||
if (order) {
|
||||
params.set("order", order);
|
||||
}
|
||||
const res = await apiFetch(`${API_BASE}/received-invoices?${params}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setInvoices(Array.isArray(data.data) ? data.data : []);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
setLoading(false);
|
||||
hasLoadedOnce.current = true;
|
||||
}
|
||||
}, [statsMonth, statsYear, search, sort, order]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch(`${API_BASE}/received-invoices/suppliers`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setSupplierNames(d.data || []);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch(`${API_BASE}/company-settings`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setCompanySettings(d.data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const currencyOptions =
|
||||
companySettings?.available_currencies || DEFAULT_CURRENCIES;
|
||||
const vatRateOptions =
|
||||
@@ -259,45 +247,6 @@ export default function ReceivedInvoices({
|
||||
const defaultCurrency = companySettings?.default_currency || "CZK";
|
||||
const defaultVatRate = String(companySettings?.default_vat_rate ?? 21);
|
||||
|
||||
// Fetch stats (silent refresh without animation)
|
||||
const refreshStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiFetch(
|
||||
`${API_BASE}/received-invoices/stats?month=${statsMonth}&year=${statsYear}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setStats(data.data);
|
||||
hasLoadedOnce.current = true;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [statsMonth, statsYear]);
|
||||
|
||||
// Fetch stats on month change (with slide animation)
|
||||
useEffect(() => {
|
||||
setStatsLoading(true);
|
||||
const load = async () => {
|
||||
try {
|
||||
const res = await apiFetch(
|
||||
`${API_BASE}/received-invoices/stats?month=${statsMonth}&year=${statsYear}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setStats(data.data);
|
||||
hasLoadedOnce.current = true;
|
||||
setSlideKey((k) => k + 1);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
setStatsLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [statsMonth, statsYear]);
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selected = Array.from(e.target.files || []);
|
||||
if (selected.length === 0) {
|
||||
@@ -395,8 +344,9 @@ export default function ReceivedInvoices({
|
||||
setUploadFiles([]);
|
||||
setUploadMeta([]);
|
||||
setUploadErrors({});
|
||||
fetchList();
|
||||
refreshStats();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["invoices", "received"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Chyba při nahrávání");
|
||||
}
|
||||
@@ -465,8 +415,9 @@ export default function ReceivedInvoices({
|
||||
alert.success(data.message || "Faktura byla aktualizována");
|
||||
setEditOpen(false);
|
||||
setEditInvoice(null);
|
||||
fetchList();
|
||||
refreshStats();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["invoices", "received"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Chyba při ukládání");
|
||||
}
|
||||
@@ -493,8 +444,9 @@ export default function ReceivedInvoices({
|
||||
if (data.success) {
|
||||
alert.success(data.message || "Faktura byla smazána");
|
||||
setDeleteConfirm({ show: false, invoice: null });
|
||||
fetchList();
|
||||
refreshStats();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["invoices", "received"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Chyba při mazání");
|
||||
}
|
||||
@@ -538,8 +490,9 @@ export default function ReceivedInvoices({
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
alert.success("Faktura označena jako uhrazená");
|
||||
fetchList();
|
||||
refreshStats();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["invoices", "received"],
|
||||
});
|
||||
} else {
|
||||
alert.error(data.error || "Nepodařilo se změnit stav");
|
||||
}
|
||||
@@ -551,26 +504,15 @@ export default function ReceivedInvoices({
|
||||
const monthLabel = `${MONTH_NAMES[statsMonth - 1]}`;
|
||||
|
||||
const renderKpi = () => {
|
||||
if (!hasLoadedOnce.current && statsLoading) {
|
||||
if (statsQuery.isPending && !hasLoadedOnce.current) {
|
||||
return (
|
||||
<div className="admin-kpi-grid admin-kpi-4 mb-6">
|
||||
{[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>
|
||||
<Skeleton
|
||||
name="received-invoices-kpi"
|
||||
loading={statsQuery.isPending && !hasLoadedOnce.current}
|
||||
fixture={<ReceivedInvoicesFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
if (!stats) {
|
||||
@@ -680,18 +622,16 @@ export default function ReceivedInvoices({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<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 w-1/4" />
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
<div className="admin-skeleton-line w-1/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showListSkeleton && (
|
||||
<Skeleton
|
||||
name="received-invoices-list"
|
||||
loading={showListSkeleton}
|
||||
fixture={<ReceivedInvoicesFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
)}
|
||||
{!loading && invoices.length === 0 && (
|
||||
{!showListSkeleton && invoices.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<div className="admin-empty-icon">
|
||||
<svg
|
||||
@@ -723,7 +663,7 @@ export default function ReceivedInvoices({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!loading && invoices.length > 0 && (
|
||||
{!showListSkeleton && invoices.length > 0 && (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useNavigate, Navigate, useSearchParams } from "react-router-dom";
|
||||
import { Navigate, useSearchParams } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import FormField from "../components/FormField";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import CompanySettings from "./CompanySettings";
|
||||
import {
|
||||
companySettingsOptions,
|
||||
systemInfoOptions,
|
||||
require2FAOptions,
|
||||
} from "../lib/queries/settings";
|
||||
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import SettingsFixture from "../fixtures/SettingsFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface SystemSettingsData {
|
||||
@@ -70,20 +78,108 @@ interface RoleForm {
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SYS_FORM: Omit<SystemSettingsData, "app_version"> = {
|
||||
break_threshold_hours: 6,
|
||||
break_duration_short: 15,
|
||||
break_duration_long: 30,
|
||||
clock_rounding_minutes: 15,
|
||||
invoice_alert_email: "",
|
||||
leave_notify_email: "",
|
||||
smtp_from: "",
|
||||
smtp_from_name: "",
|
||||
max_login_attempts: 5,
|
||||
lockout_minutes: 15,
|
||||
max_requests_per_minute: 300,
|
||||
default_currency: "CZK",
|
||||
default_vat_rate: 21,
|
||||
available_vat_rates: [0, 10, 12, 15, 21],
|
||||
available_currencies: ["CZK", "EUR", "USD", "GBP"],
|
||||
quotation_prefix: "NA",
|
||||
order_type_code: "71",
|
||||
invoice_type_code: "81",
|
||||
offer_number_pattern: "{YYYY}/{PREFIX}/{NNN}",
|
||||
order_number_pattern: "{YY}{CODE}{NNNN}",
|
||||
invoice_number_pattern: "{YY}{CODE}{NNNN}",
|
||||
};
|
||||
|
||||
export default function Settings() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [users, setUsers] = useState<{ role_id: number }[]>([]);
|
||||
const [, setAllPermissions] = useState<Permission[]>([]);
|
||||
const [permissionGroups, setPermissionGroups] = useState<
|
||||
Record<string, Permission[]>
|
||||
>({});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [require2FA, setRequire2FA] = useState(false);
|
||||
const [require2FALoading, setRequire2FALoading] = useState(true);
|
||||
const canManage = hasPermission("settings.manage");
|
||||
|
||||
// ── TanStack Query: roles, permissions, users ──
|
||||
const { data: rolesData, isPending: rolesLoading } = useQuery({
|
||||
queryKey: ["roles"],
|
||||
queryFn: async () => {
|
||||
const [rolesRes, permsRes, usersRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/roles`),
|
||||
apiFetch(`${API_BASE}/roles/permissions`),
|
||||
apiFetch(`${API_BASE}/users`),
|
||||
]);
|
||||
const rolesResult = await rolesRes.json();
|
||||
const permsResult = await permsRes.json();
|
||||
const usersResult = await usersRes.json();
|
||||
if (!rolesResult.success)
|
||||
throw new Error(rolesResult.error || "Nepodařilo se načíst role");
|
||||
if (!permsResult.success)
|
||||
throw new Error(permsResult.error || "Nepodařilo se načíst oprávnění");
|
||||
if (!usersResult.success)
|
||||
throw new Error(usersResult.error || "Nepodařilo se načíst uživatele");
|
||||
return {
|
||||
roles: Array.isArray(rolesResult.data) ? rolesResult.data : [],
|
||||
permissions: Array.isArray(permsResult.data) ? permsResult.data : [],
|
||||
users: Array.isArray(usersResult.data) ? usersResult.data : [],
|
||||
};
|
||||
},
|
||||
enabled: canManage,
|
||||
});
|
||||
|
||||
const roles = rolesData?.roles ?? [];
|
||||
const users = rolesData?.users ?? [];
|
||||
|
||||
// Group permissions by module
|
||||
const permissionGroups = useMemo<Record<string, Permission[]>>(() => {
|
||||
const perms: Permission[] = rolesData?.permissions ?? [];
|
||||
const groups: Record<string, Permission[]> = {};
|
||||
for (const p of perms) {
|
||||
const mod = p.name.split(".")[0] || "other";
|
||||
if (!groups[mod]) groups[mod] = [];
|
||||
groups[mod].push(p);
|
||||
}
|
||||
return groups;
|
||||
}, [rolesData?.permissions]);
|
||||
|
||||
// ── TanStack Query: 2FA required ──
|
||||
const { data: totpData, isPending: require2FALoading } =
|
||||
useQuery(require2FAOptions());
|
||||
const require2FA = totpData?.require_2fa ?? false;
|
||||
|
||||
// ── TanStack Query: system settings (lazy, only on system/security tab) ──
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tabParam = searchParams.get("tab");
|
||||
const activeTab = (
|
||||
tabParam === "system"
|
||||
? "system"
|
||||
: tabParam === "firma"
|
||||
? "firma"
|
||||
: "security"
|
||||
) as "security" | "system" | "firma";
|
||||
const setActiveTab = (tab: "security" | "system" | "firma") =>
|
||||
setSearchParams({ tab }, { replace: true });
|
||||
|
||||
const { data: sysSettingsData, isPending: sysSettingsLoading } = useQuery({
|
||||
...companySettingsOptions(),
|
||||
enabled: canManage && (activeTab === "system" || activeTab === "security"),
|
||||
});
|
||||
|
||||
const { data: systemInfo } = useQuery({
|
||||
...systemInfoOptions(),
|
||||
enabled: canManage && activeTab === "system",
|
||||
});
|
||||
|
||||
// ── Local state ──
|
||||
const [require2FASaving, setRequire2FASaving] = useState(false);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -102,194 +198,56 @@ export default function Settings() {
|
||||
}>({ show: false, role: null });
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tabParam = searchParams.get("tab");
|
||||
const activeTab = (
|
||||
tabParam === "system"
|
||||
? "system"
|
||||
: tabParam === "firma"
|
||||
? "firma"
|
||||
: "security"
|
||||
) as "security" | "system" | "firma";
|
||||
const setActiveTab = (tab: "security" | "system" | "firma") =>
|
||||
setSearchParams({ tab }, { replace: true });
|
||||
|
||||
const [sysSettings, setSysSettings] = useState<SystemSettingsData | null>(
|
||||
null,
|
||||
);
|
||||
const [sysSettingsLoading, setSysSettingsLoading] = useState(false);
|
||||
const [sysSettingsSaving, setSysSettingsSaving] = useState(false);
|
||||
const [systemInfo, setSystemInfo] = useState<Record<string, any> | null>(
|
||||
null,
|
||||
);
|
||||
const [sysForm, setSysForm] = useState<
|
||||
Omit<SystemSettingsData, "app_version">
|
||||
>({
|
||||
break_threshold_hours: 6,
|
||||
break_duration_short: 15,
|
||||
break_duration_long: 30,
|
||||
clock_rounding_minutes: 15,
|
||||
invoice_alert_email: "",
|
||||
leave_notify_email: "",
|
||||
smtp_from: "",
|
||||
smtp_from_name: "",
|
||||
max_login_attempts: 5,
|
||||
lockout_minutes: 15,
|
||||
max_requests_per_minute: 300,
|
||||
default_currency: "CZK",
|
||||
default_vat_rate: 21,
|
||||
available_vat_rates: [0, 10, 12, 15, 21],
|
||||
available_currencies: ["CZK", "EUR", "USD", "GBP"],
|
||||
quotation_prefix: "NA",
|
||||
order_type_code: "71",
|
||||
invoice_type_code: "81",
|
||||
offer_number_pattern: "{YYYY}/{PREFIX}/{NNN}",
|
||||
order_number_pattern: "{YY}{CODE}{NNNN}",
|
||||
invoice_number_pattern: "{YY}{CODE}{NNNN}",
|
||||
});
|
||||
const [sysForm, setSysForm] = useState(DEFAULT_SYS_FORM);
|
||||
const [sysFormInitialized, setSysFormInitialized] = useState(false);
|
||||
|
||||
const canManage = hasPermission("settings.manage");
|
||||
|
||||
if (!canManage) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
// ── Populate sysForm from query data ──
|
||||
useEffect(() => {
|
||||
if (!sysSettingsData || sysFormInitialized) return;
|
||||
const d = sysSettingsData as Record<string, unknown>;
|
||||
setSysForm({
|
||||
break_threshold_hours: (d.break_threshold_hours as number) ?? 6,
|
||||
break_duration_short: (d.break_duration_short as number) ?? 15,
|
||||
break_duration_long: (d.break_duration_long as number) ?? 30,
|
||||
clock_rounding_minutes: (d.clock_rounding_minutes as number) ?? 15,
|
||||
invoice_alert_email: (d.invoice_alert_email as string) || "",
|
||||
leave_notify_email: (d.leave_notify_email as string) || "",
|
||||
smtp_from: (d.smtp_from as string) || "",
|
||||
smtp_from_name: (d.smtp_from_name as string) || "",
|
||||
max_login_attempts: (d.max_login_attempts as number) ?? 5,
|
||||
lockout_minutes: (d.lockout_minutes as number) ?? 15,
|
||||
max_requests_per_minute: (d.max_requests_per_minute as number) ?? 300,
|
||||
default_currency: (d.default_currency as string) || "CZK",
|
||||
default_vat_rate: (d.default_vat_rate as number) ?? 21,
|
||||
available_vat_rates:
|
||||
Array.isArray(d.available_vat_rates) && d.available_vat_rates.length > 0
|
||||
? (d.available_vat_rates as number[])
|
||||
: [0, 10, 12, 15, 21],
|
||||
available_currencies:
|
||||
Array.isArray(d.available_currencies) &&
|
||||
d.available_currencies.length > 0
|
||||
? (d.available_currencies as string[])
|
||||
: ["CZK", "EUR", "USD", "GBP"],
|
||||
quotation_prefix: (d.quotation_prefix as string) || "NA",
|
||||
order_type_code: (d.order_type_code as string) || "71",
|
||||
invoice_type_code: (d.invoice_type_code as string) || "81",
|
||||
offer_number_pattern:
|
||||
(d.offer_number_pattern as string) || "{YYYY}/{PREFIX}/{NNN}",
|
||||
order_number_pattern:
|
||||
(d.order_number_pattern as string) || "{YY}{CODE}{NNNN}",
|
||||
invoice_number_pattern:
|
||||
(d.invoice_number_pattern as string) || "{YY}{CODE}{NNNN}",
|
||||
});
|
||||
setSysFormInitialized(true);
|
||||
}, [sysSettingsData, sysFormInitialized]);
|
||||
|
||||
useModalLock(showModal);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!canManage) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [rolesRes, permsRes, usersRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/roles`),
|
||||
apiFetch(`${API_BASE}/roles/permissions`),
|
||||
apiFetch(`${API_BASE}/users`),
|
||||
]);
|
||||
const rolesResult = await rolesRes.json();
|
||||
const permsResult = await permsRes.json();
|
||||
const usersResult = await usersRes.json();
|
||||
|
||||
if (rolesResult.success) {
|
||||
setRoles(Array.isArray(rolesResult.data) ? rolesResult.data : []);
|
||||
} else {
|
||||
alert.error(rolesResult.error || "Nepodařilo se načíst role");
|
||||
}
|
||||
|
||||
if (permsResult.success) {
|
||||
const perms: Permission[] = Array.isArray(permsResult.data)
|
||||
? permsResult.data
|
||||
: [];
|
||||
setAllPermissions(perms);
|
||||
// Group by module (part before '.')
|
||||
const groups: Record<string, Permission[]> = {};
|
||||
for (const p of perms) {
|
||||
const mod = p.name.split(".")[0] || "other";
|
||||
if (!groups[mod]) groups[mod] = [];
|
||||
groups[mod].push(p);
|
||||
}
|
||||
setPermissionGroups(groups);
|
||||
}
|
||||
|
||||
if (usersResult.success) {
|
||||
setUsers(Array.isArray(usersResult.data) ? usersResult.data : []);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [alert, canManage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const fetch2FARequired = useCallback(async () => {
|
||||
if (!canManage) {
|
||||
setRequire2FALoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/totp/required`);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setRequire2FA(result.data.require_2fa);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
setRequire2FALoading(false);
|
||||
}
|
||||
}, [canManage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch2FARequired();
|
||||
}, [fetch2FARequired]);
|
||||
|
||||
const fetchSystemSettings = useCallback(async () => {
|
||||
if (!canManage) return;
|
||||
setSysSettingsLoading(true);
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/company-settings`);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const d: SystemSettingsData = result.data;
|
||||
setSysSettings(d);
|
||||
setSysForm({
|
||||
break_threshold_hours: d.break_threshold_hours ?? 6,
|
||||
break_duration_short: d.break_duration_short ?? 15,
|
||||
break_duration_long: d.break_duration_long ?? 30,
|
||||
clock_rounding_minutes: d.clock_rounding_minutes ?? 15,
|
||||
invoice_alert_email: d.invoice_alert_email || "",
|
||||
leave_notify_email: d.leave_notify_email || "",
|
||||
smtp_from: d.smtp_from || "",
|
||||
smtp_from_name: d.smtp_from_name || "",
|
||||
max_login_attempts: d.max_login_attempts ?? 5,
|
||||
lockout_minutes: d.lockout_minutes ?? 15,
|
||||
max_requests_per_minute: d.max_requests_per_minute ?? 300,
|
||||
default_currency: d.default_currency || "CZK",
|
||||
default_vat_rate: d.default_vat_rate ?? 21,
|
||||
available_vat_rates:
|
||||
Array.isArray(d.available_vat_rates) &&
|
||||
d.available_vat_rates.length > 0
|
||||
? d.available_vat_rates
|
||||
: [0, 10, 12, 15, 21],
|
||||
available_currencies:
|
||||
Array.isArray(d.available_currencies) &&
|
||||
d.available_currencies.length > 0
|
||||
? d.available_currencies
|
||||
: ["CZK", "EUR", "USD", "GBP"],
|
||||
quotation_prefix: d.quotation_prefix || "NA",
|
||||
order_type_code: d.order_type_code || "71",
|
||||
invoice_type_code: d.invoice_type_code || "81",
|
||||
offer_number_pattern:
|
||||
d.offer_number_pattern || "{YYYY}/{PREFIX}/{NNN}",
|
||||
order_number_pattern: d.order_number_pattern || "{YY}{CODE}{NNNN}",
|
||||
invoice_number_pattern:
|
||||
d.invoice_number_pattern || "{YY}{CODE}{NNNN}",
|
||||
});
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se načíst systémová nastavení");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setSysSettingsLoading(false);
|
||||
}
|
||||
// Fetch system info
|
||||
apiFetch(`${API_BASE}/company-settings/system-info`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setSystemInfo(d.data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [alert, canManage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSystemSettings();
|
||||
}, [fetchSystemSettings]);
|
||||
// ── Early return after all hooks ──
|
||||
if (!canManage) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
const handleSaveSystemSettings = async () => {
|
||||
setSysSettingsSaving(true);
|
||||
@@ -302,7 +260,7 @@ export default function Settings() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Systémová nastavení byla uložena");
|
||||
fetchSystemSettings();
|
||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
@@ -323,8 +281,8 @@ export default function Settings() {
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setRequire2FA(!require2FA);
|
||||
alert.success(result.message || "2FA nastavení uloženo");
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
@@ -339,7 +297,7 @@ export default function Settings() {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
};
|
||||
@@ -443,7 +401,7 @@ export default function Settings() {
|
||||
result.message ||
|
||||
(editingRole ? "Role byla aktualizována" : "Role byla vytvořena"),
|
||||
);
|
||||
fetchData();
|
||||
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit roli");
|
||||
}
|
||||
@@ -471,7 +429,7 @@ export default function Settings() {
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, role: null });
|
||||
alert.success(result.message || "Role byla smazána");
|
||||
fetchData();
|
||||
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat roli");
|
||||
}
|
||||
@@ -482,39 +440,15 @@ export default function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (rolesLoading) {
|
||||
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>
|
||||
<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>
|
||||
<Skeleton
|
||||
name="settings"
|
||||
loading={rolesLoading}
|
||||
fixture={<SettingsFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -523,10 +457,13 @@ export default function Settings() {
|
||||
const get2FADescription = (): React.ReactNode => {
|
||||
if (require2FALoading) {
|
||||
return (
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "200px", height: "12px" }}
|
||||
/>
|
||||
<Skeleton
|
||||
name="settings-2fa"
|
||||
loading={require2FALoading}
|
||||
fixture={<SettingsFixture />}
|
||||
>
|
||||
<span />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
if (require2FA)
|
||||
@@ -783,7 +720,7 @@ export default function Settings() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.map((role) => (
|
||||
{roles.map((role: Role) => (
|
||||
<tr key={role.id}>
|
||||
<td>
|
||||
<div
|
||||
@@ -804,7 +741,7 @@ export default function Settings() {
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ color: "var(--text-secondary)" }}>
|
||||
{role.description || "\u2014"}
|
||||
{role.description || "—"}
|
||||
</td>
|
||||
<td>
|
||||
<span className="admin-badge admin-badge-info">
|
||||
@@ -815,7 +752,11 @@ export default function Settings() {
|
||||
</td>
|
||||
<td>
|
||||
<span className="admin-badge admin-badge-secondary">
|
||||
{users.filter((u) => u.role_id === role.id).length}
|
||||
{
|
||||
users.filter(
|
||||
(u: { role_id: number }) => u.role_id === role.id,
|
||||
).length
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -845,20 +786,26 @@ export default function Settings() {
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title={
|
||||
users.filter((u) => u.role_id === role.id)
|
||||
.length > 0
|
||||
users.filter(
|
||||
(u: { role_id: number }) =>
|
||||
u.role_id === role.id,
|
||||
).length > 0
|
||||
? "Nelze smazat roli s přiřazenými uživateli"
|
||||
: "Smazat"
|
||||
}
|
||||
aria-label={
|
||||
users.filter((u) => u.role_id === role.id)
|
||||
.length > 0
|
||||
users.filter(
|
||||
(u: { role_id: number }) =>
|
||||
u.role_id === role.id,
|
||||
).length > 0
|
||||
? "Nelze smazat roli s přiřazenými uživateli"
|
||||
: "Smazat"
|
||||
}
|
||||
disabled={
|
||||
users.filter((u) => u.role_id === role.id)
|
||||
.length > 0
|
||||
users.filter(
|
||||
(u: { role_id: number }) =>
|
||||
u.role_id === role.id,
|
||||
).length > 0
|
||||
}
|
||||
>
|
||||
<svg
|
||||
@@ -888,21 +835,14 @@ export default function Settings() {
|
||||
{/* System Settings Tab */}
|
||||
{activeTab === "system" && canManage && (
|
||||
<>
|
||||
{sysSettingsLoading ? (
|
||||
<div
|
||||
className="admin-skeleton"
|
||||
style={{ padding: 0, gap: "1.5rem" }}
|
||||
{sysSettingsLoading && !sysFormInitialized ? (
|
||||
<Skeleton
|
||||
name="settings-system"
|
||||
loading={sysSettingsLoading && !sysFormInitialized}
|
||||
fixture={<SettingsFixture />}
|
||||
>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="admin-card">
|
||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
||||
<div className="admin-skeleton-line w-1/3 mb-2" />
|
||||
<div className="admin-skeleton-line w-full" />
|
||||
<div className="admin-skeleton-line w-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div />
|
||||
</Skeleton>
|
||||
) : (
|
||||
<>
|
||||
{/* Section 1: Docházka */}
|
||||
@@ -1374,12 +1314,33 @@ export default function Settings() {
|
||||
<tbody>
|
||||
{(
|
||||
[
|
||||
["Verze", systemInfo.app_version],
|
||||
["Node.js", systemInfo.node_version],
|
||||
["Platforma", systemInfo.platform],
|
||||
["Uptime", systemInfo.uptime],
|
||||
["Prostředí", systemInfo.environment],
|
||||
["Časová zóna", systemInfo.timezone],
|
||||
[
|
||||
"Verze",
|
||||
(systemInfo as Record<string, unknown>)
|
||||
.app_version,
|
||||
],
|
||||
[
|
||||
"Node.js",
|
||||
(systemInfo as Record<string, unknown>)
|
||||
.node_version,
|
||||
],
|
||||
[
|
||||
"Platforma",
|
||||
(systemInfo as Record<string, unknown>).platform,
|
||||
],
|
||||
[
|
||||
"Uptime",
|
||||
(systemInfo as Record<string, unknown>).uptime,
|
||||
],
|
||||
[
|
||||
"Prostředí",
|
||||
(systemInfo as Record<string, unknown>)
|
||||
.environment,
|
||||
],
|
||||
[
|
||||
"Časová zóna",
|
||||
(systemInfo as Record<string, unknown>).timezone,
|
||||
],
|
||||
] as [string, string][]
|
||||
).map(([label, val]) => (
|
||||
<tr key={label}>
|
||||
@@ -1415,14 +1376,22 @@ export default function Settings() {
|
||||
</tr>
|
||||
{(
|
||||
[
|
||||
["Proces (RSS)", systemInfo.memory?.rss],
|
||||
[
|
||||
"Proces (RSS)",
|
||||
(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
).memory?.rss as string,
|
||||
],
|
||||
[
|
||||
"Heap",
|
||||
`${systemInfo.memory?.heap_used} / ${systemInfo.memory?.heap_total}`,
|
||||
`${(systemInfo as Record<string, Record<string, unknown>>).memory?.heap_used} / ${(systemInfo as Record<string, Record<string, unknown>>).memory?.heap_total}`,
|
||||
],
|
||||
[
|
||||
"Systém",
|
||||
`${systemInfo.memory?.system_free} volné z ${systemInfo.memory?.system_total}`,
|
||||
`${(systemInfo as Record<string, Record<string, unknown>>).memory?.system_free} volné z ${(systemInfo as Record<string, Record<string, unknown>>).memory?.system_total}`,
|
||||
],
|
||||
] as [string, string][]
|
||||
).map(([label, val]) => (
|
||||
@@ -1464,9 +1433,14 @@ export default function Settings() {
|
||||
</td>
|
||||
<td style={{ padding: "4px 0" }}>
|
||||
<span
|
||||
className={`admin-badge ${systemInfo.database?.status === "ok" ? "admin-badge-success" : "admin-badge-danger"}`}
|
||||
className={`admin-badge ${(systemInfo as Record<string, Record<string, unknown>>).database?.status === "ok" ? "admin-badge-success" : "admin-badge-danger"}`}
|
||||
>
|
||||
{systemInfo.database?.status === "ok"
|
||||
{(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
).database?.status === "ok"
|
||||
? "Připojeno"
|
||||
: "Chyba"}
|
||||
</span>
|
||||
@@ -1482,7 +1456,14 @@ export default function Settings() {
|
||||
Migrace
|
||||
</td>
|
||||
<td style={{ padding: "4px 0" }}>
|
||||
{systemInfo.database?.migrations_applied}
|
||||
{
|
||||
(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
).database?.migrations_applied as string
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -1502,9 +1483,33 @@ export default function Settings() {
|
||||
</tr>
|
||||
{(
|
||||
[
|
||||
["Projekty", systemInfo.nas?.projects],
|
||||
["Finance", systemInfo.nas?.financials],
|
||||
["Nabídky", systemInfo.nas?.offers],
|
||||
[
|
||||
"Projekty",
|
||||
(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>
|
||||
).nas?.projects,
|
||||
],
|
||||
[
|
||||
"Finance",
|
||||
(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>
|
||||
).nas?.financials,
|
||||
],
|
||||
[
|
||||
"Nabídky",
|
||||
(
|
||||
systemInfo as Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>
|
||||
).nas?.offers,
|
||||
],
|
||||
] as [string, Record<string, any>][]
|
||||
).map(([label, info]) => (
|
||||
<tr key={label}>
|
||||
@@ -1541,10 +1546,15 @@ export default function Settings() {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "60%", height: 14 }}
|
||||
/>
|
||||
<Skeleton
|
||||
name="settings-permissions"
|
||||
loading={
|
||||
!role.permissions || role.permissions.length === 0
|
||||
}
|
||||
fixture={<span>...</span>}
|
||||
>
|
||||
<span>{role.permissions?.length || 0} oprávnění</span>
|
||||
</Skeleton>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -12,6 +13,9 @@ import Forbidden from "../components/Forbidden";
|
||||
import { formatDate } from "../utils/attendanceHelpers";
|
||||
import { formatKm } from "../utils/formatters";
|
||||
import apiFetch from "../utils/api";
|
||||
import { tripListOptions, tripVehiclesOptions } from "../lib/queries/trips";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import TripsFixture from "../fixtures/TripsFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface Vehicle {
|
||||
@@ -49,10 +53,20 @@ interface TripForm {
|
||||
export default function Trips() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: tripsData, isPending: tripsLoading } = useQuery(
|
||||
tripListOptions({}),
|
||||
);
|
||||
const { data: vehiclesData } = useQuery(tripVehiclesOptions());
|
||||
|
||||
const trips = (tripsData ?? []) as Record<string, unknown>[] as Trip[];
|
||||
const vehicles = (vehiclesData ?? []) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[] as Vehicle[];
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [trips, setTrips] = useState<Trip[]>([]);
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingTrip, setEditingTrip] = useState<Trip | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
@@ -72,37 +86,6 @@ export default function Trips() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [, setLastKm] = useState(0);
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (showLoading = true) => {
|
||||
if (showLoading) setLoading(true);
|
||||
try {
|
||||
const [tripsRes, vehiclesRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/trips`),
|
||||
apiFetch(`${API_BASE}/vehicles`),
|
||||
]);
|
||||
const tripsResult = await tripsRes.json();
|
||||
const vehiclesResult = await vehiclesRes.json();
|
||||
if (tripsResult.success) {
|
||||
setTrips(Array.isArray(tripsResult.data) ? tripsResult.data : []);
|
||||
}
|
||||
if (vehiclesResult.success) {
|
||||
setVehicles(
|
||||
Array.isArray(vehiclesResult.data) ? vehiclesResult.data : [],
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
} finally {
|
||||
if (showLoading) setLoading(false);
|
||||
}
|
||||
},
|
||||
[alert],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useModalLock(showModal);
|
||||
|
||||
if (!hasPermission("trips.record")) return <Forbidden />;
|
||||
@@ -208,8 +191,7 @@ export default function Trips() {
|
||||
|
||||
if (result.success) {
|
||||
setShowModal(false);
|
||||
await fetchData(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -230,7 +212,7 @@ export default function Trips() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
await fetchData(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -248,65 +230,11 @@ export default function Trips() {
|
||||
return end > start ? end - start : 0;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (tripsLoading) {
|
||||
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-grid admin-grid-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: "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>
|
||||
</div>
|
||||
<Skeleton name="trips" loading={tripsLoading} fixture={<TripsFixture />}>
|
||||
<div />
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import {
|
||||
tripListOptions,
|
||||
tripVehiclesOptions,
|
||||
tripUsersOptions,
|
||||
} from "../lib/queries/trips";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
|
||||
import AdminDatePicker from "../components/AdminDatePicker";
|
||||
import FormField from "../components/FormField";
|
||||
@@ -12,6 +19,8 @@ import useModalLock from "../hooks/useModalLock";
|
||||
import { formatDate } from "../utils/attendanceHelpers";
|
||||
import { formatKm } from "../utils/formatters";
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import TripsAdminFixture from "../fixtures/TripsAdminFixture";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
interface Vehicle {
|
||||
@@ -88,8 +97,7 @@ function mapTrip(bt: BackendTrip): Trip {
|
||||
export default function TripsAdmin() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
const [filterMonth, setFilterMonth] = useState(() =>
|
||||
String(new Date().getMonth() + 1),
|
||||
);
|
||||
@@ -98,9 +106,6 @@ export default function TripsAdmin() {
|
||||
);
|
||||
const [filterVehicleId, setFilterVehicleId] = useState("");
|
||||
const [filterUserId, setFilterUserId] = useState("");
|
||||
const [trips, setTrips] = useState<Trip[]>([]);
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||
const [users, setUsers] = useState<UserShort[]>([]);
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
@@ -121,56 +126,27 @@ export default function TripsAdmin() {
|
||||
trip: Trip | null;
|
||||
}>({ show: false, trip: null });
|
||||
|
||||
// Fetch vehicles and users once on mount
|
||||
useEffect(() => {
|
||||
const fetchLookups = async () => {
|
||||
try {
|
||||
const [vRes, uRes, csRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/vehicles`),
|
||||
apiFetch(`${API_BASE}/trips/users`),
|
||||
apiFetch(`${API_BASE}/company-settings`),
|
||||
]);
|
||||
const vJson = await vRes.json();
|
||||
const uJson = await uRes.json();
|
||||
const csJson = await csRes.json();
|
||||
if (vJson.success) setVehicles(vJson.data);
|
||||
if (csJson.success) setCompanyName(csJson.data.company_name || "");
|
||||
if (uJson.success) {
|
||||
setUsers(uJson.data);
|
||||
}
|
||||
} catch {
|
||||
// silently fail, filters will just be empty
|
||||
}
|
||||
};
|
||||
fetchLookups();
|
||||
}, []);
|
||||
const { data: vehiclesData = [] } = useQuery(tripVehiclesOptions());
|
||||
const vehicles = vehiclesData as Vehicle[];
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (showLoading = true) => {
|
||||
if (showLoading) setLoading(true);
|
||||
try {
|
||||
let url = `${API_BASE}/trips?limit=1000&month=${filterMonth}&year=${filterYear}`;
|
||||
if (filterVehicleId) url += `&vehicle_id=${filterVehicleId}`;
|
||||
if (filterUserId) url += `&user_id=${filterUserId}`;
|
||||
const { data: tripUsersData = [] } = useQuery(tripUsersOptions());
|
||||
const tripUsers = tripUsersData as UserShort[];
|
||||
|
||||
const response = await apiFetch(url);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const mapped = (result.data as BackendTrip[]).map(mapTrip);
|
||||
setTrips(mapped);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
} finally {
|
||||
if (showLoading) setLoading(false);
|
||||
}
|
||||
},
|
||||
[filterMonth, filterYear, filterVehicleId, filterUserId, alert],
|
||||
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||
const companyName =
|
||||
((companySettings as Record<string, unknown> | undefined)
|
||||
?.company_name as string) ?? "";
|
||||
|
||||
const { data: tripsData, isPending } = useQuery(
|
||||
tripListOptions({
|
||||
month: Number(filterMonth) || undefined,
|
||||
year: Number(filterYear) || undefined,
|
||||
vehicleId: filterVehicleId ? Number(filterVehicleId) : undefined,
|
||||
userId: filterUserId ? Number(filterUserId) : undefined,
|
||||
perPage: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
const trips = ((tripsData ?? []) as BackendTrip[]).map(mapTrip);
|
||||
|
||||
useModalLock(showEditModal);
|
||||
|
||||
@@ -211,8 +187,7 @@ export default function TripsAdmin() {
|
||||
|
||||
if (result.success) {
|
||||
setShowEditModal(false);
|
||||
await fetchData(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -237,7 +212,7 @@ export default function TripsAdmin() {
|
||||
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, trip: null });
|
||||
await fetchData(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
@@ -259,7 +234,7 @@ export default function TripsAdmin() {
|
||||
};
|
||||
const getSelectedUserName = () => {
|
||||
if (!filterUserId) return null;
|
||||
const u = users.find((u) => String(u.id) === filterUserId);
|
||||
const u = tripUsers.find((u) => String(u.id) === filterUserId);
|
||||
return u?.name || null;
|
||||
};
|
||||
|
||||
@@ -468,7 +443,7 @@ export default function TripsAdmin() {
|
||||
className="admin-form-select"
|
||||
>
|
||||
<option value="">Všichni řidiči</option>
|
||||
{users.map((u) => (
|
||||
{tripUsers.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</option>
|
||||
@@ -565,119 +540,117 @@ export default function TripsAdmin() {
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<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" />
|
||||
<Skeleton
|
||||
name="trips-admin"
|
||||
loading={isPending}
|
||||
fixture={<TripsAdminFixture />}
|
||||
>
|
||||
<>
|
||||
{trips.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádné záznamy jízd pro vybrané období.</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!loading && trips.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádné záznamy jízd pro vybrané období.</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && trips.length > 0 && (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Řidič</th>
|
||||
<th>Vozidlo</th>
|
||||
<th>Trasa</th>
|
||||
<th>Stav km</th>
|
||||
<th>Vzdálenost</th>
|
||||
<th>Typ</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trips.map((trip) => (
|
||||
<tr key={trip.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(trip.trip_date)}
|
||||
</td>
|
||||
<td>{trip.driver_name}</td>
|
||||
<td>
|
||||
<span className="admin-badge">{trip.spz}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{trip.route_from} → {trip.route_to}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{formatKm(trip.start_km)} - {formatKm(trip.end_km)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<strong>{formatKm(trip.distance)} km</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
|
||||
>
|
||||
{trip.is_business ? "Služební" : "Soukromá"}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button
|
||||
onClick={() => openEditModal(trip)}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
)}
|
||||
{trips.length > 0 && (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Řidič</th>
|
||||
<th>Vozidlo</th>
|
||||
<th>Trasa</th>
|
||||
<th>Stav km</th>
|
||||
<th>Vzdálenost</th>
|
||||
<th>Typ</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trips.map((trip) => (
|
||||
<tr key={trip.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(trip.trip_date)}
|
||||
</td>
|
||||
<td>{trip.driver_name}</td>
|
||||
<td>
|
||||
<span className="admin-badge">{trip.spz}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{trip.route_from} → {trip.route_to}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{formatKm(trip.start_km)} -{" "}
|
||||
{formatKm(trip.end_km)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<strong>{formatKm(trip.distance)} km</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteConfirm({ show: true, trip })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat"
|
||||
aria-label="Smazat"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{trip.is_business ? "Služební" : "Soukromá"}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="admin-table-actions">
|
||||
<button
|
||||
onClick={() => openEditModal(trip)}
|
||||
className="admin-btn-icon"
|
||||
title="Upravit"
|
||||
aria-label="Upravit"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setDeleteConfirm({ show: true, trip })
|
||||
}
|
||||
className="admin-btn-icon danger"
|
||||
title="Smazat"
|
||||
aria-label="Smazat"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -7,9 +8,9 @@ import Forbidden from "../components/Forbidden";
|
||||
import { formatDate } from "../utils/attendanceHelpers";
|
||||
import { formatKm } from "../utils/formatters";
|
||||
import FormField from "../components/FormField";
|
||||
import apiFetch from "../utils/api";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
import { tripHistoryOptions, tripVehiclesOptions } from "../lib/queries/trips";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import TripsHistoryFixture from "../fixtures/TripsHistoryFixture";
|
||||
|
||||
interface Vehicle {
|
||||
id: number | string;
|
||||
@@ -34,14 +35,30 @@ interface Trip {
|
||||
export default function TripsHistory() {
|
||||
const alert = useAlert();
|
||||
const { user, hasPermission } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [month, setMonth] = useState(() => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
});
|
||||
const [vehicleId, setVehicleId] = useState("");
|
||||
const [trips, setTrips] = useState<Trip[]>([]);
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||
|
||||
const { data: vehiclesData = [] } = useQuery(tripVehiclesOptions());
|
||||
const vehicles = vehiclesData as Vehicle[];
|
||||
|
||||
const { data: tripsData, isPending } = useQuery(
|
||||
tripHistoryOptions({
|
||||
month,
|
||||
vehicleId: vehicleId ? Number(vehicleId) : undefined,
|
||||
userId: user?.id,
|
||||
}),
|
||||
);
|
||||
const trips = ((tripsData ?? []) as Record<string, unknown>[]).map((t) => ({
|
||||
...t,
|
||||
spz: (t.vehicles as Record<string, string>)?.spz || "",
|
||||
driver_name: t.users
|
||||
? `${(t.users as Record<string, string>).first_name || ""} ${(t.users as Record<string, string>).last_name || ""}`.trim()
|
||||
: "",
|
||||
distance: ((t.end_km as number) || 0) - ((t.start_km as number) || 0),
|
||||
})) as Trip[];
|
||||
|
||||
const totals = trips.reduce(
|
||||
(acc, t) => ({
|
||||
@@ -52,52 +69,6 @@ export default function TripsHistory() {
|
||||
{ total: 0, business: 0, count: 0 },
|
||||
);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ month });
|
||||
if (user?.id) params.set("user_id", String(user.id));
|
||||
if (vehicleId) params.set("vehicle_id", vehicleId);
|
||||
|
||||
const [tripsRes, vehiclesRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/trips?${params}`),
|
||||
apiFetch(`${API_BASE}/vehicles`),
|
||||
]);
|
||||
if (tripsRes.status === 401) return;
|
||||
const tripsResult = await tripsRes.json();
|
||||
const vehiclesResult = await vehiclesRes.json();
|
||||
if (tripsResult.success) {
|
||||
const raw = Array.isArray(tripsResult.data)
|
||||
? tripsResult.data
|
||||
: tripsResult.data?.items || [];
|
||||
setTrips(
|
||||
raw.map((t: Record<string, unknown>) => ({
|
||||
...t,
|
||||
spz: (t.vehicles as Record<string, string>)?.spz || "",
|
||||
driver_name: t.users
|
||||
? `${(t.users as Record<string, string>).first_name || ""} ${(t.users as Record<string, string>).last_name || ""}`.trim()
|
||||
: "",
|
||||
distance:
|
||||
((t.end_km as number) || 0) - ((t.start_km as number) || 0),
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (vehiclesResult.success) {
|
||||
setVehicles(
|
||||
Array.isArray(vehiclesResult.data) ? vehiclesResult.data : [],
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [month, vehicleId, alert, user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (!hasPermission("trips.history")) return <Forbidden />;
|
||||
|
||||
const getMonthName = (monthStr: string): string => {
|
||||
@@ -240,88 +211,86 @@ export default function TripsHistory() {
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
<div className="admin-card-body">
|
||||
{loading && (
|
||||
<div className="admin-skeleton gap-5">
|
||||
{[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" />
|
||||
<Skeleton
|
||||
name="trips-history"
|
||||
loading={isPending}
|
||||
fixture={<TripsHistoryFixture />}
|
||||
>
|
||||
<>
|
||||
{trips.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádné záznamy jízd pro vybrané období.</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!loading && trips.length === 0 && (
|
||||
<div className="admin-empty-state">
|
||||
<p>Žádné záznamy jízd pro vybrané období.</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && trips.length > 0 && (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Vozidlo</th>
|
||||
<th>Řidič</th>
|
||||
<th>Trasa</th>
|
||||
<th>Stav km</th>
|
||||
<th>Vzdálenost</th>
|
||||
<th>Typ</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trips.map((trip) => (
|
||||
<tr key={trip.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(trip.trip_date)}
|
||||
</td>
|
||||
<td>
|
||||
<span className="admin-badge">{trip.spz}</span>
|
||||
</td>
|
||||
<td style={{ color: "var(--text-secondary)" }}>
|
||||
{trip.driver_name}
|
||||
</td>
|
||||
<td>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{trip.route_from} → {trip.route_to}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
{formatKm(trip.start_km)} - {formatKm(trip.end_km)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<strong>{formatKm(trip.distance)} km</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
|
||||
>
|
||||
{trip.is_business ? "Služební" : "Soukromá"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
>
|
||||
{trip.notes || "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{trips.length > 0 && (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Vozidlo</th>
|
||||
<th>Řidič</th>
|
||||
<th>Trasa</th>
|
||||
<th>Stav km</th>
|
||||
<th>Vzdálenost</th>
|
||||
<th>Typ</th>
|
||||
<th>Poznámka</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trips.map((trip) => (
|
||||
<tr key={trip.id}>
|
||||
<td className="admin-mono">
|
||||
{formatDate(trip.trip_date)}
|
||||
</td>
|
||||
<td>
|
||||
<span className="admin-badge">{trip.spz}</span>
|
||||
</td>
|
||||
<td style={{ color: "var(--text-secondary)" }}>
|
||||
{trip.driver_name}
|
||||
</td>
|
||||
<td>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{trip.route_from} → {trip.route_to}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
{formatKm(trip.start_km)} -{" "}
|
||||
{formatKm(trip.end_km)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-mono">
|
||||
<strong>{formatKm(trip.distance)} km</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
|
||||
>
|
||||
{trip.is_business ? "Služební" : "Soukromá"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
>
|
||||
{trip.notes || "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user