Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
746d17e182 | ||
|
|
e96e51598a | ||
|
|
9abec36f07 | ||
|
|
ecd8e3679f | ||
|
|
ba95723b61 | ||
|
|
12289bdce3 |
37
boneyard.config.json
Normal file
37
boneyard.config.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": [375, 768, 1280],
|
||||||
|
"out": "./src/admin/bones",
|
||||||
|
"color": "#e0e0e0",
|
||||||
|
"animate": "shimmer",
|
||||||
|
"shimmerColor": "#f0f0f0",
|
||||||
|
"speed": "1.2s",
|
||||||
|
"shimmerAngle": 110,
|
||||||
|
"wait": 3000,
|
||||||
|
"routes": [
|
||||||
|
"/",
|
||||||
|
"/users",
|
||||||
|
"/attendance",
|
||||||
|
"/attendance/history",
|
||||||
|
"/attendance/admin",
|
||||||
|
"/attendance/balances",
|
||||||
|
"/attendance/requests",
|
||||||
|
"/attendance/approval",
|
||||||
|
"/attendance/create",
|
||||||
|
"/trips",
|
||||||
|
"/trips/history",
|
||||||
|
"/trips/admin",
|
||||||
|
"/vehicles",
|
||||||
|
"/offers",
|
||||||
|
"/offers/new",
|
||||||
|
"/offers/customers",
|
||||||
|
"/offers/templates",
|
||||||
|
"/orders",
|
||||||
|
"/orders/1",
|
||||||
|
"/projects",
|
||||||
|
"/projects/80",
|
||||||
|
"/invoices",
|
||||||
|
"/invoices/new",
|
||||||
|
"/settings",
|
||||||
|
"/audit-log"
|
||||||
|
]
|
||||||
|
}
|
||||||
220
package-lock.json
generated
220
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.3",
|
"version": "1.5.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.3",
|
"version": "1.5.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -19,8 +19,10 @@
|
|||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@types/jsdom": "^28.0.1",
|
"@types/jsdom": "^28.0.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"boneyard-js": "^1.8.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
@@ -148,6 +150,13 @@
|
|||||||
"specificity": "bin/cli.js"
|
"specificity": "bin/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@chenglou/pretext": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chenglou/pretext/-/pretext-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@csstools/color-helpers": {
|
"node_modules/@csstools/color-helpers": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||||
@@ -353,7 +362,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -365,7 +373,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -376,7 +383,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -390,7 +396,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -407,7 +412,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -424,7 +428,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -441,7 +444,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -458,7 +460,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -475,7 +476,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -492,7 +492,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -509,7 +508,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -526,7 +524,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -543,7 +540,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -560,7 +556,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -577,7 +572,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -594,7 +588,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -611,7 +604,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -628,7 +620,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -645,7 +636,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -662,7 +652,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -679,7 +668,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -696,7 +684,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -713,7 +700,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -730,7 +716,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -747,7 +732,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -764,7 +748,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -781,7 +764,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -798,7 +780,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -815,7 +796,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1195,7 +1175,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1224,7 +1203,7 @@
|
|||||||
"version": "0.115.0",
|
"version": "0.115.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
||||||
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
@@ -1234,7 +1213,7 @@
|
|||||||
"version": "0.115.0",
|
"version": "0.115.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
|
||||||
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
@@ -1385,7 +1364,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1402,7 +1380,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1419,7 +1396,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1436,7 +1412,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1453,7 +1428,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1470,7 +1444,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1487,7 +1460,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1504,7 +1476,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1521,7 +1492,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1538,7 +1508,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1555,7 +1524,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1572,7 +1540,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1589,7 +1556,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1606,7 +1572,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1623,7 +1588,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1646,6 +1610,32 @@
|
|||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.100.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz",
|
||||||
|
"integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.100.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz",
|
||||||
|
"integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.100.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/token": {
|
"node_modules/@tokenizer/token": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
@@ -1662,7 +1652,6 @@
|
|||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2332,6 +2321,53 @@
|
|||||||
"require-from-string": "^2.0.2"
|
"require-from-string": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boneyard-js": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/boneyard-js/-/boneyard-js-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-tnffA34xFgGSKpQ6QXebalkz2g/rMRAUIh3S0OBoPTK+OgYR3QEQvD3fHGKNQDbCTjn3W7dgNfiBhZGpnqlcNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"boneyard-js": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@chenglou/pretext": "^0.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=14",
|
||||||
|
"preact": ">=10",
|
||||||
|
"react": ">=18",
|
||||||
|
"react-native": ">=0.71",
|
||||||
|
"svelte": ">=5.29",
|
||||||
|
"vite": ">=5",
|
||||||
|
"vue": ">=3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@angular/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"preact": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
@@ -2889,7 +2925,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -3087,7 +3123,7 @@
|
|||||||
"version": "0.27.4",
|
"version": "0.27.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3444,7 +3480,7 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -3568,7 +3604,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3656,7 +3691,7 @@
|
|||||||
"version": "4.13.6",
|
"version": "4.13.6",
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resolve-pkg-maps": "^1.0.0"
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
@@ -4135,7 +4170,7 @@
|
|||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -4168,7 +4203,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4189,7 +4223,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4210,7 +4243,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4231,7 +4263,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4252,7 +4283,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4273,7 +4303,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4294,7 +4323,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4315,7 +4343,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4336,7 +4363,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4357,7 +4383,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4378,7 +4403,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4618,7 +4642,7 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4931,7 +4955,7 @@
|
|||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -4988,6 +5012,50 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
@@ -5001,7 +5069,7 @@
|
|||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -5521,7 +5589,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
@@ -5556,7 +5624,7 @@
|
|||||||
"version": "1.0.0-rc.9",
|
"version": "1.0.0-rc.9",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
||||||
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.115.0",
|
"@oxc-project/types": "=0.115.0",
|
||||||
@@ -5590,7 +5658,7 @@
|
|||||||
"version": "1.0.0-rc.9",
|
"version": "1.0.0-rc.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
||||||
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
@@ -6159,7 +6227,7 @@
|
|||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -6279,7 +6347,7 @@
|
|||||||
"version": "4.21.0",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.27.0",
|
||||||
@@ -6334,7 +6402,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
|
||||||
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/runtime": "0.115.0",
|
"@oxc-project/runtime": "0.115.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.5.5",
|
"version": "1.5.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"bones": "boneyard-js build http://localhost:3000"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -34,8 +35,10 @@
|
|||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@types/jsdom": "^28.0.1",
|
"@types/jsdom": "^28.0.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"boneyard-js": "^1.8.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
import { AlertProvider } from "./context/AlertContext";
|
import { AlertProvider } from "./context/AlertContext";
|
||||||
|
import { queryClient } from "./lib/queryClient";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
import AlertContainer from "./components/AlertContainer";
|
import AlertContainer from "./components/AlertContainer";
|
||||||
@@ -14,8 +16,8 @@ import "./buttons.css";
|
|||||||
import "./layout.css";
|
import "./layout.css";
|
||||||
import "./components.css";
|
import "./components.css";
|
||||||
import "./tables.css";
|
import "./tables.css";
|
||||||
import "./skeleton.css";
|
|
||||||
import "./datepicker.css";
|
import "./datepicker.css";
|
||||||
|
import "./bones/registry";
|
||||||
import "./filemanager.css";
|
import "./filemanager.css";
|
||||||
import "./pagination.css";
|
import "./pagination.css";
|
||||||
import "./responsive.css";
|
import "./responsive.css";
|
||||||
@@ -57,63 +59,80 @@ export default function AdminApp() {
|
|||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<AlertContainer />
|
<QueryClientProvider client={queryClient}>
|
||||||
<ErrorBoundary>
|
<AlertContainer />
|
||||||
<Suspense
|
<ErrorBoundary>
|
||||||
fallback={
|
<Suspense
|
||||||
<div className="admin-loading">
|
fallback={
|
||||||
<div className="admin-spinner" />
|
<div className="admin-loading">
|
||||||
</div>
|
<div className="admin-spinner" />
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<Routes>
|
>
|
||||||
<Route path="login" element={<Login />} />
|
<Routes>
|
||||||
<Route element={<AdminLayout />}>
|
<Route path="login" element={<Login />} />
|
||||||
<Route index element={<Dashboard />} />
|
<Route element={<AdminLayout />}>
|
||||||
<Route path="users" element={<Users />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="attendance" element={<Attendance />} />
|
<Route path="users" element={<Users />} />
|
||||||
<Route
|
<Route path="attendance" element={<Attendance />} />
|
||||||
path="attendance/history"
|
<Route
|
||||||
element={<AttendanceHistory />}
|
path="attendance/history"
|
||||||
/>
|
element={<AttendanceHistory />}
|
||||||
<Route path="attendance/admin" element={<AttendanceAdmin />} />
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="attendance/balances"
|
path="attendance/admin"
|
||||||
element={<AttendanceBalances />}
|
element={<AttendanceAdmin />}
|
||||||
/>
|
/>
|
||||||
<Route path="attendance/requests" element={<LeaveRequests />} />
|
<Route
|
||||||
<Route path="attendance/approval" element={<LeaveApproval />} />
|
path="attendance/balances"
|
||||||
<Route
|
element={<AttendanceBalances />}
|
||||||
path="attendance/create"
|
/>
|
||||||
element={<AttendanceCreate />}
|
<Route
|
||||||
/>
|
path="attendance/requests"
|
||||||
<Route
|
element={<LeaveRequests />}
|
||||||
path="attendance/location/:id"
|
/>
|
||||||
element={<AttendanceLocation />}
|
<Route
|
||||||
/>
|
path="attendance/approval"
|
||||||
<Route path="trips" element={<Trips />} />
|
element={<LeaveApproval />}
|
||||||
<Route path="trips/history" element={<TripsHistory />} />
|
/>
|
||||||
<Route path="trips/admin" element={<TripsAdmin />} />
|
<Route
|
||||||
<Route path="vehicles" element={<Vehicles />} />
|
path="attendance/create"
|
||||||
<Route path="offers" element={<Offers />} />
|
element={<AttendanceCreate />}
|
||||||
<Route path="offers/new" element={<OfferDetail />} />
|
/>
|
||||||
<Route path="offers/:id" element={<OfferDetail />} />
|
<Route
|
||||||
<Route path="offers/customers" element={<OffersCustomers />} />
|
path="attendance/location/:id"
|
||||||
<Route path="offers/templates" element={<OffersTemplates />} />
|
element={<AttendanceLocation />}
|
||||||
<Route path="orders" element={<Orders />} />
|
/>
|
||||||
<Route path="orders/:id" element={<OrderDetail />} />
|
<Route path="trips" element={<Trips />} />
|
||||||
<Route path="projects" element={<Projects />} />
|
<Route path="trips/history" element={<TripsHistory />} />
|
||||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
<Route path="trips/admin" element={<TripsAdmin />} />
|
||||||
<Route path="invoices" element={<Invoices />} />
|
<Route path="vehicles" element={<Vehicles />} />
|
||||||
<Route path="invoices/new" element={<InvoiceDetail />} />
|
<Route path="offers" element={<Offers />} />
|
||||||
<Route path="invoices/:id" element={<InvoiceDetail />} />
|
<Route path="offers/new" element={<OfferDetail />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="offers/:id" element={<OfferDetail />} />
|
||||||
<Route path="audit-log" element={<AuditLog />} />
|
<Route
|
||||||
</Route>
|
path="offers/customers"
|
||||||
<Route path="*" element={<NotFound />} />
|
element={<OffersCustomers />}
|
||||||
</Routes>
|
/>
|
||||||
</Suspense>
|
<Route
|
||||||
</ErrorBoundary>
|
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>
|
</AlertProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -330,15 +330,6 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Additional Utilities ─────────────────────────────────────────── */
|
/* ── Additional Utilities ─────────────────────────────────────────── */
|
||||||
|
|
||||||
/* Font sizes */
|
/* Font sizes */
|
||||||
|
|||||||
1142
src/admin/bones/attendance-balances.bones.json
Normal file
1142
src/admin/bones/attendance-balances.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
263
src/admin/bones/attendance-create.bones.json
Normal file
263
src/admin/bones/attendance-create.bones.json
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 403,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
100,
|
||||||
|
361,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
55,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
83,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
127,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
156,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
200,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
229,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
273,
|
||||||
|
92.5926,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
302,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
346,
|
||||||
|
19.2352,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 420,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
23.3738,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
46,
|
||||||
|
81.5217,
|
||||||
|
373,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
65,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
94,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
138,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
167,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
211,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
284,
|
||||||
|
76.3587,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
313,
|
||||||
|
76.3587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
357,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "attendance-create",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 369,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
17.2722,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
46,
|
||||||
|
60.241,
|
||||||
|
323,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
65,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
93,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
129,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
156,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
192,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
219,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
255,
|
||||||
|
56.4257,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
282,
|
||||||
|
56.4257,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
318,
|
||||||
|
6.3771,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "7c4e446bf97f164a0fba87e2dd7df7d1"
|
||||||
|
}
|
||||||
1094
src/admin/bones/attendance-history-fund.bones.json
Normal file
1094
src/admin/bones/attendance-history-fund.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
1094
src/admin/bones/attendance-history-table.bones.json
Normal file
1094
src/admin/bones/attendance-history-table.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
1149
src/admin/bones/audit-log-rows.bones.json
Normal file
1149
src/admin/bones/audit-log-rows.bones.json
Normal file
File diff suppressed because it is too large
Load Diff
506
src/admin/bones/dash-sessions.bones.json
Normal file
506
src/admin/bones/dash-sessions.bones.json
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 341,
|
||||||
|
"width": 341,
|
||||||
|
"height": 304,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
304,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.8123,
|
||||||
|
17,
|
||||||
|
34.9386,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.3383,
|
||||||
|
13,
|
||||||
|
36.8493,
|
||||||
|
37,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.2933,
|
||||||
|
63,
|
||||||
|
99.4135,
|
||||||
|
83,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
86,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
95,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.0407,
|
||||||
|
79,
|
||||||
|
19.5152,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
110,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
110,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
110,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
167,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
176,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
162,
|
||||||
|
75.9531,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
189,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
189,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
189,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9853,
|
||||||
|
246,
|
||||||
|
10.5572,
|
||||||
|
36,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.6246,
|
||||||
|
255,
|
||||||
|
5.2786,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
241,
|
||||||
|
75.9531,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.0616,
|
||||||
|
267,
|
||||||
|
21.6734,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.081,
|
||||||
|
267,
|
||||||
|
1.0585,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.4855,
|
||||||
|
267,
|
||||||
|
26.8649,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 726,
|
||||||
|
"width": 726,
|
||||||
|
"height": 319,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
319,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.6171,
|
||||||
|
19,
|
||||||
|
16.4106,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.0749,
|
||||||
|
15,
|
||||||
|
17.308,
|
||||||
|
37,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.1377,
|
||||||
|
67,
|
||||||
|
99.7245,
|
||||||
|
85,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
89,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
100,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.5278,
|
||||||
|
83,
|
||||||
|
9.1662,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
114,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
114,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
114,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
173,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
184,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
168,
|
||||||
|
85.3994,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
198,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
198,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
198,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4435,
|
||||||
|
257,
|
||||||
|
5.5096,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.9587,
|
||||||
|
268,
|
||||||
|
2.4793,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
251,
|
||||||
|
85.3994,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.157,
|
||||||
|
281,
|
||||||
|
11.0279,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.2868,
|
||||||
|
281,
|
||||||
|
0.5381,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9268,
|
||||||
|
281,
|
||||||
|
13.6686,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "dash-sessions",
|
||||||
|
"viewportWidth": 484,
|
||||||
|
"width": 484,
|
||||||
|
"height": 309,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
309,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.9256,
|
||||||
|
15,
|
||||||
|
24.6158,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1785,
|
||||||
|
15,
|
||||||
|
23.8959,
|
||||||
|
29,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.2066,
|
||||||
|
59,
|
||||||
|
99.5868,
|
||||||
|
83,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
80,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
91,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.7917,
|
||||||
|
76,
|
||||||
|
12.9358,
|
||||||
|
24,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
105,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
105,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
105,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
164,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
175,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
158,
|
||||||
|
78.0992,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
188,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
188,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
188,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.1653,
|
||||||
|
247,
|
||||||
|
8.2645,
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.438,
|
||||||
|
258,
|
||||||
|
3.719,
|
||||||
|
18,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
242,
|
||||||
|
78.0992,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.7355,
|
||||||
|
271,
|
||||||
|
16.5418,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.9303,
|
||||||
|
271,
|
||||||
|
0.8071,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.3902,
|
||||||
|
271,
|
||||||
|
20.503,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "e057ca7b36a30c5971a4225ec3ad4680"
|
||||||
|
}
|
||||||
707
src/admin/bones/invoice-detail.bones.json
Normal file
707
src/admin/bones/invoice-detail.bones.json
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 466,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
3.4188,
|
||||||
|
12,
|
||||||
|
5.698,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8148,
|
||||||
|
9,
|
||||||
|
30.0881,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
30.6713,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
34.0901,
|
||||||
|
52,
|
||||||
|
31.2455,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.7545,
|
||||||
|
52,
|
||||||
|
31.2455,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
100,
|
||||||
|
152,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
125,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
154,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
125,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
154,
|
||||||
|
44.0171,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
197,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
197,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
226,
|
||||||
|
44.0171,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
280,
|
||||||
|
100,
|
||||||
|
186,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
293,
|
||||||
|
92.5926,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
322,
|
||||||
|
33.3066,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
322,
|
||||||
|
35.1718,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
322,
|
||||||
|
36.9836,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
322,
|
||||||
|
36.9881,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
353,
|
||||||
|
33.3066,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
353,
|
||||||
|
35.1718,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
353,
|
||||||
|
36.9836,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
353,
|
||||||
|
36.9881,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
387,
|
||||||
|
33.3066,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
387,
|
||||||
|
35.1718,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
387,
|
||||||
|
36.9836,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
387,
|
||||||
|
36.9881,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
420,
|
||||||
|
33.3066,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
37.0103,
|
||||||
|
420,
|
||||||
|
35.1718,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.1822,
|
||||||
|
420,
|
||||||
|
36.9836,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.1658,
|
||||||
|
420,
|
||||||
|
36.9881,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 444,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
1.6304,
|
||||||
|
12,
|
||||||
|
2.7174,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.0652,
|
||||||
|
7,
|
||||||
|
17.5378,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.9692,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.7729,
|
||||||
|
0,
|
||||||
|
13.6379,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.0413,
|
||||||
|
0,
|
||||||
|
10.9587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
164,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
27,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
151,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
180,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
151,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
180,
|
||||||
|
46.3315,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
100,
|
||||||
|
204,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
259,
|
||||||
|
94.837,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
288,
|
||||||
|
22.3803,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
288,
|
||||||
|
23.5882,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
288,
|
||||||
|
24.431,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
288,
|
||||||
|
24.4374,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
321,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
321,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
321,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
321,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
356,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
356,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
356,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
356,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
391,
|
||||||
|
22.3803,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.9618,
|
||||||
|
391,
|
||||||
|
23.5882,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.55,
|
||||||
|
391,
|
||||||
|
24.431,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9811,
|
||||||
|
391,
|
||||||
|
24.4374,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "invoice-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 457,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0.6024,
|
||||||
|
7,
|
||||||
|
2.008,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.0161,
|
||||||
|
2,
|
||||||
|
12.9597,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.8407,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.4226,
|
||||||
|
0,
|
||||||
|
9.6762,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
92.3036,
|
||||||
|
0,
|
||||||
|
7.6964,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
160,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
24,
|
||||||
|
9999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
165,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
138,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
165,
|
||||||
|
47.2892,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
226,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
245,
|
||||||
|
96.1847,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
273,
|
||||||
|
22.9606,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
273,
|
||||||
|
24.065,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
273,
|
||||||
|
24.578,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
273,
|
||||||
|
24.5811,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
311,
|
||||||
|
22.9606,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
311,
|
||||||
|
24.065,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
311,
|
||||||
|
24.578,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
311,
|
||||||
|
24.5811,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
354,
|
||||||
|
22.9606,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
354,
|
||||||
|
24.065,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
354,
|
||||||
|
24.578,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
354,
|
||||||
|
24.5811,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
22.9606,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8682,
|
||||||
|
396,
|
||||||
|
24.065,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.9332,
|
||||||
|
396,
|
||||||
|
24.578,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5112,
|
||||||
|
396,
|
||||||
|
24.5811,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "934452d45a0bef9320dc379fb3f43bb5"
|
||||||
|
}
|
||||||
599
src/admin/bones/leave-approval.bones.json
Normal file
599
src/admin/bones/leave-approval.bones.json
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 294,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
233,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
19.2753,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
74,
|
||||||
|
18.7812,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
74,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
74,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
74,
|
||||||
|
9.4373,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
74,
|
||||||
|
54.4471,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
105,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
105,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
105,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
105,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
105,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
105,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
159,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
159,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
159,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
159,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
159,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
159,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
213,
|
||||||
|
19.2753,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.979,
|
||||||
|
213,
|
||||||
|
18.7812,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.7601,
|
||||||
|
213,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8104,
|
||||||
|
213,
|
||||||
|
23.0502,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8606,
|
||||||
|
213,
|
||||||
|
9.4373,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
97.2979,
|
||||||
|
213,
|
||||||
|
54.4471,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 299,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
29.4264,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
29.4264,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
12.6911,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
86,
|
||||||
|
12.3769,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
86,
|
||||||
|
15.0921,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
86,
|
||||||
|
15.0921,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
86,
|
||||||
|
6.4856,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
86,
|
||||||
|
33.0991,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
119,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
119,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
119,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
119,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
119,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
119,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
173,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
173,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
173,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
173,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
173,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
173,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
227,
|
||||||
|
12.6911,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.2726,
|
||||||
|
227,
|
||||||
|
12.3769,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.6495,
|
||||||
|
227,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.7416,
|
||||||
|
227,
|
||||||
|
15.0921,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57.8337,
|
||||||
|
227,
|
||||||
|
6.4856,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.3194,
|
||||||
|
227,
|
||||||
|
33.0991,
|
||||||
|
54,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "leave-approval",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 299,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
21.7448,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
21.7448,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
232,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
13.8664,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
86,
|
||||||
|
13.5589,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
86,
|
||||||
|
16.196,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
86,
|
||||||
|
16.196,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
86,
|
||||||
|
7.8878,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
86,
|
||||||
|
28.4795,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
124,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
124,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
124,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
124,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
124,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
124,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
176,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
176,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
176,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
176,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
176,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
228,
|
||||||
|
13.8664,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15.774,
|
||||||
|
228,
|
||||||
|
13.5589,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.333,
|
||||||
|
228,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.529,
|
||||||
|
228,
|
||||||
|
16.196,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.725,
|
||||||
|
228,
|
||||||
|
7.8878,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.6128,
|
||||||
|
228,
|
||||||
|
28.4795,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "4b74917f659334073252a738cfa9c4ac"
|
||||||
|
}
|
||||||
704
src/admin/bones/leave-requests.bones.json
Normal file
704
src/admin/bones/leave-requests.bones.json
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 367,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
254,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
21.1806,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
126,
|
||||||
|
20.6375,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
126,
|
||||||
|
25.3294,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
126,
|
||||||
|
25.3294,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
126,
|
||||||
|
10.3677,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
126,
|
||||||
|
20.7977,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
126,
|
||||||
|
18.8079,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
157,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
157,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
157,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
157,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
157,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
157,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
157,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
218,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
218,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
218,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
218,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
218,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
218,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
218,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
279,
|
||||||
|
21.1806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.8843,
|
||||||
|
279,
|
||||||
|
20.6375,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5217,
|
||||||
|
279,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8511,
|
||||||
|
279,
|
||||||
|
25.3294,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.1806,
|
||||||
|
279,
|
||||||
|
10.3677,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.5483,
|
||||||
|
279,
|
||||||
|
20.7977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.346,
|
||||||
|
279,
|
||||||
|
18.8079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 320,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
27.1888,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
27.1888,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.6914,
|
||||||
|
4,
|
||||||
|
16.3086,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
253,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
14.313,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
86,
|
||||||
|
13.9585,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
86,
|
||||||
|
17.0219,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
86,
|
||||||
|
17.0219,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
86,
|
||||||
|
7.3157,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
86,
|
||||||
|
13.2006,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
86,
|
||||||
|
12.0053,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
119,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
119,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
119,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
119,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
119,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
119,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
119,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
180,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
180,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
180,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
180,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
180,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
180,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
180,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
241,
|
||||||
|
14.313,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8945,
|
||||||
|
241,
|
||||||
|
13.9585,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.853,
|
||||||
|
241,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8749,
|
||||||
|
241,
|
||||||
|
17.0219,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.8968,
|
||||||
|
241,
|
||||||
|
7.3157,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2126,
|
||||||
|
241,
|
||||||
|
13.2006,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4131,
|
||||||
|
241,
|
||||||
|
12.0053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "leave-requests",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 308,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
20.0913,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
20.0913,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
88.3503,
|
||||||
|
10,
|
||||||
|
11.6497,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
241,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
14.9787,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
86,
|
||||||
|
14.6461,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
86,
|
||||||
|
17.495,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
86,
|
||||||
|
17.495,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
86,
|
||||||
|
8.52,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
86,
|
||||||
|
12.74,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
86,
|
||||||
|
10.31,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
124,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
124,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
124,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
124,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
124,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
124,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
124,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
179,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
179,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
179,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
179,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
179,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
179,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
179,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
234,
|
||||||
|
14.9787,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.8863,
|
||||||
|
234,
|
||||||
|
14.6461,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
31.5324,
|
||||||
|
234,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.0274,
|
||||||
|
234,
|
||||||
|
17.495,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
66.5223,
|
||||||
|
234,
|
||||||
|
8.52,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.0424,
|
||||||
|
234,
|
||||||
|
12.74,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.7824,
|
||||||
|
234,
|
||||||
|
10.31,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "125231cbf4c6abc4e73bb48732dc9353"
|
||||||
|
}
|
||||||
620
src/admin/bones/offer-detail.bones.json
Normal file
620
src/admin/bones/offer-detail.bones.json
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 483,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.5356,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
86,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
146,
|
||||||
|
100,
|
||||||
|
338,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
159,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
187,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
159,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
187,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
249,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
249,
|
||||||
|
44.0171,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.2792,
|
||||||
|
278,
|
||||||
|
44.0171,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
34.4062,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
339,
|
||||||
|
34.8157,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
339,
|
||||||
|
36.6097,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
339,
|
||||||
|
36.6186,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
370,
|
||||||
|
34.4062,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
370,
|
||||||
|
34.8157,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
370,
|
||||||
|
36.6097,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
370,
|
||||||
|
36.6186,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
404,
|
||||||
|
34.4062,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
404,
|
||||||
|
34.8157,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
404,
|
||||||
|
36.6097,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
404,
|
||||||
|
36.6186,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
437,
|
||||||
|
34.4062,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.1099,
|
||||||
|
437,
|
||||||
|
34.8157,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.9256,
|
||||||
|
437,
|
||||||
|
36.6097,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.5353,
|
||||||
|
437,
|
||||||
|
36.6186,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 416,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
5.9783,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.4829,
|
||||||
|
7,
|
||||||
|
19.8412,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
90.8267,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
356,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
169,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
198,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
169,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
198,
|
||||||
|
46.3315,
|
||||||
|
46,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
260,
|
||||||
|
22.8558,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
260,
|
||||||
|
23.4333,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
260,
|
||||||
|
24.2718,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
260,
|
||||||
|
24.2761,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
292,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
292,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
292,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
292,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
327,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
327,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
327,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
327,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
22.8558,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.4373,
|
||||||
|
362,
|
||||||
|
23.4333,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.8706,
|
||||||
|
362,
|
||||||
|
24.2718,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.1424,
|
||||||
|
362,
|
||||||
|
24.2761,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offer-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 419,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
3.2129,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0878,
|
||||||
|
1,
|
||||||
|
14.6618,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
93.6229,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
48,
|
||||||
|
100,
|
||||||
|
371,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
67,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
94,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
67,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
94,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
151,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
178,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
151,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
178,
|
||||||
|
47.2892,
|
||||||
|
41,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
235,
|
||||||
|
23.2194,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
235,
|
||||||
|
23.9787,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
235,
|
||||||
|
24.4917,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
235,
|
||||||
|
24.4949,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
273,
|
||||||
|
23.2194,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
273,
|
||||||
|
23.9787,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
273,
|
||||||
|
24.4917,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
273,
|
||||||
|
24.4949,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
316,
|
||||||
|
23.2194,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
316,
|
||||||
|
23.9787,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
316,
|
||||||
|
24.4917,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
316,
|
||||||
|
24.4949,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
358,
|
||||||
|
23.2194,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.1271,
|
||||||
|
358,
|
||||||
|
23.9787,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1058,
|
||||||
|
358,
|
||||||
|
24.4917,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73.5975,
|
||||||
|
358,
|
||||||
|
24.4949,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "67676fde7dd5c432922d819fc9bf48db"
|
||||||
|
}
|
||||||
641
src/admin/bones/offers-customers.bones.json
Normal file
641
src/admin/bones/offers-customers.bones.json
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 549,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
186,
|
||||||
|
34.562,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
186,
|
||||||
|
23.7936,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
186,
|
||||||
|
32.4653,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
186,
|
||||||
|
51.6293,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
217,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
217,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
217,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
217,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
278,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
278,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
278,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
339,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
339,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
339,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
400,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
400,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
400,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
400,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
461,
|
||||||
|
34.562,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.2657,
|
||||||
|
461,
|
||||||
|
23.7936,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.0593,
|
||||||
|
461,
|
||||||
|
32.4653,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
94.5246,
|
||||||
|
461,
|
||||||
|
51.6293,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.9458,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.9458,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.6322,
|
||||||
|
4,
|
||||||
|
19.3678,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
23.2868,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
146,
|
||||||
|
16.453,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
146,
|
||||||
|
21.9175,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
146,
|
||||||
|
33.1798,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
179,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
179,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
179,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
240,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
240,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
240,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
301,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
301,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
301,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
362,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
362,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
362,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
23.2868,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.8683,
|
||||||
|
423,
|
||||||
|
16.453,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42.3212,
|
||||||
|
423,
|
||||||
|
21.9175,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64.2387,
|
||||||
|
423,
|
||||||
|
33.1798,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers-customers",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
9.5664,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
9.5664,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.0897,
|
||||||
|
10,
|
||||||
|
13.9103,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
25.673,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
138,
|
||||||
|
19.0904,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
138,
|
||||||
|
24.3254,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
138,
|
||||||
|
27.0959,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
176,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
176,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
176,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
231,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
231,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
231,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
286,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
286,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
286,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
341,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
341,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
341,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
25.673,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.5806,
|
||||||
|
396,
|
||||||
|
19.0904,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
46.6711,
|
||||||
|
396,
|
||||||
|
24.3254,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.9965,
|
||||||
|
396,
|
||||||
|
27.0959,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "63b2dec2b6ceb84d931a000ab8b669dd"
|
||||||
|
}
|
||||||
452
src/admin/bones/offers-templates.bones.json
Normal file
452
src/admin/bones/offers-templates.bones.json
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 436,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
13,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
73,
|
||||||
|
39.659,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
73,
|
||||||
|
39.6857,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
73,
|
||||||
|
63.1054,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
104,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
104,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
104,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
165,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
165,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
226,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
226,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
287,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
287,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
39.659,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43.3627,
|
||||||
|
348,
|
||||||
|
39.6857,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.0484,
|
||||||
|
348,
|
||||||
|
63.1054,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 435,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
19,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
26.9744,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
79,
|
||||||
|
26.9977,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
79,
|
||||||
|
40.8649,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
112,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
112,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
112,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
173,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
173,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
173,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
234,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
234,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
234,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
295,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
295,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
295,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
356,
|
||||||
|
26.9744,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5559,
|
||||||
|
356,
|
||||||
|
26.9977,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.5536,
|
||||||
|
356,
|
||||||
|
40.8649,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers-templates",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 403,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
19,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
71,
|
||||||
|
30.8719,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
71,
|
||||||
|
30.897,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
71,
|
||||||
|
34.4158,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
109,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
109,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
109,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
164,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
164,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
164,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
219,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
219,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
219,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
274,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
274,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
274,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
329,
|
||||||
|
30.8719,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7796,
|
||||||
|
329,
|
||||||
|
30.897,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63.6766,
|
||||||
|
329,
|
||||||
|
34.4158,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "5e5881859bd932a42345c69a6a30ca65"
|
||||||
|
}
|
||||||
872
src/admin/bones/offers.bones.json
Normal file
872
src/admin/bones/offers.bones.json
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
26.7495,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
134,
|
||||||
|
20.6019,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
134,
|
||||||
|
21.4165,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
134,
|
||||||
|
30.7692,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
165,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
165,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
165,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
226,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
226,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
226,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
287,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
287,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
287,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
348,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
348,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
348,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
409,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.055,
|
||||||
|
409,
|
||||||
|
21.4165,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.4715,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95.5217,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118.5719,
|
||||||
|
409,
|
||||||
|
30.7692,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.3451,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.3451,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
17.6779,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
146,
|
||||||
|
13.7101,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
146,
|
||||||
|
13.3301,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
146,
|
||||||
|
15.2917,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
146,
|
||||||
|
15.2917,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
146,
|
||||||
|
19.5355,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
179,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
179,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
179,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
179,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
179,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
240,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
240,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
240,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
240,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
240,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
301,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
301,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
301,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
301,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
301,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
362,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
362,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
362,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
362,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
362,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
17.6779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2594,
|
||||||
|
423,
|
||||||
|
13.7101,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.9695,
|
||||||
|
423,
|
||||||
|
13.3301,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2996,
|
||||||
|
423,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.5913,
|
||||||
|
423,
|
||||||
|
15.2917,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
77.883,
|
||||||
|
423,
|
||||||
|
19.5355,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "offers",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
8.3835,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
8.3835,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
18.8912,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
138,
|
||||||
|
15.0085,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
138,
|
||||||
|
13.333,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
138,
|
||||||
|
16.5553,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
138,
|
||||||
|
16.5553,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
138,
|
||||||
|
15.8415,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
176,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
176,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
176,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
176,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
176,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
231,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
231,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
231,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
231,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
231,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
286,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
286,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
286,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
286,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
286,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
341,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
341,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
341,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
341,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
341,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
18.8912,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.7988,
|
||||||
|
396,
|
||||||
|
15.0085,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.8073,
|
||||||
|
396,
|
||||||
|
13.333,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
49.1403,
|
||||||
|
396,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.6956,
|
||||||
|
396,
|
||||||
|
16.5553,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.2509,
|
||||||
|
396,
|
||||||
|
15.8415,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "62d793eb0343d832087d687b76639e09"
|
||||||
|
}
|
||||||
998
src/admin/bones/orders.bones.json
Normal file
998
src/admin/bones/orders.bones.json
Normal file
@@ -0,0 +1,998 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
26.7495,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
134,
|
||||||
|
29.1043,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
134,
|
||||||
|
20.6019,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
134,
|
||||||
|
26.6515,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
134,
|
||||||
|
23.0502,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
134,
|
||||||
|
21.2028,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
134,
|
||||||
|
17.094,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
165,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
165,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
165,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
165,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
165,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
165,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
226,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
226,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
226,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
226,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
226,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
226,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
287,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
287,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
287,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
287,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
287,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
287,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
348,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
348,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
348,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
348,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
348,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
348,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
26.7495,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.4532,
|
||||||
|
409,
|
||||||
|
29.1043,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.5575,
|
||||||
|
409,
|
||||||
|
20.6019,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
80.1594,
|
||||||
|
409,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
106.8109,
|
||||||
|
409,
|
||||||
|
23.0502,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
129.8611,
|
||||||
|
409,
|
||||||
|
21.2028,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
151.0639,
|
||||||
|
409,
|
||||||
|
17.094,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.6461,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.6461,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
15.642,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
146,
|
||||||
|
16.9837,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
146,
|
||||||
|
12.1306,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
146,
|
||||||
|
14.5338,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
146,
|
||||||
|
13.5296,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
146,
|
||||||
|
12.4745,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
146,
|
||||||
|
9.5427,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
179,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
179,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
179,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
179,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
179,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
179,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
240,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
240,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
240,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
240,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
240,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
240,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
301,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
301,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
301,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
301,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
301,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
301,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
362,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
362,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
362,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
362,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
362,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
362,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
15.642,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.2235,
|
||||||
|
423,
|
||||||
|
16.9837,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.2072,
|
||||||
|
423,
|
||||||
|
12.1306,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.3378,
|
||||||
|
423,
|
||||||
|
14.5338,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
61.8716,
|
||||||
|
423,
|
||||||
|
13.5296,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75.4012,
|
||||||
|
423,
|
||||||
|
12.4745,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.8758,
|
||||||
|
423,
|
||||||
|
9.5427,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "orders",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.3008,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.3008,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
16.2243,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
138,
|
||||||
|
17.5044,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
138,
|
||||||
|
12.8891,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
138,
|
||||||
|
13.7535,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
138,
|
||||||
|
14.2178,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
138,
|
||||||
|
13.2138,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
138,
|
||||||
|
8.382,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
176,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
176,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
176,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
176,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
176,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
176,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
231,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
231,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
231,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
231,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
231,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
231,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
286,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
286,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
286,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
286,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
286,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
286,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
341,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
341,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
341,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
341,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
341,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
341,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
16.2243,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.1319,
|
||||||
|
396,
|
||||||
|
17.5044,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.6363,
|
||||||
|
396,
|
||||||
|
12.8891,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.5254,
|
||||||
|
396,
|
||||||
|
13.7535,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
62.2788,
|
||||||
|
396,
|
||||||
|
14.2178,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76.4966,
|
||||||
|
396,
|
||||||
|
13.2138,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.7104,
|
||||||
|
396,
|
||||||
|
8.382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "677a0002aa805c9f7790bc68c6374bb5"
|
||||||
|
}
|
||||||
371
src/admin/bones/project-detail.bones.json
Normal file
371
src/admin/bones/project-detail.bones.json
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 481,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
3.4188,
|
||||||
|
12,
|
||||||
|
5.698,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8148,
|
||||||
|
9,
|
||||||
|
50.2315,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
52,
|
||||||
|
48.0057,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.4245,
|
||||||
|
52,
|
||||||
|
48.5755,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
100,
|
||||||
|
188,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
125,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
154,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
125,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
154,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
214,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
243,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
214,
|
||||||
|
48.1481,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56.4103,
|
||||||
|
243,
|
||||||
|
48.1481,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
316,
|
||||||
|
100,
|
||||||
|
165,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
329,
|
||||||
|
92.5926,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
357,
|
||||||
|
92.5926,
|
||||||
|
104,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 453,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
1.6304,
|
||||||
|
12,
|
||||||
|
2.7174,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.0652,
|
||||||
|
7,
|
||||||
|
29.2799,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
78.2375,
|
||||||
|
0,
|
||||||
|
9.1733,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
89.0413,
|
||||||
|
0,
|
||||||
|
10.9587,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
79,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
108,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
168,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
197,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
168,
|
||||||
|
46.3315,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.087,
|
||||||
|
197,
|
||||||
|
46.3315,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
276,
|
||||||
|
100,
|
||||||
|
177,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
295,
|
||||||
|
94.837,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
323,
|
||||||
|
94.837,
|
||||||
|
104,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "project-detail",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 404,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0.6024,
|
||||||
|
7,
|
||||||
|
2.008,
|
||||||
|
20,
|
||||||
|
"50%"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.0161,
|
||||||
|
2,
|
||||||
|
21.6365,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7217,
|
||||||
|
0,
|
||||||
|
6.3771,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
92.3036,
|
||||||
|
0,
|
||||||
|
7.6964,
|
||||||
|
34,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
180,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
69,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
96,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
148,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
175,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
148,
|
||||||
|
47.2892,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
50.8032,
|
||||||
|
175,
|
||||||
|
47.2892,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
246,
|
||||||
|
100,
|
||||||
|
157,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
265,
|
||||||
|
96.1847,
|
||||||
|
17,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
294,
|
||||||
|
96.1847,
|
||||||
|
84,
|
||||||
|
8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "ab5e1f108d42c55b0e6382fcaffff793"
|
||||||
|
}
|
||||||
746
src/admin/bones/projects.bones.json
Normal file
746
src/admin/bones/projects.bones.json
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 497,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
74,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
134,
|
||||||
|
34.6065,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
134,
|
||||||
|
31.3568,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
134,
|
||||||
|
26.6515,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
134,
|
||||||
|
27.7066,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
134,
|
||||||
|
22.1287,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
165,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
165,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
165,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
165,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
165,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
226,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
226,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
226,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
226,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
226,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
287,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
287,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
287,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
287,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
287,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
348,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
348,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
348,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
348,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
348,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
409,
|
||||||
|
34.6065,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.3102,
|
||||||
|
409,
|
||||||
|
31.3568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.667,
|
||||||
|
409,
|
||||||
|
26.6515,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
96.3186,
|
||||||
|
409,
|
||||||
|
27.7066,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124.0251,
|
||||||
|
409,
|
||||||
|
22.1287,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
10.9099,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
10.9099,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
23.429,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
146,
|
||||||
|
21.2806,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
146,
|
||||||
|
18.1704,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
146,
|
||||||
|
17.6694,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
146,
|
||||||
|
14.2875,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
179,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
179,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
179,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
179,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
240,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
240,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
240,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
240,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
301,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
301,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
301,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
301,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
362,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
362,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
362,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
362,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
23.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.0105,
|
||||||
|
423,
|
||||||
|
21.2806,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.2911,
|
||||||
|
423,
|
||||||
|
18.1704,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.4615,
|
||||||
|
423,
|
||||||
|
17.6694,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
83.1309,
|
||||||
|
423,
|
||||||
|
14.2875,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "projects",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 470,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
8.0619,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
8.0619,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
24.4588,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
138,
|
||||||
|
22.4068,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
138,
|
||||||
|
19.4308,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
138,
|
||||||
|
17.2612,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
138,
|
||||||
|
12.6271,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
176,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
176,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
176,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
176,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
231,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
231,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
231,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
231,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
231,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
286,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
286,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
286,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
286,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
286,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
341,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
341,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
341,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
341,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
341,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
396,
|
||||||
|
24.4588,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.3664,
|
||||||
|
396,
|
||||||
|
22.4068,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
48.7732,
|
||||||
|
396,
|
||||||
|
19.4308,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68.2041,
|
||||||
|
396,
|
||||||
|
17.2612,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4653,
|
||||||
|
396,
|
||||||
|
12.6271,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "17f8285c3ca514ddef6d48c1183ed642"
|
||||||
|
}
|
||||||
50
src/admin/bones/registry.ts
Normal file
50
src/admin/bones/registry.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client"
|
||||||
|
// Auto-generated by `npx boneyard-js build` — do not edit
|
||||||
|
import { registerBones } from 'boneyard-js'
|
||||||
|
import { configureBoneyard } from 'boneyard-js/react'
|
||||||
|
|
||||||
|
import _dash_sessions from './dash-sessions.bones.json'
|
||||||
|
import _attendance_history_fund from './attendance-history-fund.bones.json'
|
||||||
|
import _attendance_history_table from './attendance-history-table.bones.json'
|
||||||
|
import _leave_requests from './leave-requests.bones.json'
|
||||||
|
import _leave_approval from './leave-approval.bones.json'
|
||||||
|
import _attendance_balances from './attendance-balances.bones.json'
|
||||||
|
import _trips_history from './trips-history.bones.json'
|
||||||
|
import _trips_admin from './trips-admin.bones.json'
|
||||||
|
import _vehicles from './vehicles.bones.json'
|
||||||
|
import _offers from './offers.bones.json'
|
||||||
|
import _orders from './orders.bones.json'
|
||||||
|
import _projects from './projects.bones.json'
|
||||||
|
import _offers_customers from './offers-customers.bones.json'
|
||||||
|
import _users from './users.bones.json'
|
||||||
|
import _audit_log_rows from './audit-log-rows.bones.json'
|
||||||
|
import _offer_detail from './offer-detail.bones.json'
|
||||||
|
import _invoice_detail from './invoice-detail.bones.json'
|
||||||
|
import _project_detail from './project-detail.bones.json'
|
||||||
|
import _attendance_create from './attendance-create.bones.json'
|
||||||
|
import _offers_templates from './offers-templates.bones.json'
|
||||||
|
|
||||||
|
configureBoneyard({"color":"#e0e0e0","animate":"shimmer","shimmerColor":"#f0f0f0","speed":"1.2s","shimmerAngle":110})
|
||||||
|
|
||||||
|
registerBones({
|
||||||
|
"dash-sessions": _dash_sessions,
|
||||||
|
"attendance-history-fund": _attendance_history_fund,
|
||||||
|
"attendance-history-table": _attendance_history_table,
|
||||||
|
"leave-requests": _leave_requests,
|
||||||
|
"leave-approval": _leave_approval,
|
||||||
|
"attendance-balances": _attendance_balances,
|
||||||
|
"trips-history": _trips_history,
|
||||||
|
"trips-admin": _trips_admin,
|
||||||
|
"vehicles": _vehicles,
|
||||||
|
"offers": _offers,
|
||||||
|
"orders": _orders,
|
||||||
|
"projects": _projects,
|
||||||
|
"offers-customers": _offers_customers,
|
||||||
|
"users": _users,
|
||||||
|
"audit-log-rows": _audit_log_rows,
|
||||||
|
"offer-detail": _offer_detail,
|
||||||
|
"invoice-detail": _invoice_detail,
|
||||||
|
"project-detail": _project_detail,
|
||||||
|
"attendance-create": _attendance_create,
|
||||||
|
"offers-templates": _offers_templates,
|
||||||
|
})
|
||||||
725
src/admin/bones/trips-admin.bones.json
Normal file
725
src/admin/bones/trips-admin.bones.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 317,
|
||||||
|
"width": 317,
|
||||||
|
"height": 437,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
376,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
74,
|
||||||
|
37.8795,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
74,
|
||||||
|
43.4592,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
74,
|
||||||
|
31.6739,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
74,
|
||||||
|
16.6108,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
74,
|
||||||
|
28.1053,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
105,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
105,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
105,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
105,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
105,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
166,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
166,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
166,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
166,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
166,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
227,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
227,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
227,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
227,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
227,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
288,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
288,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
288,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
288,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
288,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
349,
|
||||||
|
37.8795,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.9805,
|
||||||
|
349,
|
||||||
|
43.4592,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85.4397,
|
||||||
|
349,
|
||||||
|
31.6739,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
117.1136,
|
||||||
|
349,
|
||||||
|
16.6108,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
133.7244,
|
||||||
|
349,
|
||||||
|
28.1053,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 690,
|
||||||
|
"width": 690,
|
||||||
|
"height": 442,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.3202,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.3202,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
375,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
86,
|
||||||
|
22.8057,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
86,
|
||||||
|
26.0711,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
86,
|
||||||
|
19.1757,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
86,
|
||||||
|
10.3601,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
86,
|
||||||
|
16.0802,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
119,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
119,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
119,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
119,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
119,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
180,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
180,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
180,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
180,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
180,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
241,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
241,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
241,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
241,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
241,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
302,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
302,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
302,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
302,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
302,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
363,
|
||||||
|
22.8057,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.5593,
|
||||||
|
363,
|
||||||
|
26.0711,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
51.6304,
|
||||||
|
363,
|
||||||
|
19.1757,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.8062,
|
||||||
|
363,
|
||||||
|
10.3601,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1662,
|
||||||
|
363,
|
||||||
|
16.0802,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "trips-admin",
|
||||||
|
"viewportWidth": 950,
|
||||||
|
"width": 950,
|
||||||
|
"height": 418,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.8536,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.8536,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
351,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
86,
|
||||||
|
23.523,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
86,
|
||||||
|
26.574,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
86,
|
||||||
|
20.1382,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
86,
|
||||||
|
11.9046,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
86,
|
||||||
|
13.8602,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
124,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
124,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
124,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
124,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
124,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
179,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
179,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
179,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
179,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
179,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
234,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
234,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
234,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
234,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
234,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
289,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
289,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
289,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
289,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
289,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
344,
|
||||||
|
23.523,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
25.523,
|
||||||
|
344,
|
||||||
|
26.574,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.097,
|
||||||
|
344,
|
||||||
|
20.1382,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72.2352,
|
||||||
|
344,
|
||||||
|
11.9046,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.1398,
|
||||||
|
344,
|
||||||
|
13.8602,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "39a325f430c84bb51960a684759a8f0c"
|
||||||
|
}
|
||||||
725
src/admin/bones/trips-history.bones.json
Normal file
725
src/admin/bones/trips-history.bones.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 317,
|
||||||
|
"width": 317,
|
||||||
|
"height": 300,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
61,
|
||||||
|
100,
|
||||||
|
239,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
74,
|
||||||
|
35.2326,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
74,
|
||||||
|
40.4278,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
74,
|
||||||
|
29.4657,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
74,
|
||||||
|
37.1402,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
74,
|
||||||
|
15.4623,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
105,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
105,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
105,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
105,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
105,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
138,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
138,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
138,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
138,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
138,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
172,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
172,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
172,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
172,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
172,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
205,
|
||||||
|
35.2326,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
205,
|
||||||
|
40.4278,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
205,
|
||||||
|
29.4657,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
205,
|
||||||
|
37.1402,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
205,
|
||||||
|
15.4623,
|
||||||
|
34,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4.1009,
|
||||||
|
239,
|
||||||
|
35.2326,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
39.3336,
|
||||||
|
239,
|
||||||
|
40.4278,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79.7614,
|
||||||
|
239,
|
||||||
|
29.4657,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
109.2271,
|
||||||
|
239,
|
||||||
|
37.1402,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146.3673,
|
||||||
|
239,
|
||||||
|
15.4623,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 690,
|
||||||
|
"width": 690,
|
||||||
|
"height": 312,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
16.6033,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
16.6033,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
245,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
86,
|
||||||
|
21.0417,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
86,
|
||||||
|
24.0534,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
86,
|
||||||
|
17.6925,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
86,
|
||||||
|
22.1445,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
86,
|
||||||
|
9.5607,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
119,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
119,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
119,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
119,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
119,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
154,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
154,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
154,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
154,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
154,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
189,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
189,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
189,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
189,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
189,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
224,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
224,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
224,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
224,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
224,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.7536,
|
||||||
|
259,
|
||||||
|
21.0417,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7953,
|
||||||
|
259,
|
||||||
|
24.0534,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.8487,
|
||||||
|
259,
|
||||||
|
17.6925,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.5412,
|
||||||
|
259,
|
||||||
|
22.1445,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.6857,
|
||||||
|
259,
|
||||||
|
9.5607,
|
||||||
|
35,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "trips-history",
|
||||||
|
"viewportWidth": 958,
|
||||||
|
"width": 958,
|
||||||
|
"height": 355,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
11.9585,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
11.9585,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
288,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
86,
|
||||||
|
21.1541,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
86,
|
||||||
|
23.8974,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
86,
|
||||||
|
18.1106,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
86,
|
||||||
|
22.1604,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
86,
|
||||||
|
10.7108,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
124,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
124,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
124,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
124,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
124,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
167,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
167,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
167,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
167,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
167,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
209,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
209,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
209,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
209,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
209,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
252,
|
||||||
|
21.1541,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
252,
|
||||||
|
23.8974,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
252,
|
||||||
|
18.1106,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
252,
|
||||||
|
22.1604,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
252,
|
||||||
|
10.7108,
|
||||||
|
43,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9833,
|
||||||
|
294,
|
||||||
|
21.1541,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.1374,
|
||||||
|
294,
|
||||||
|
23.8974,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.0348,
|
||||||
|
294,
|
||||||
|
18.1106,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
65.1455,
|
||||||
|
294,
|
||||||
|
22.1604,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.3059,
|
||||||
|
294,
|
||||||
|
10.7108,
|
||||||
|
42,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "6b54a0afbb4863895e318916b1fdca67"
|
||||||
|
}
|
||||||
767
src/admin/bones/users.bones.json
Normal file
767
src/admin/bones/users.bones.json
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 549,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
26,
|
||||||
|
100,
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
113,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
126,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
186,
|
||||||
|
37.362,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
186,
|
||||||
|
26.429,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
186,
|
||||||
|
36.1779,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
186,
|
||||||
|
23.62,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
186,
|
||||||
|
18.8613,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
217,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
217,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
217,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
217,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
217,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
278,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
278,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
278,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
278,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
278,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
339,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
339,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
339,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
339,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
339,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
400,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
400,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
400,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
400,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
400,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
461,
|
||||||
|
37.362,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
41.0657,
|
||||||
|
461,
|
||||||
|
26.429,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.4947,
|
||||||
|
461,
|
||||||
|
36.1779,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
103.6725,
|
||||||
|
461,
|
||||||
|
23.62,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127.2926,
|
||||||
|
461,
|
||||||
|
18.8613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 502,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12.6741,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
12.6741,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.2479,
|
||||||
|
4,
|
||||||
|
18.7521,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
86,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
146,
|
||||||
|
24.3079,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
146,
|
||||||
|
18.6481,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
146,
|
||||||
|
23.5628,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
146,
|
||||||
|
15.6568,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
146,
|
||||||
|
12.6613,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
179,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
179,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
179,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
179,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
179,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
240,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
240,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
240,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
240,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
240,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
301,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
301,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
301,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
301,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
301,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
362,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
362,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
362,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
362,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
362,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
423,
|
||||||
|
24.3079,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
26.8894,
|
||||||
|
423,
|
||||||
|
18.6481,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45.5375,
|
||||||
|
423,
|
||||||
|
23.5628,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
69.1003,
|
||||||
|
423,
|
||||||
|
15.6568,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
84.7571,
|
||||||
|
423,
|
||||||
|
12.6613,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "users",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 505,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
9.3656,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
30,
|
||||||
|
9.3656,
|
||||||
|
21,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.5446,
|
||||||
|
10,
|
||||||
|
13.4554,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
100,
|
||||||
|
438,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
86,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
138,
|
||||||
|
25.3655,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
138,
|
||||||
|
20.4302,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
138,
|
||||||
|
22.8571,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
138,
|
||||||
|
15.9011,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
138,
|
||||||
|
11.6309,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
176,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
176,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
176,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
176,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
176,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
238,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
238,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
238,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
238,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
238,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
300,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
300,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
300,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
300,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
300,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
362,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
362,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
362,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
362,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
362,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
424,
|
||||||
|
25.3655,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.2732,
|
||||||
|
424,
|
||||||
|
20.4302,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47.7033,
|
||||||
|
424,
|
||||||
|
22.8571,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70.5604,
|
||||||
|
424,
|
||||||
|
15.9011,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86.4615,
|
||||||
|
424,
|
||||||
|
11.6309,
|
||||||
|
62,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "53e8df6c8f8bf975b3b88bfca3bbd804"
|
||||||
|
}
|
||||||
746
src/admin/bones/vehicles.bones.json
Normal file
746
src/admin/bones/vehicles.bones.json
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": {
|
||||||
|
"375": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 351,
|
||||||
|
"width": 351,
|
||||||
|
"height": 530,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
22,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
34,
|
||||||
|
100,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
94,
|
||||||
|
100,
|
||||||
|
436,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
107,
|
||||||
|
92.5926,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
167,
|
||||||
|
23.9583,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
167,
|
||||||
|
24.4168,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
167,
|
||||||
|
29.042,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
167,
|
||||||
|
18.8435,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
167,
|
||||||
|
46.1895,
|
||||||
|
31,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
197,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
197,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
197,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
197,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
197,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
258,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
258,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
258,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
258,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
258,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
319,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
319,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
319,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
319,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
319,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
380,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
380,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
380,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
380,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
380,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.7037,
|
||||||
|
441,
|
||||||
|
23.9583,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
27.662,
|
||||||
|
441,
|
||||||
|
24.4168,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
52.0789,
|
||||||
|
441,
|
||||||
|
29.042,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81.1209,
|
||||||
|
441,
|
||||||
|
18.8435,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
99.9644,
|
||||||
|
441,
|
||||||
|
46.1895,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"768": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 736,
|
||||||
|
"width": 736,
|
||||||
|
"height": 495,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
10.1478,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82.6427,
|
||||||
|
0,
|
||||||
|
17.3573,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
60,
|
||||||
|
100,
|
||||||
|
435,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
79,
|
||||||
|
94.837,
|
||||||
|
44,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
139,
|
||||||
|
16.4126,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
139,
|
||||||
|
16.5039,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
139,
|
||||||
|
19.5058,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
139,
|
||||||
|
12.8843,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
139,
|
||||||
|
29.5304,
|
||||||
|
33,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
172,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
172,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
172,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
172,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
172,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
233,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
233,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
233,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
233,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
233,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
294,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
294,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
294,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
294,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
294,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
355,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
355,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
355,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
355,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
355,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.5815,
|
||||||
|
416,
|
||||||
|
16.4126,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
18.9941,
|
||||||
|
416,
|
||||||
|
16.5039,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35.498,
|
||||||
|
416,
|
||||||
|
19.5058,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55.0038,
|
||||||
|
416,
|
||||||
|
12.8843,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
67.8881,
|
||||||
|
416,
|
||||||
|
29.5304,
|
||||||
|
61,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1280": {
|
||||||
|
"name": "vehicles",
|
||||||
|
"viewportWidth": 996,
|
||||||
|
"width": 996,
|
||||||
|
"height": 451,
|
||||||
|
"bones": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
7.4987,
|
||||||
|
26,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
87.5753,
|
||||||
|
0,
|
||||||
|
12.4247,
|
||||||
|
32,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
48,
|
||||||
|
100,
|
||||||
|
403,
|
||||||
|
10,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
67,
|
||||||
|
96.1847,
|
||||||
|
36,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
119,
|
||||||
|
18.3531,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
119,
|
||||||
|
18.2762,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
119,
|
||||||
|
21.1785,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
119,
|
||||||
|
14.7841,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
119,
|
||||||
|
23.5928,
|
||||||
|
38,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
157,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
157,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
157,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
157,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
157,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
212,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
212,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
212,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
212,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
212,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
267,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
267,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
267,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
267,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
267,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
322,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
322,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
322,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
322,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
322,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9076,
|
||||||
|
377,
|
||||||
|
18.3531,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.2607,
|
||||||
|
377,
|
||||||
|
18.2762,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
38.537,
|
||||||
|
377,
|
||||||
|
21.1785,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
59.7154,
|
||||||
|
377,
|
||||||
|
14.7841,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74.4996,
|
||||||
|
377,
|
||||||
|
23.5928,
|
||||||
|
55,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_hash": "567bad6080dc9ba9767c6e40a88559b9"
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { projectFilesOptions } from "../lib/queries/projects";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import ConfirmModal from "./ConfirmModal";
|
import ConfirmModal from "./ConfirmModal";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import ProjectFileManagerFixture from "../fixtures/ProjectFileManagerFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -196,14 +200,11 @@ export default function ProjectFileManager({
|
|||||||
hasNasFolder,
|
hasNasFolder,
|
||||||
}: ProjectFileManagerProps) {
|
}: ProjectFileManagerProps) {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const isCancelling = useRef(false);
|
const isCancelling = useRef(false);
|
||||||
|
|
||||||
const [items, setItems] = useState<FileItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [currentPath, setCurrentPath] = useState("");
|
const [currentPath, setCurrentPath] = useState("");
|
||||||
const [breadcrumb, setBreadcrumb] = useState<string[]>([""]);
|
|
||||||
const [fullPath, setFullPath] = useState("");
|
|
||||||
|
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -217,59 +218,25 @@ export default function ProjectFileManager({
|
|||||||
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const canManage = hasPermission("projects.files");
|
const canManage = hasPermission("projects.files");
|
||||||
|
|
||||||
const fetchFiles = useCallback(
|
const {
|
||||||
async (path = "", options: { ignore?: boolean } = {}) => {
|
data: filesData,
|
||||||
setLoading(true);
|
isPending: filesLoading,
|
||||||
setErrorMessage(null);
|
error: filesError,
|
||||||
try {
|
} = useQuery(projectFilesOptions(projectId, currentPath));
|
||||||
const params = new URLSearchParams({ project_id: String(projectId) });
|
const items = filesData?.items ?? [];
|
||||||
if (path) {
|
const breadcrumb = filesData?.breadcrumb ?? [""];
|
||||||
params.set("path", path);
|
const fullPath = filesData?.full_path ?? "";
|
||||||
}
|
const errorMessage = filesError
|
||||||
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
|
? filesError.message || "Nepodařilo se načíst soubory"
|
||||||
if (options.ignore) return;
|
: null;
|
||||||
if (res.status === 401) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
setItems(data.data.items || []);
|
|
||||||
setBreadcrumb(data.data.breadcrumb || [""]);
|
|
||||||
setCurrentPath(data.data.path || "");
|
|
||||||
setFullPath(data.data.full_path || "");
|
|
||||||
} else if (res.status === 404) {
|
|
||||||
setItems([]);
|
|
||||||
setBreadcrumb([""]);
|
|
||||||
} else {
|
|
||||||
setErrorMessage(data.error || "Nepodařilo se načíst soubory");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!options.ignore) {
|
|
||||||
setErrorMessage("Chyba připojení");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!options.ignore) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectId],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const opts = { ignore: false };
|
|
||||||
fetchFiles("", opts);
|
|
||||||
return () => {
|
|
||||||
opts.ignore = true;
|
|
||||||
};
|
|
||||||
}, [fetchFiles]);
|
|
||||||
|
|
||||||
const navigateTo = (path: string) => {
|
const navigateTo = (path: string) => {
|
||||||
setNewFolderMode(false);
|
setNewFolderMode(false);
|
||||||
setRenamingItem(null);
|
setRenamingItem(null);
|
||||||
fetchFiles(path);
|
setCurrentPath(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBreadcrumbClick = (index: number) => {
|
const handleBreadcrumbClick = (index: number) => {
|
||||||
@@ -332,7 +299,9 @@ export default function ProjectFileManager({
|
|||||||
? "Soubor byl nahrán"
|
? "Soubor byl nahrán"
|
||||||
: `Nahráno ${successCount} souborů`;
|
: `Nahráno ${successCount} souborů`;
|
||||||
alert.success(msg);
|
alert.success(msg);
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
alert.error(errorMsg);
|
alert.error(errorMsg);
|
||||||
@@ -383,7 +352,9 @@ export default function ProjectFileManager({
|
|||||||
alert.success("Složka byla vytvořena");
|
alert.success("Složka byla vytvořena");
|
||||||
setNewFolderMode(false);
|
setNewFolderMode(false);
|
||||||
setNewFolderName("");
|
setNewFolderName("");
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se vytvořit složku");
|
alert.error(data.error || "Nepodařilo se vytvořit složku");
|
||||||
}
|
}
|
||||||
@@ -444,7 +415,9 @@ export default function ProjectFileManager({
|
|||||||
? "Složka byla smazána"
|
? "Složka byla smazána"
|
||||||
: "Soubor byl smazán",
|
: "Soubor byl smazán",
|
||||||
);
|
);
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se smazat");
|
alert.error(data.error || "Nepodařilo se smazat");
|
||||||
}
|
}
|
||||||
@@ -479,7 +452,9 @@ export default function ProjectFileManager({
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success("Přejmenováno");
|
alert.success("Přejmenováno");
|
||||||
fetchFiles(currentPath);
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["projects", String(projectId), "files"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se přejmenovat");
|
alert.error(data.error || "Nepodařilo se přejmenovat");
|
||||||
}
|
}
|
||||||
@@ -495,32 +470,15 @@ export default function ProjectFileManager({
|
|||||||
setRenameValue(item.name);
|
setRenameValue(item.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && items.length === 0 && !errorMessage) {
|
if (filesLoading && items.length === 0 && !errorMessage) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-card">
|
<Skeleton
|
||||||
<div className="admin-card-body">
|
name="project-file-manager"
|
||||||
<h3 className="admin-card-title">Soubory</h3>
|
loading={filesLoading && items.length === 0}
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "0.5rem" }}>
|
fixture={<ProjectFileManagerFixture />}
|
||||||
{[0, 1, 2, 3].map((i) => (
|
>
|
||||||
<div key={i} className="admin-skeleton-row">
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: `${60 + i * 10}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +668,7 @@ export default function ProjectFileManager({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{items.length === 0 && !loading ? (
|
{items.length === 0 && !filesLoading ? (
|
||||||
<div className="fm-empty">
|
<div className="fm-empty">
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAlert } from "../../context/AlertContext";
|
import { useAlert } from "../../context/AlertContext";
|
||||||
import ConfirmModal from "../ConfirmModal";
|
import ConfirmModal from "../ConfirmModal";
|
||||||
import useModalLock from "../../hooks/useModalLock";
|
import useModalLock from "../../hooks/useModalLock";
|
||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
||||||
|
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import DashSessionsFixture from "../../fixtures/DashSessionsFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface DeviceInfo {
|
|
||||||
icon?: string;
|
|
||||||
browser?: string;
|
|
||||||
os?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Session {
|
|
||||||
id: number | string;
|
|
||||||
is_current: boolean;
|
|
||||||
device_info?: DeviceInfo;
|
|
||||||
ip_address: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteModalState {
|
interface DeleteModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
@@ -77,9 +67,10 @@ function getDeviceIcon(iconType?: string) {
|
|||||||
|
|
||||||
export default function DashSessions() {
|
export default function DashSessions() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [sessions, setSessions] = useState<Session[]>([]);
|
const { data: sessions = [], isPending: sessionsLoading } =
|
||||||
const [sessionsLoading, setSessionsLoading] = useState(true);
|
useQuery(sessionsOptions());
|
||||||
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({
|
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
session: null,
|
session: null,
|
||||||
@@ -89,26 +80,6 @@ export default function DashSessions() {
|
|||||||
|
|
||||||
useModalLock(deleteAllModal);
|
useModalLock(deleteAllModal);
|
||||||
|
|
||||||
const fetchSessions = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/sessions`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setSessions(
|
|
||||||
Array.isArray(data.data) ? data.data : data.data?.sessions || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// session fetch failed silently
|
|
||||||
} finally {
|
|
||||||
setSessionsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSessions();
|
|
||||||
}, [fetchSessions]);
|
|
||||||
|
|
||||||
const handleDeleteSession = async () => {
|
const handleDeleteSession = async () => {
|
||||||
if (!deleteModal.session) {
|
if (!deleteModal.session) {
|
||||||
return;
|
return;
|
||||||
@@ -122,7 +93,7 @@ export default function DashSessions() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setDeleteModal({ isOpen: false, session: null });
|
setDeleteModal({ isOpen: false, session: null });
|
||||||
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||||
alert.success("Relace byla ukončena");
|
alert.success("Relace byla ukončena");
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se ukončit relaci");
|
alert.error(data.error || "Nepodařilo se ukončit relaci");
|
||||||
@@ -143,7 +114,7 @@ export default function DashSessions() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setDeleteAllModal(false);
|
setDeleteAllModal(false);
|
||||||
setSessions((prev) => prev.filter((s) => s.is_current));
|
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||||
alert.success(data.message || "Ostatní relace byly ukončeny");
|
alert.success(data.message || "Ostatní relace byly ukončeny");
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se ukončit relace");
|
alert.error(data.error || "Nepodařilo se ukončit relace");
|
||||||
@@ -183,98 +154,84 @@ export default function DashSessions() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
{sessionsLoading && (
|
<Skeleton
|
||||||
<div
|
name="dash-sessions"
|
||||||
className="admin-skeleton"
|
loading={sessionsLoading}
|
||||||
style={{ padding: "1rem", gap: "1rem" }}
|
fixture={<DashSessionsFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2].map((i) => (
|
<>
|
||||||
<div key={i} className="admin-skeleton-row">
|
{sessions.length === 0 && (
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/2"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!sessionsLoading && sessions.length === 0 && (
|
|
||||||
<div
|
|
||||||
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) => (
|
|
||||||
<div
|
<div
|
||||||
key={session.id}
|
className="text-secondary"
|
||||||
className={`dash-session-item ${session.is_current ? "dash-session-item-current" : ""}`}
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="dash-session-icon">
|
Žádné aktivní relace
|
||||||
{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>
|
||||||
))}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const cachedUserRef = useRef<User | null>(null);
|
const cachedUserRef = useRef<User | null>(null);
|
||||||
const sessionFetchedRef = useRef(false);
|
const sessionFetchedRef = useRef(false);
|
||||||
const silentRefreshInFlightRef = useRef<Promise<boolean> | null>(null);
|
const silentRefreshInFlightRef = useRef<Promise<boolean> | null>(null);
|
||||||
|
const hadValidSessionRef = useRef(false);
|
||||||
const [user, setUser] = useState<User | null>(cachedUserRef.current);
|
const [user, setUser] = useState<User | null>(cachedUserRef.current);
|
||||||
const [loading, setLoading] = useState(!sessionFetchedRef.current);
|
const [loading, setLoading] = useState(!sessionFetchedRef.current);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -138,13 +139,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (data.success && data.data?.access_token) {
|
if (data.success && data.data?.access_token) {
|
||||||
setAccessTokenFn(data.data.access_token, data.data.expires_in);
|
setAccessTokenFn(data.data.access_token, data.data.expires_in);
|
||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
accessTokenRef.current = null;
|
accessTokenRef.current = null;
|
||||||
tokenExpiresAtRef.current = null;
|
tokenExpiresAtRef.current = null;
|
||||||
setUser(null);
|
setUser(null);
|
||||||
cachedUserRef.current = null;
|
cachedUserRef.current = null;
|
||||||
setSessionExpired();
|
if (hadValidSessionRef.current) setSessionExpired();
|
||||||
return false;
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
// Network error — don't kick the user out, just return false
|
// Network error — don't kick the user out, just return false
|
||||||
@@ -178,6 +180,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (data.data.access_token) setAccessTokenFn(data.data.access_token);
|
if (data.data.access_token) setAccessTokenFn(data.data.access_token);
|
||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +236,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
sessionFetchedRef.current = true;
|
sessionFetchedRef.current = true;
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
@@ -273,6 +277,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(mapUser(data.data.user));
|
setUser(mapUser(data.data.user));
|
||||||
cachedUserRef.current = mapUser(data.data.user);
|
cachedUserRef.current = mapUser(data.data.user);
|
||||||
sessionFetchedRef.current = true;
|
sessionFetchedRef.current = true;
|
||||||
|
hadValidSessionRef.current = true;
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
setError(data.error);
|
setError(data.error);
|
||||||
@@ -302,6 +307,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
cachedUserRef.current = null;
|
cachedUserRef.current = null;
|
||||||
sessionFetchedRef.current = false;
|
sessionFetchedRef.current = false;
|
||||||
|
hadValidSessionRef.current = false;
|
||||||
if (refreshTimeoutRef.current) {
|
if (refreshTimeoutRef.current) {
|
||||||
clearTimeout(refreshTimeoutRef.current);
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
refreshTimeoutRef.current = null;
|
refreshTimeoutRef.current = null;
|
||||||
|
|||||||
69
src/admin/fixtures/AttendanceAdminFixture.tsx
Normal file
69
src/admin/fixtures/AttendanceAdminFixture.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default function AttendanceAdminFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa docházky</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-secondary">
|
||||||
|
Vyplnit měsíc
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-primary">Přidat záznam</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Měsíc</label>
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
<label className="admin-form-label">Zaměstnanec</label>
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-grid admin-grid-3">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div key={i} className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="flex-row gap-2 mb-2">
|
||||||
|
<span style={{ fontWeight: 600 }}>Jan Novák</span>
|
||||||
|
<span className="attendance-working-badge finished">
|
||||||
|
✗
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-value">8:00</div>
|
||||||
|
<div className="admin-stat-label">odpracováno</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Příchod</th>
|
||||||
|
<th>Odchod</th>
|
||||||
|
<th>Hodiny</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>1. 4. 2025</td>
|
||||||
|
<td className="admin-mono">08:00</td>
|
||||||
|
<td className="admin-mono">16:00</td>
|
||||||
|
<td className="admin-mono">8:00</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/admin/fixtures/AttendanceBalancesFixture.tsx
Normal file
104
src/admin/fixtures/AttendanceBalancesFixture.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
export default function AttendanceBalancesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa bilancí</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<select className="admin-form-select" style={{ minWidth: 100 }}>
|
||||||
|
<option>2025</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zaměstnanec</th>
|
||||||
|
<th>Nárok (h)</th>
|
||||||
|
<th>Čerpáno (h)</th>
|
||||||
|
<th>Zbývá (h)</th>
|
||||||
|
<th>Nemoc (h)</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="fw-500">Jan Novák</td>
|
||||||
|
<td className="admin-mono">160</td>
|
||||||
|
<td className="admin-mono">40.0</td>
|
||||||
|
<td className="admin-mono">120.0</td>
|
||||||
|
<td className="admin-mono">8.0</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon" title="Upravit">
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<h2 className="admin-page-title mb-4" style={{ fontSize: "1.25rem" }}>
|
||||||
|
Měsíční přehled fondu 2025
|
||||||
|
</h2>
|
||||||
|
<div className="admin-grid admin-grid-3">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div key={i} className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ fontWeight: 600, fontSize: "1rem", margin: 0 }}>
|
||||||
|
Duben
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.375rem",
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Jan Novák</span>
|
||||||
|
<span className="text-secondary">8h</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 3,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "75%",
|
||||||
|
background: "var(--gradient)",
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/admin/fixtures/AttendanceCreateFixture.tsx
Normal file
43
src/admin/fixtures/AttendanceCreateFixture.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export default function AttendanceCreateFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<h1 className="admin-page-title">Zapsat docházku</h1>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ maxWidth: 600 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<FormField label="Uživatel">
|
||||||
|
<select className="admin-form-select">
|
||||||
|
<option>Jan Novák</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Datum">
|
||||||
|
<input type="date" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Příchod">
|
||||||
|
<input type="time" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Odchod">
|
||||||
|
<input type="time" className="admin-form-input" />
|
||||||
|
</FormField>
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormField({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/admin/fixtures/AttendanceFixture.tsx
Normal file
79
src/admin/fixtures/AttendanceFixture.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export default function AttendanceFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Docházka</h1>
|
||||||
|
<p className="admin-page-subtitle">pondělí 28. dubna 2025</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-layout">
|
||||||
|
<div className="attendance-main">
|
||||||
|
<div className="attendance-clock-card">
|
||||||
|
<div className="attendance-clock-header">
|
||||||
|
<div className="attendance-clock-status">
|
||||||
|
<span className="attendance-status-dot" />
|
||||||
|
<span>Nepracuji</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-clock-time">08:30</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-clock-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary w-full">
|
||||||
|
Příchod
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary w-full">
|
||||||
|
Žádost o nepřítomnost
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-sidebar">
|
||||||
|
<div className="attendance-balance-card">
|
||||||
|
<h3 className="attendance-balance-title">Dovolená 2025</h3>
|
||||||
|
<div className="attendance-balance-value">
|
||||||
|
<span className="attendance-balance-number">12</span>
|
||||||
|
<span className="attendance-balance-unit">dnů</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-detail">
|
||||||
|
<span>Celkem: 160h</span>
|
||||||
|
<span>Čerpáno: 64h</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-bar">
|
||||||
|
<div
|
||||||
|
className="attendance-balance-progress"
|
||||||
|
style={{ width: "60%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card">
|
||||||
|
<div className="admin-stat-icon danger">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-label">Nemoc 2025</span>
|
||||||
|
<span className="admin-stat-value">16h čerpáno</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-quick-links">
|
||||||
|
<h4 className="attendance-quick-title">Rychlé odkazy</h4>
|
||||||
|
<a className="attendance-quick-link">
|
||||||
|
<span>Moje žádosti</span>
|
||||||
|
</a>
|
||||||
|
<a className="attendance-quick-link">
|
||||||
|
<span>Historie docházky</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/admin/fixtures/AttendanceHistoryFixture.tsx
Normal file
102
src/admin/fixtures/AttendanceHistoryFixture.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
export default function AttendanceHistoryFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Historie docházky</h1>
|
||||||
|
<p className="admin-page-subtitle">duben 2025</p>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-secondary">Tisk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Měsíc</label>
|
||||||
|
<input className="admin-form-input" readOnly value="04/2025" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mb-6">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<div className="admin-stat-icon info">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 200 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "baseline",
|
||||||
|
marginBottom: "0.375rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 600 }}>Fond: 120h / 160h</span>
|
||||||
|
<span
|
||||||
|
className="text-secondary"
|
||||||
|
style={{ fontSize: "0.8125rem" }}
|
||||||
|
>
|
||||||
|
20 prac. dnů
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="attendance-balance-bar">
|
||||||
|
<div
|
||||||
|
className="attendance-balance-progress"
|
||||||
|
style={{ width: "75%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Příchod</th>
|
||||||
|
<th>Pauza</th>
|
||||||
|
<th>Odchod</th>
|
||||||
|
<th>Hodiny</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 4. 2025</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-info">
|
||||||
|
Práce
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">08:00</td>
|
||||||
|
<td className="admin-mono">12:00 – 12:30</td>
|
||||||
|
<td className="admin-mono">16:30</td>
|
||||||
|
<td className="admin-mono">8:00</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/admin/fixtures/AttendanceLocationFixture.tsx
Normal file
49
src/admin/fixtures/AttendanceLocationFixture.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export default function AttendanceLocationFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
<h1 className="admin-page-title">Lokace</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ height: 300, marginBottom: "1rem" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
||||||
|
>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ marginBottom: "0.5rem" }}>Poloha</h3>
|
||||||
|
<p>50.0755° N, 14.4378° E</p>
|
||||||
|
<p>Praha, Česká republika</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 style={{ marginBottom: "0.5rem" }}>Čas záznamu</h3>
|
||||||
|
<p>1. 1. 2024 08:00</p>
|
||||||
|
<p>Přesnost: 10 m</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/admin/fixtures/AuditLogFixture.tsx
Normal file
52
src/admin/fixtures/AuditLogFixture.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
export default function AuditLogFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Audit log</h1>
|
||||||
|
<p className="admin-page-subtitle">Záznam změn v systému</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-search-bar mb-4"
|
||||||
|
style={{ display: "flex", gap: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
<select className="admin-form-select" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Čas</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
<th>Entita</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024 10:00</td>
|
||||||
|
<td>admin</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-create">
|
||||||
|
Vytvoření
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>Faktura</td>
|
||||||
|
<td style={{ maxWidth: 300 }}>Nová faktura FV-2024-001</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
src/admin/fixtures/CompanySettingsFixture.tsx
Normal file
69
src/admin/fixtures/CompanySettingsFixture.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default function CompanySettingsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nastavení firmy</h1>
|
||||||
|
<p className="admin-page-subtitle">Firemní údaje a bankovní účty</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
Uložit nastavení
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-settings-grid">
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h3 className="admin-card-title">Firemní údaje</h3>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form">
|
||||||
|
<label className="admin-form-label">Název firmy</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
readOnly
|
||||||
|
value="BOHA s.r.o."
|
||||||
|
/>
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">Ulice</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
readOnly
|
||||||
|
value="Hlavní 123"
|
||||||
|
/>
|
||||||
|
<label className="admin-form-label">Město</label>
|
||||||
|
<input className="admin-form-input" readOnly value="Praha" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h3 className="admin-card-title">Bankovní účty</h3>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Banka</th>
|
||||||
|
<th>Číslo účtu</th>
|
||||||
|
<th>Měna</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Hlavní účet</td>
|
||||||
|
<td>ČSOB</td>
|
||||||
|
<td className="admin-mono">123456/0300</td>
|
||||||
|
<td>CZK</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/admin/fixtures/DashSessionsFixture.tsx
Normal file
63
src/admin/fixtures/DashSessionsFixture.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export default function DashSessionsFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div
|
||||||
|
className="admin-card-header"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "0.75rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="admin-card-title">Přihlášená zařízení</h2>
|
||||||
|
<button className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||||
|
Odhlásit ostatní
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
|
<div className="dash-sessions-list">
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`dash-session-item${i === 0 ? " dash-session-item-current" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="dash-session-icon">
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||||
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="dash-session-info">
|
||||||
|
<div className="dash-session-device">
|
||||||
|
Chrome na Windows
|
||||||
|
{i === 0 && (
|
||||||
|
<span
|
||||||
|
className="admin-badge admin-badge-success"
|
||||||
|
style={{ marginLeft: "0.5rem" }}
|
||||||
|
>
|
||||||
|
Aktuální
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dash-session-meta">
|
||||||
|
<span>192.168.1.100</span>
|
||||||
|
<span className="dash-session-meta-separator">|</span>
|
||||||
|
<span>před 2 hodinami</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
src/admin/fixtures/DashboardFixture.tsx
Normal file
94
src/admin/fixtures/DashboardFixture.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
export default function DashboardFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Dashboard</h1>
|
||||||
|
<p className="admin-page-subtitle">Přehled</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["Nabídky", "Objednávky", "Faktury", "Projekty"].map((label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>12</div>
|
||||||
|
<div style={{ fontSize: "0.75rem" }}>tento měsíc</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-quick-actions"
|
||||||
|
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
||||||
|
>
|
||||||
|
{["Nová nabídka", "Nová faktura", "Zapsat docházku"].map((label, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "2fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-card" style={{ minHeight: 320 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Docházka dnes</h3>
|
||||||
|
<div style={{ height: 200 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
<div className="admin-card" style={{ minHeight: 150 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Aktivita</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ minHeight: 150 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Profil</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
||||||
|
>
|
||||||
|
<div className="admin-card" style={{ minHeight: 200 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Relace</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ minHeight: 200 }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Poslední aktivity</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/admin/fixtures/InvoiceDetailFixture.tsx
Normal file
86
src/admin/fixtures/InvoiceDetailFixture.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export default function InvoiceDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
<a href="/invoices" className="admin-btn-icon">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">FV-2024-001</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Zaplaceno</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div>Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
Vystavena
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum vystavení</label>
|
||||||
|
<div>1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum splatnosti</label>
|
||||||
|
<div>15. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Položky</h3>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Služba {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
src/admin/fixtures/InvoicesFixture.tsx
Normal file
83
src/admin/fixtures/InvoicesFixture.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
export default function InvoicesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Faktury</h1>
|
||||||
|
<p className="admin-page-subtitle">15 faktur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["Vystaveno", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
||||||
|
(label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
||||||
|
{i * 5 + 3}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Částka</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">FV-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
Vystaveno
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right">50 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/admin/fixtures/LeaveApprovalFixture.tsx
Normal file
51
src/admin/fixtures/LeaveApprovalFixture.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default function LeaveApprovalFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Schvalování dovolené</h1>
|
||||||
|
<p className="admin-page-subtitle">2 čekající</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Od</th>
|
||||||
|
<th>Do</th>
|
||||||
|
<th>Dní</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Dovolená</td>
|
||||||
|
<td className="admin-mono">1. 7. 2024</td>
|
||||||
|
<td className="admin-mono">5. 7. 2024</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn admin-btn-sm admin-btn-primary">
|
||||||
|
Schválit
|
||||||
|
</button>
|
||||||
|
<button className="admin-btn admin-btn-sm admin-btn-secondary">
|
||||||
|
Zamítnout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/admin/fixtures/LeaveRequestsFixture.tsx
Normal file
53
src/admin/fixtures/LeaveRequestsFixture.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default function LeaveRequestsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Žádosti o dovolenou</h1>
|
||||||
|
<p className="admin-page-subtitle">3 žádosti</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">+ Nová žádost</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Od</th>
|
||||||
|
<th>Do</th>
|
||||||
|
<th>Dní</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Dovolená</td>
|
||||||
|
<td className="admin-mono">1. 7. 2024</td>
|
||||||
|
<td className="admin-mono">5. 7. 2024</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-pending">
|
||||||
|
Čeká
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">Zrušit</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/admin/fixtures/OfferDetailFixture.tsx
Normal file
60
src/admin/fixtures/OfferDetailFixture.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export default function OfferDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<button className="admin-btn-icon">←</button>
|
||||||
|
<h1 className="admin-page-title">NAB-2024-001</h1>
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Číslo nabídky</label>
|
||||||
|
<div className="admin-form-input">NAB-2024-001</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div className="admin-form-input">Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum</label>
|
||||||
|
<div className="admin-form-input">1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Platnost do</label>
|
||||||
|
<div className="admin-form-input">31. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table className="admin-table" style={{ marginTop: "1rem" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Položka {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/admin/fixtures/OffersCustomersFixture.tsx
Normal file
49
src/admin/fixtures/OffersCustomersFixture.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export default function OffersCustomersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Zákazníci</h1>
|
||||||
|
<p className="admin-page-subtitle">8 zákazníků</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat zákazníka
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Město</th>
|
||||||
|
<th>IČO</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>Praha</td>
|
||||||
|
<td className="admin-mono">12345678</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/admin/fixtures/OffersFixture.tsx
Normal file
54
src/admin/fixtures/OffersFixture.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export default function OffersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nabídky</h1>
|
||||||
|
<p className="admin-page-subtitle">12 nabídek</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Celkem</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">NAB-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-offer-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right fw-500">100 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/admin/fixtures/OffersTemplatesFixture.tsx
Normal file
36
src/admin/fixtures/OffersTemplatesFixture.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export default function OffersTemplatesFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Šablona {i + 1}</td>
|
||||||
|
<td className="admin-mono text-right">1 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/admin/fixtures/OrderDetailFixture.tsx
Normal file
89
src/admin/fixtures/OrderDetailFixture.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
export default function OrderDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div className="admin-page-header-left">
|
||||||
|
<Link
|
||||||
|
to="/orders"
|
||||||
|
className="admin-btn-icon"
|
||||||
|
style={{ marginRight: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">OBJ-2024-001</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Stornovat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Zákazník</label>
|
||||||
|
<div>Firma s.r.o.</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<span className="admin-badge admin-badge-order-realizace">
|
||||||
|
V realizaci
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Datum vytvoření</label>
|
||||||
|
<div>1. 1. 2024</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Celkem</label>
|
||||||
|
<div className="fw-500">50 000 Kč</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Položky</h3>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Položka</th>
|
||||||
|
<th>Množství</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Celkem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Položka {i + 1}</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
<td>10 000 Kč</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/admin/fixtures/OrdersFixture.tsx
Normal file
55
src/admin/fixtures/OrdersFixture.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export default function OrdersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Objednávky</h1>
|
||||||
|
<p className="admin-page-subtitle">8 objednávek</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Nabídka</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th className="text-right">Celkem</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">OBJ-2024-00{i + 1}</td>
|
||||||
|
<td>NAB-2024-00{i + 1}</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-order-realizace">
|
||||||
|
V realizaci
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td className="admin-mono text-right fw-500">50 000 Kč</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/admin/fixtures/ProjectDetailFixture.tsx
Normal file
70
src/admin/fixtures/ProjectDetailFixture.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export default function ProjectDetailFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
<a href="/projects" className="admin-btn-icon">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">PRJ-001 Projekt Alpha</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Uložit</button>
|
||||||
|
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div
|
||||||
|
className="admin-form-row"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Název projektu</label>
|
||||||
|
<input
|
||||||
|
className="admin-form-input"
|
||||||
|
value="Projekt Alpha"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Stav</label>
|
||||||
|
<select className="admin-form-select">
|
||||||
|
<option>Aktivní</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Začátek</label>
|
||||||
|
<input type="date" className="admin-form-input" readOnly />
|
||||||
|
</div>
|
||||||
|
<div className="admin-form-group">
|
||||||
|
<label className="admin-form-label">Konec</label>
|
||||||
|
<input type="date" className="admin-form-input" readOnly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Poznámky</h3>
|
||||||
|
<textarea className="admin-form-input" rows={4} readOnly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/admin/fixtures/ProjectFileManagerFixture.tsx
Normal file
53
src/admin/fixtures/ProjectFileManagerFixture.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export default function ProjectFileManagerFixture() {
|
||||||
|
return (
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<h3 className="admin-card-title">Soubory</h3>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
marginBottom: "0.75rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Projekt</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span>Dokumentace</span>
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
padding: "0.5rem 0",
|
||||||
|
borderBottom: i < 3 ? "1px solid var(--border-color)" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
</svg>
|
||||||
|
<span style={{ flex: 1 }}>dokument_{i + 1}.pdf</span>
|
||||||
|
<span
|
||||||
|
style={{ color: "var(--text-secondary)", fontSize: "0.8rem" }}
|
||||||
|
>
|
||||||
|
2.{i + 1} MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/admin/fixtures/ProjectsFixture.tsx
Normal file
51
src/admin/fixtures/ProjectsFixture.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default function ProjectsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Projekty</h1>
|
||||||
|
<p className="admin-page-subtitle">6 projektů</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Zákazník</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">PRJ-2024-00{i + 1}</td>
|
||||||
|
<td>Projekt Alpha</td>
|
||||||
|
<td>Firma s.r.o.</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-project-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">👁</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/admin/fixtures/ReceivedInvoicesFixture.tsx
Normal file
80
src/admin/fixtures/ReceivedInvoicesFixture.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
export default function ReceivedInvoicesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Přijaté faktury</h1>
|
||||||
|
<p className="admin-page-subtitle">8 faktur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="dash-kpi-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["K úhradě", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
||||||
|
(label, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="dash-kpi-card"
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
||||||
|
{i * 3 + 2}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Číslo</th>
|
||||||
|
<th>Dodavatel</th>
|
||||||
|
<th>Částka</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">PF-2024-00{i + 1}</td>
|
||||||
|
<td>Dodavatel s.r.o.</td>
|
||||||
|
<td className="admin-mono text-right">10 000 Kč</td>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-invoice-issued">
|
||||||
|
K úhradě
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/admin/fixtures/SettingsFixture.tsx
Normal file
70
src/admin/fixtures/SettingsFixture.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export default function SettingsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Nastavení</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-tab-bar">
|
||||||
|
<button className="admin-tab active">Role</button>
|
||||||
|
<button className="admin-tab">Systém</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Název role</th>
|
||||||
|
<th>Popis</th>
|
||||||
|
<th>Oprávnění</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="fw-500">admin</td>
|
||||||
|
<td>Správa celého systému</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-info">
|
||||||
|
všechna
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon" title="Upravit">
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-header">
|
||||||
|
<h2 className="admin-card-title">Docházka</h2>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-form">
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-form-label">
|
||||||
|
Limit pro přestávku (hodiny)
|
||||||
|
</label>
|
||||||
|
<input className="admin-form-input" readOnly value="6" />
|
||||||
|
<label className="admin-form-label">
|
||||||
|
Délka krátké přestávky (min)
|
||||||
|
</label>
|
||||||
|
<input className="admin-form-input" readOnly value="15" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/admin/fixtures/TripsAdminFixture.tsx
Normal file
44
src/admin/fixtures/TripsAdminFixture.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export default function TripsAdminFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Správa jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">10 jízd</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Km</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>Škoda Octavia</td>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td className="admin-mono">200</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/admin/fixtures/TripsFixture.tsx
Normal file
132
src/admin/fixtures/TripsFixture.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
export default function TripsFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Kniha jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">duben 2025</p>
|
||||||
|
</div>
|
||||||
|
<div className="admin-page-actions">
|
||||||
|
<button className="admin-btn admin-btn-primary">Přidat jízdu</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-grid admin-grid-4">
|
||||||
|
<div className="admin-stat-card info">
|
||||||
|
<div className="admin-stat-icon info">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="20" x2="12" y2="10" />
|
||||||
|
<line x1="18" y1="20" x2="18" y2="4" />
|
||||||
|
<line x1="6" y1="20" x2="6" y2="16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">12</span>
|
||||||
|
<span className="admin-stat-label">Počet jízd</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card">
|
||||||
|
<div className="admin-stat-icon">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">1 240 km</span>
|
||||||
|
<span className="admin-stat-label">Celkem naježděno</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card success">
|
||||||
|
<div className="admin-stat-icon success">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
|
||||||
|
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
|
||||||
|
<circle cx="5.5" cy="18" r="2" />
|
||||||
|
<circle cx="18.5" cy="18" r="2" />
|
||||||
|
<path d="M8 18h8" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">980 km</span>
|
||||||
|
<span className="admin-stat-label">Služební</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-card warning">
|
||||||
|
<div className="admin-stat-icon warning">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="admin-stat-content">
|
||||||
|
<span className="admin-stat-value">260 km</span>
|
||||||
|
<span className="admin-stat-label">Soukromé</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card mt-6">
|
||||||
|
<div className="admin-card-header flex-between">
|
||||||
|
<h2 className="admin-card-title">Poslední jízdy</h2>
|
||||||
|
<a className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||||
|
Zobrazit historii
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Trasa</th>
|
||||||
|
<th>Vzdálenost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 4. 2025</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge">1A2 3456</span>
|
||||||
|
</td>
|
||||||
|
<td>Praha → Brno</td>
|
||||||
|
<td className="admin-mono">
|
||||||
|
<strong>200 km</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/admin/fixtures/TripsHistoryFixture.tsx
Normal file
40
src/admin/fixtures/TripsHistoryFixture.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export default function TripsHistoryFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Historie jízd</h1>
|
||||||
|
<p className="admin-page-subtitle">10 jízd</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Vozidlo</th>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>Trasa</th>
|
||||||
|
<th>Km</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="admin-mono">1. 1. 2024</td>
|
||||||
|
<td>Škoda Octavia</td>
|
||||||
|
<td>Jan Novák</td>
|
||||||
|
<td>Praha → Brno</td>
|
||||||
|
<td className="admin-mono">200</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/admin/fixtures/UsersFixture.tsx
Normal file
66
src/admin/fixtures/UsersFixture.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export default function UsersFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="admin-page-title">Uživatelé</h1>
|
||||||
|
<p className="admin-page-subtitle">5 uživatelů</p>
|
||||||
|
</div>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat uživatele
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Uživatel</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Stav</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-user">
|
||||||
|
<div className="admin-table-avatar">A</div>
|
||||||
|
<div>
|
||||||
|
<div className="admin-table-name">Jan Novák</div>
|
||||||
|
<div className="admin-table-username">@jan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>jan@email.cz</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-admin">
|
||||||
|
Administrátor
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-badge admin-badge-active">
|
||||||
|
Aktivní
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/admin/fixtures/VehiclesFixture.tsx
Normal file
48
src/admin/fixtures/VehiclesFixture.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export default function VehiclesFixture() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-page-header">
|
||||||
|
<h1 className="admin-page-title">Vozidla</h1>
|
||||||
|
<button className="admin-btn admin-btn-primary">
|
||||||
|
+ Přidat vozidlo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-card">
|
||||||
|
<div className="admin-card-body">
|
||||||
|
<div className="admin-search-bar mb-4">
|
||||||
|
<input className="admin-form-input" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-responsive">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Značka</th>
|
||||||
|
<th>Model</th>
|
||||||
|
<th>SPZ</th>
|
||||||
|
<th>Rok</th>
|
||||||
|
<th>Akce</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>Škoda</td>
|
||||||
|
<td>Octavia</td>
|
||||||
|
<td className="admin-mono">1A2 3456</td>
|
||||||
|
<td>2024</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
|
<button className="admin-btn-icon">✎</button>
|
||||||
|
<button className="admin-btn-icon danger">🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/admin/hooks/usePaginatedQuery.ts
Normal file
44
src/admin/hooks/usePaginatedQuery.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
useQuery,
|
||||||
|
keepPreviousData,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
interface PaginatedResult<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around useQuery for paginated list endpoints.
|
||||||
|
* Accepts the return value of queryOptions() from lib/queries/*
|
||||||
|
* and extracts items + pagination from the response.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function usePaginatedQuery<T>(options: any) {
|
||||||
|
const query = useQuery({
|
||||||
|
...options,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
} as UseQueryOptions<PaginatedResult<T>>);
|
||||||
|
|
||||||
|
const data = query.data as PaginatedResult<T> | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (data?.data ?? []) as T[],
|
||||||
|
pagination: data?.pagination ?? null,
|
||||||
|
isPending: query.isPending,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
isPlaceholderData: query.isPlaceholderData,
|
||||||
|
isError: query.isError,
|
||||||
|
error: query.error,
|
||||||
|
refetch: query.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useQueryClient, useQuery, keepPreviousData };
|
||||||
83
src/admin/lib/apiAdapter.ts
Normal file
83
src/admin/lib/apiAdapter.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import apiFetch from "../utils/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin adapter that converts apiFetch responses into the shape TanStack Query expects.
|
||||||
|
* - Checks response.ok and result.success
|
||||||
|
* - Throws on errors so TanStack Query can handle retry/error states
|
||||||
|
* - Returns result.data directly (unwrapped from the API envelope)
|
||||||
|
*/
|
||||||
|
export async function jsonQuery<T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await apiFetch(url, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: { success: boolean; data?: unknown; error?: string };
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as typeof result;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.error || `Request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationMeta {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResult<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: PaginationMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function paginatedJsonQuery<T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<PaginatedResult<T>> {
|
||||||
|
const response = await apiFetch(url, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: {
|
||||||
|
success: boolean;
|
||||||
|
data?: unknown;
|
||||||
|
error?: string;
|
||||||
|
pagination?: PaginationMeta;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as typeof result;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.error || `Request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(result.data)
|
||||||
|
? result.data
|
||||||
|
: ((result.data as { items?: T[] })?.items ?? []);
|
||||||
|
const pagination = result.pagination ??
|
||||||
|
(result.data as { pagination?: PaginationMeta })?.pagination ?? {
|
||||||
|
total: items.length,
|
||||||
|
page: 1,
|
||||||
|
per_page: items.length,
|
||||||
|
total_pages: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data: items as T[], pagination };
|
||||||
|
}
|
||||||
99
src/admin/lib/queries/attendance.ts
Normal file
99
src/admin/lib/queries/attendance.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
interface LocationRaw {
|
||||||
|
users?: { first_name: string; last_name: string };
|
||||||
|
user_name?: string;
|
||||||
|
shift_date: string;
|
||||||
|
arrival_time?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
arrival_lat?: string | number | null;
|
||||||
|
arrival_lng?: string | number | null;
|
||||||
|
arrival_accuracy?: number | null;
|
||||||
|
arrival_address?: string | null;
|
||||||
|
departure_lat?: string | number | null;
|
||||||
|
departure_lng?: string | number | null;
|
||||||
|
departure_accuracy?: number | null;
|
||||||
|
departure_address?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationRecord {
|
||||||
|
user_name: string;
|
||||||
|
shift_date: string;
|
||||||
|
arrival_time?: string | null;
|
||||||
|
departure_time?: string | null;
|
||||||
|
arrival_lat?: string | number | null;
|
||||||
|
arrival_lng?: string | number | null;
|
||||||
|
arrival_accuracy?: number | null;
|
||||||
|
arrival_address?: string | null;
|
||||||
|
departure_lat?: string | number | null;
|
||||||
|
departure_lng?: string | number | null;
|
||||||
|
departure_accuracy?: number | null;
|
||||||
|
departure_address?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const attendanceHistoryOptions = (filters: {
|
||||||
|
month?: string;
|
||||||
|
userId?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "history", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("limit", "1000");
|
||||||
|
if (filters.month) params.set("month", filters.month);
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/attendance?${params.toString()}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceBalancesOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "balances", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=balances&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceWorkFundOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "workfund", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=workfund&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceProjectReportOptions = (year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "project-report", year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/attendance?action=project_report&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const attendanceLocationOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["attendance", "location", id],
|
||||||
|
queryFn: async (): Promise<LocationRecord> => {
|
||||||
|
const response = await apiFetch(
|
||||||
|
`/api/admin/attendance?action=location&id=${id}`,
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Záznam nebyl nalezen");
|
||||||
|
}
|
||||||
|
const raw = (result.data.record || result.data) as LocationRaw;
|
||||||
|
const userName = raw.users
|
||||||
|
? `${raw.users.first_name} ${raw.users.last_name}`.trim()
|
||||||
|
: raw.user_name || "";
|
||||||
|
const { users: _users, ...rest } = raw;
|
||||||
|
return { ...rest, user_name: userName };
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
28
src/admin/lib/queries/auditLog.ts
Normal file
28
src/admin/lib/queries/auditLog.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const auditLogOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
action?: string;
|
||||||
|
entityType?: string;
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
page?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["audit-log", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.action) params.set("action", filters.action);
|
||||||
|
if (filters.entityType) params.set("entity_type", filters.entityType);
|
||||||
|
if (filters.dateFrom) params.set("date_from", filters.dateFrom);
|
||||||
|
if (filters.dateTo) params.set("date_to", filters.dateTo);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<{
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
pagination: Record<string, unknown>;
|
||||||
|
}>(`/api/admin/audit-log${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
18
src/admin/lib/queries/common.ts
Normal file
18
src/admin/lib/queries/common.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const bankAccountsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["bank-accounts"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>("/api/admin/bank-accounts"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const supplierListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["suppliers"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<string[]>("/api/admin/received-invoices/suppliers"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
42
src/admin/lib/queries/dashboard.ts
Normal file
42
src/admin/lib/queries/dashboard.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const dashboardOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["dashboard"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>>("/api/admin/dashboard"),
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const require2FAOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "2fa"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ require_2fa: boolean }>("/api/admin/totp/required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: number | string;
|
||||||
|
is_current: boolean;
|
||||||
|
device_info?: {
|
||||||
|
icon?: string;
|
||||||
|
browser?: string;
|
||||||
|
os?: string;
|
||||||
|
};
|
||||||
|
ip_address: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["sessions"],
|
||||||
|
queryFn: async (): Promise<Session[]> => {
|
||||||
|
const response = await apiFetch("/api/admin/sessions");
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
return Array.isArray(data.data) ? data.data : data.data?.sessions || [];
|
||||||
|
}
|
||||||
|
throw new Error(data.error || "Nepodařilo se načíst relace");
|
||||||
|
},
|
||||||
|
});
|
||||||
126
src/admin/lib/queries/invoices.ts
Normal file
126
src/admin/lib/queries/invoices.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export interface CurrencyAmount {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Invoice {
|
||||||
|
id: number;
|
||||||
|
invoice_number: string;
|
||||||
|
customer_name: string | null;
|
||||||
|
status: string;
|
||||||
|
issue_date: string;
|
||||||
|
due_date: string;
|
||||||
|
total: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoiceStats {
|
||||||
|
paid_month: CurrencyAmount[];
|
||||||
|
paid_month_czk: number;
|
||||||
|
paid_month_count: number;
|
||||||
|
awaiting: CurrencyAmount[];
|
||||||
|
awaiting_czk: number;
|
||||||
|
awaiting_count: number;
|
||||||
|
overdue: CurrencyAmount[];
|
||||||
|
overdue_czk: number;
|
||||||
|
overdue_count: number;
|
||||||
|
vat_month: CurrencyAmount[];
|
||||||
|
vat_month_czk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invoiceListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
status?: string;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.status) params.set("status", filters.status);
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery<Invoice>(
|
||||||
|
`/api/admin/invoices${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const receivedInvoiceListOptions = (filters: {
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"invoices",
|
||||||
|
"received",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
year: filters.year,
|
||||||
|
search: filters.search,
|
||||||
|
sort: filters.sort,
|
||||||
|
order: filters.order,
|
||||||
|
page: filters.page,
|
||||||
|
perPage: filters.perPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(
|
||||||
|
`/api/admin/received-invoices${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const invoiceStatsOptions = (month: number, year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "stats", month, year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<InvoiceStats>(
|
||||||
|
`/api/admin/invoices/stats?month=${month}&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const receivedInvoiceStatsOptions = (month: number, year: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", "received", "stats", month, year],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/received-invoices/stats?month=${month}&year=${year}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const invoiceDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["invoices", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/invoices/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
29
src/admin/lib/queries/leave.ts
Normal file
29
src/admin/lib/queries/leave.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const leaveRequestsOptions = (mine = true) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave-requests", mine ? "mine" : "all"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
`/api/admin/leave-requests${mine ? "?mine=1" : ""}`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const leavePendingOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave", "pending"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>(
|
||||||
|
"/api/admin/leave-requests?status=pending",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const leaveProcessedOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["leave", "processed"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>(
|
||||||
|
"/api/admin/leave-requests?status=approved,rejected",
|
||||||
|
),
|
||||||
|
});
|
||||||
58
src/admin/lib/queries/offers.ts
Normal file
58
src/admin/lib/queries/offers.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const offerCustomersOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offer-customers"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>>("/api/admin/customers"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerTemplatesOptions = (action?: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offer-templates", action ?? "all"],
|
||||||
|
queryFn: () => {
|
||||||
|
const url = action
|
||||||
|
? `/api/admin/offers-templates?action=${action}`
|
||||||
|
: "/api/admin/offers-templates";
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(url);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/offers${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/offers/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offerNextNumberOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["offers", "next-number"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ next_number?: string; number?: string }>(
|
||||||
|
"/api/admin/offers/next-number",
|
||||||
|
),
|
||||||
|
});
|
||||||
31
src/admin/lib/queries/orders.ts
Normal file
31
src/admin/lib/queries/orders.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const orderListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["orders", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/orders${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const orderDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["orders", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/orders/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
78
src/admin/lib/queries/projects.ts
Normal file
78
src/admin/lib/queries/projects.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
|
||||||
|
export const projectListOptions = (filters: {
|
||||||
|
search?: string;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", "list", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.set("search", filters.search);
|
||||||
|
if (filters.sort) params.set("sort", filters.sort);
|
||||||
|
if (filters.order) params.set("order", filters.order);
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return paginatedJsonQuery(`/api/admin/projects${qs ? `?${qs}` : ""}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectDetailOptions = (id: string | undefined) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", id],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(`/api/admin/projects/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ProjectFilesData {
|
||||||
|
items: Array<{
|
||||||
|
name: string;
|
||||||
|
type: "file" | "folder";
|
||||||
|
size?: number;
|
||||||
|
size_formatted?: string;
|
||||||
|
modified?: string;
|
||||||
|
extension?: string;
|
||||||
|
item_count?: number;
|
||||||
|
is_symlink?: boolean;
|
||||||
|
link_target?: string;
|
||||||
|
}>;
|
||||||
|
breadcrumb: string[];
|
||||||
|
path: string;
|
||||||
|
full_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectFilesOptions = (projectId: number, path: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["projects", String(projectId), "files", path],
|
||||||
|
queryFn: async (): Promise<ProjectFilesData> => {
|
||||||
|
const params = new URLSearchParams({ project_id: String(projectId) });
|
||||||
|
if (path) params.set("path", path);
|
||||||
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await apiFetch(`/api/admin/project-files?${params}`);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Chyba připojení");
|
||||||
|
}
|
||||||
|
if (res.status === 401) throw new Error("Unauthorized");
|
||||||
|
if (res.status === 404) {
|
||||||
|
return { items: [], breadcrumb: [""], path: "", full_path: "" };
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || !data.success) {
|
||||||
|
throw new Error(data.error || `Request failed (${res.status})`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: data.data.items || [],
|
||||||
|
breadcrumb: data.data.breadcrumb || [""],
|
||||||
|
path: data.data.path || "",
|
||||||
|
full_path: data.data.full_path || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
29
src/admin/lib/queries/settings.ts
Normal file
29
src/admin/lib/queries/settings.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const companySettingsOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["company-settings"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>("/api/admin/company-settings"),
|
||||||
|
staleTime: 5 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const systemInfoOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "system-info"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>>(
|
||||||
|
"/api/admin/company-settings/system-info",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @deprecated Use systemInfoOptions instead — this query fetches system-info, not system settings. */
|
||||||
|
export const systemSettingsOptions = systemInfoOptions;
|
||||||
|
|
||||||
|
export const require2FAOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["settings", "2fa"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<{ require_2fa: boolean }>("/api/admin/totp/required"),
|
||||||
|
});
|
||||||
82
src/admin/lib/queries/trips.ts
Normal file
82
src/admin/lib/queries/trips.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const tripListOptions = (filters: {
|
||||||
|
month?: number;
|
||||||
|
year?: number;
|
||||||
|
vehicleId?: number;
|
||||||
|
userId?: number;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"trips",
|
||||||
|
"list",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
year: filters.year,
|
||||||
|
vehicleId: filters.vehicleId,
|
||||||
|
userId: filters.userId,
|
||||||
|
page: filters.page,
|
||||||
|
perPage: filters.perPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", String(filters.month));
|
||||||
|
if (filters.year) params.set("year", String(filters.year));
|
||||||
|
if (filters.vehicleId)
|
||||||
|
params.set("vehicle_id", String(filters.vehicleId));
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/trips${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripVehiclesOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["trips", "vehicles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/vehicles"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripUsersOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["trips", "users"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Record<string, unknown>[]>("/api/admin/trips/users"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tripHistoryOptions = (filters: {
|
||||||
|
month?: string;
|
||||||
|
vehicleId?: number;
|
||||||
|
userId?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"trips",
|
||||||
|
"history",
|
||||||
|
{
|
||||||
|
month: filters.month,
|
||||||
|
vehicleId: filters.vehicleId,
|
||||||
|
userId: filters.userId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.month) params.set("month", filters.month);
|
||||||
|
if (filters.vehicleId)
|
||||||
|
params.set("vehicle_id", String(filters.vehicleId));
|
||||||
|
if (filters.userId) params.set("user_id", String(filters.userId));
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<Record<string, unknown>[]>(
|
||||||
|
`/api/admin/trips${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
18
src/admin/lib/queries/users.ts
Normal file
18
src/admin/lib/queries/users.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const userListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: () => {
|
||||||
|
// The users endpoint returns { success, data: { items, ... } } or { success, data: [...] }
|
||||||
|
return jsonQuery<Record<string, unknown>>("/api/admin/users");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["roles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/roles"),
|
||||||
|
staleTime: 2 * 60_000,
|
||||||
|
});
|
||||||
8
src/admin/lib/queries/vehicles.ts
Normal file
8
src/admin/lib/queries/vehicles.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { jsonQuery } from "../apiAdapter";
|
||||||
|
|
||||||
|
export const vehicleListOptions = () =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["vehicles"],
|
||||||
|
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/vehicles"),
|
||||||
|
});
|
||||||
13
src/admin/lib/queryClient.ts
Normal file
13
src/admin/lib/queryClient.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
gcTime: 5 * 60_000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
retry: 1,
|
||||||
|
refetchOnReconnect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -14,6 +15,9 @@ import {
|
|||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { jsonQuery } from "../lib/apiAdapter";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceFixture from "../fixtures/AttendanceFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -92,22 +96,20 @@ function getFundBarBackground(fund: MonthlyFund) {
|
|||||||
export default function Attendance() {
|
export default function Attendance() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [data, setData] = useState<AttendanceData>({
|
const statusQuery = useQuery({
|
||||||
ongoing_shift: null,
|
queryKey: ["attendance", "status"],
|
||||||
today_shifts: [],
|
queryFn: () => jsonQuery<AttendanceData>("/api/admin/attendance/status"),
|
||||||
date: "",
|
|
||||||
leave_balance: {
|
|
||||||
vacation_total: 160,
|
|
||||||
vacation_used: 0,
|
|
||||||
vacation_remaining: 160,
|
|
||||||
sick_used: 0,
|
|
||||||
},
|
|
||||||
monthly_fund: null,
|
|
||||||
project_logs: [],
|
|
||||||
active_project_id: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectsQuery = useQuery({
|
||||||
|
queryKey: ["attendance", "projects"],
|
||||||
|
queryFn: () =>
|
||||||
|
jsonQuery<Project[]>("/api/admin/attendance?action=projects"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
||||||
const [leaveForm, setLeaveForm] = useState({
|
const [leaveForm, setLeaveForm] = useState({
|
||||||
leave_type: "vacation",
|
leave_type: "vacation",
|
||||||
@@ -117,10 +119,7 @@ export default function Attendance() {
|
|||||||
});
|
});
|
||||||
const [requestSubmitting, setRequestSubmitting] = useState(false);
|
const [requestSubmitting, setRequestSubmitting] = useState(false);
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
|
||||||
const [switchingProject, setSwitchingProject] = useState(false);
|
const [switchingProject, setSwitchingProject] = useState(false);
|
||||||
const [projectLogs, setProjectLogs] = useState<ProjectLog[]>([]);
|
|
||||||
const [activeProjectId, setActiveProjectId] = useState<number | null>(null);
|
|
||||||
const [gpsConfirm, setGpsConfirm] = useState<{
|
const [gpsConfirm, setGpsConfirm] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
action: string | null;
|
action: string | null;
|
||||||
@@ -139,45 +138,12 @@ export default function Attendance() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// Sync notes from query data when the shift changes
|
||||||
try {
|
useEffect(() => {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance/status`);
|
if (statusQuery.data) {
|
||||||
if (response.status === 401) return;
|
setNotes(statusQuery.data.ongoing_shift?.notes || "");
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setData(result.data);
|
|
||||||
setNotes(result.data.ongoing_shift?.notes || "");
|
|
||||||
setProjectLogs(result.data.project_logs || []);
|
|
||||||
setActiveProjectId(result.data.active_project_id || null);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [alert]);
|
}, [statusQuery.data]);
|
||||||
|
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useModalLock(isLeaveModalOpen);
|
useModalLock(isLeaveModalOpen);
|
||||||
|
|
||||||
@@ -277,7 +243,9 @@ export default function Attendance() {
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
punchTimeoutRef.current = setTimeout(() => {
|
punchTimeoutRef.current = setTimeout(() => {
|
||||||
alert.success(result.data?.message || result.message || "Uloženo");
|
alert.success(result.data?.message || result.message || "Uloženo");
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -302,7 +270,9 @@ export default function Attendance() {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Přestávka zaznamenána",
|
result.data?.message || result.message || "Přestávka zaznamenána",
|
||||||
);
|
);
|
||||||
@@ -348,7 +318,9 @@ export default function Attendance() {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Projekt přepnut",
|
result.data?.message || result.message || "Projekt přepnut",
|
||||||
);
|
);
|
||||||
@@ -390,7 +362,9 @@ export default function Attendance() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsLeaveModalOpen(false);
|
setIsLeaveModalOpen(false);
|
||||||
await fetchData();
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["attendance", "status"],
|
||||||
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
alert.success(
|
alert.success(
|
||||||
result.data?.message || result.message || "Žádost odeslána",
|
result.data?.message || result.message || "Žádost odeslána",
|
||||||
@@ -411,103 +385,15 @@ export default function Attendance() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (!statusQuery.data) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="attendance"
|
||||||
className="admin-skeleton-row"
|
loading={statusQuery.isPending}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AttendanceFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: "1.5rem" }}>
|
|
||||||
<div className="admin-card" style={{ flex: 2 }}>
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "120px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "180px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-row">
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.25rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ marginBottom: "0.25rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +401,11 @@ export default function Attendance() {
|
|||||||
ongoing_shift: ongoingShift,
|
ongoing_shift: ongoingShift,
|
||||||
today_shifts: todayShifts,
|
today_shifts: todayShifts,
|
||||||
leave_balance: leaveBalance,
|
leave_balance: leaveBalance,
|
||||||
} = data;
|
} = statusQuery.data;
|
||||||
|
const data = statusQuery.data;
|
||||||
|
const projects = projectsQuery.data ?? [];
|
||||||
|
const projectLogs = data.project_logs ?? [];
|
||||||
|
const activeProjectId = data.active_project_id ?? null;
|
||||||
const isOngoingShift = ongoingShift && !ongoingShift.departure_time;
|
const isOngoingShift = ongoingShift && !ongoingShift.departure_time;
|
||||||
const completedToday = todayShifts.filter((s) => s.departure_time);
|
const completedToday = todayShifts.filter((s) => s.departure_time);
|
||||||
const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8);
|
const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import useModalLock from "../hooks/useModalLock";
|
|||||||
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { formatMinutes } from "../utils/attendanceHelpers";
|
import { formatMinutes } from "../utils/attendanceHelpers";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceAdminFixture from "../fixtures/AttendanceAdminFixture";
|
||||||
|
|
||||||
interface UserTotalData {
|
interface UserTotalData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -95,84 +97,13 @@ export default function AttendanceAdmin() {
|
|||||||
|
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="attendance-admin"
|
||||||
className="admin-skeleton-row"
|
loading={isInitialLoad}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AttendanceAdminFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-row" style={{ gap: "0.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton"
|
|
||||||
style={{ gap: "0.75rem", padding: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-skeleton-row">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ flex: 1, borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ flex: 1, borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{[0, 1, 2].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "0.75rem" }}>
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-full"
|
|
||||||
style={{ height: "4px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -6,8 +6,16 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
attendanceBalancesOptions,
|
||||||
|
attendanceWorkFundOptions,
|
||||||
|
attendanceProjectReportOptions,
|
||||||
|
} from "../lib/queries/attendance";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceBalancesFixture from "../fixtures/AttendanceBalancesFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface BalanceEntry {
|
interface BalanceEntry {
|
||||||
@@ -134,23 +142,20 @@ const getProgressBackground = (
|
|||||||
export default function AttendanceBalances() {
|
export default function AttendanceBalances() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [year, setYear] = useState(new Date().getFullYear());
|
const [year, setYear] = useState(new Date().getFullYear());
|
||||||
const [data, setData] = useState<BalancesData>({
|
const { data: balancesRaw, isPending: balancesPending } = useQuery(
|
||||||
users: [],
|
attendanceBalancesOptions(year),
|
||||||
balances: {},
|
);
|
||||||
});
|
const { data: fundRaw, isPending: fundPending } = useQuery(
|
||||||
|
attendanceWorkFundOptions(year),
|
||||||
const [fundLoading, setFundLoading] = useState(true);
|
);
|
||||||
const [fundData, setFundData] = useState<FundData>({
|
const { data: projectRaw, isPending: projectPending } = useQuery(
|
||||||
months: {},
|
attendanceProjectReportOptions(year),
|
||||||
holidays: [],
|
);
|
||||||
users: [],
|
const balancesData = balancesRaw as BalancesData | undefined;
|
||||||
balances: {},
|
const fundData = fundRaw as FundData | undefined;
|
||||||
});
|
const projectData = projectRaw as ProjectData | undefined;
|
||||||
|
|
||||||
const [projectLoading, setProjectLoading] = useState(true);
|
|
||||||
const [projectData, setProjectData] = useState<ProjectData>({ months: {} });
|
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<{
|
const [editingUser, setEditingUser] = useState<{
|
||||||
@@ -169,67 +174,6 @@ export default function AttendanceBalances() {
|
|||||||
userName: string;
|
userName: string;
|
||||||
}>({ show: false, userId: null, userName: "" });
|
}>({ show: false, userId: null, userName: "" });
|
||||||
|
|
||||||
const fetchData = useCallback(
|
|
||||||
async (showLoading = true) => {
|
|
||||||
if (showLoading) setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=balances&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst data");
|
|
||||||
} finally {
|
|
||||||
if (showLoading) setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[year, alert],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchFundData = useCallback(async () => {
|
|
||||||
setFundLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=workfund&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setFundData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent - fund data is supplementary
|
|
||||||
} finally {
|
|
||||||
setFundLoading(false);
|
|
||||||
}
|
|
||||||
}, [year]);
|
|
||||||
|
|
||||||
const fetchProjectData = useCallback(async () => {
|
|
||||||
setProjectLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/attendance?action=project_report&year=${year}`,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setProjectData(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent - project data is supplementary
|
|
||||||
} finally {
|
|
||||||
setProjectLoading(false);
|
|
||||||
}
|
|
||||||
}, [year]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadAll = async () => {
|
|
||||||
await Promise.all([fetchData(), fetchFundData(), fetchProjectData()]);
|
|
||||||
};
|
|
||||||
loadAll();
|
|
||||||
}, [fetchData, fetchFundData, fetchProjectData]);
|
|
||||||
|
|
||||||
useModalLock(showEditModal);
|
useModalLock(showEditModal);
|
||||||
|
|
||||||
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
||||||
@@ -265,8 +209,7 @@ export default function AttendanceBalances() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
await fetchData(false);
|
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -297,7 +240,7 @@ export default function AttendanceBalances() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setResetConfirm({ show: false, userId: null, userName: "" });
|
setResetConfirm({ show: false, userId: null, userName: "" });
|
||||||
await fetchData(false);
|
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -315,7 +258,7 @@ export default function AttendanceBalances() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getYearFundTotals = (userId: string) => {
|
const getYearFundTotals = (userId: string) => {
|
||||||
if (!fundData.months || Object.keys(fundData.months).length === 0)
|
if (!fundData?.months || Object.keys(fundData.months).length === 0)
|
||||||
return null;
|
return null;
|
||||||
let totalFund = 0;
|
let totalFund = 0;
|
||||||
let totalWorked = 0;
|
let totalWorked = 0;
|
||||||
@@ -380,132 +323,137 @@ export default function AttendanceBalances() {
|
|||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances"
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
loading={balancesPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
{balancesData &&
|
||||||
</div>
|
Object.keys(balancesData.balances).length === 0 && (
|
||||||
))}
|
<div className="admin-empty-state">
|
||||||
</div>
|
<p>Žádní uživatelé k zobrazení.</p>
|
||||||
)}
|
</div>
|
||||||
{!loading && Object.keys(data.balances).length === 0 && (
|
)}
|
||||||
<div className="admin-empty-state">
|
{balancesData &&
|
||||||
<p>Žádní uživatelé k zobrazení.</p>
|
Object.keys(balancesData.balances).length > 0 && (
|
||||||
</div>
|
<div className="admin-table-responsive">
|
||||||
)}
|
<table className="admin-table">
|
||||||
{!loading && Object.keys(data.balances).length > 0 && (
|
<thead>
|
||||||
<div className="admin-table-responsive">
|
<tr>
|
||||||
<table className="admin-table">
|
<th>Zaměstnanec</th>
|
||||||
<thead>
|
<th>Nárok (h)</th>
|
||||||
<tr>
|
<th>Čerpáno (h)</th>
|
||||||
<th>Zaměstnanec</th>
|
<th>Zbývá (h)</th>
|
||||||
<th>Nárok (h)</th>
|
<th>Nemoc (h)</th>
|
||||||
<th>Čerpáno (h)</th>
|
<th>Fond roku</th>
|
||||||
<th>Zbývá (h)</th>
|
<th>Pokryto</th>
|
||||||
<th>Nemoc (h)</th>
|
<th>+/−</th>
|
||||||
<th>Fond roku</th>
|
<th>Akce</th>
|
||||||
<th>Pokryto</th>
|
</tr>
|
||||||
<th>+/−</th>
|
</thead>
|
||||||
<th>Akce</th>
|
<tbody>
|
||||||
</tr>
|
{Object.entries(balancesData.balances).map(
|
||||||
</thead>
|
([userId, balance]) => {
|
||||||
<tbody>
|
const yf = getYearFundTotals(userId);
|
||||||
{Object.entries(data.balances).map(([userId, balance]) => {
|
return (
|
||||||
const yf = getYearFundTotals(userId);
|
<tr key={userId}>
|
||||||
return (
|
<td className="fw-500">{balance.name}</td>
|
||||||
<tr key={userId}>
|
<td className="admin-mono">
|
||||||
<td className="fw-500">{balance.name}</td>
|
{balance.vacation_total}
|
||||||
<td className="admin-mono">{balance.vacation_total}</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{balance.vacation_used.toFixed(1)}
|
{balance.vacation_used.toFixed(1)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
<span
|
<span
|
||||||
className={getVacationClass(
|
className={getVacationClass(
|
||||||
balance.vacation_remaining,
|
balance.vacation_remaining,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{balance.vacation_remaining.toFixed(1)}
|
{balance.vacation_remaining.toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{balance.sick_used.toFixed(1)}
|
{balance.sick_used.toFixed(1)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{yf ? `${yf.fund}h` : "—"}
|
{yf ? `${yf.fund}h` : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{yf ? `${yf.covered}h` : "—"}
|
{yf ? `${yf.covered}h` : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{yf ? renderFundDiff(yf) : "—"}
|
{yf ? renderFundDiff(yf) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="admin-table-actions">
|
<div className="admin-table-actions">
|
||||||
<button
|
<button
|
||||||
onClick={() => openEditModal(userId, balance)}
|
onClick={() =>
|
||||||
className="admin-btn-icon"
|
openEditModal(userId, balance)
|
||||||
title="Upravit"
|
}
|
||||||
aria-label="Upravit"
|
className="admin-btn-icon"
|
||||||
>
|
title="Upravit"
|
||||||
<svg
|
aria-label="Upravit"
|
||||||
width="18"
|
>
|
||||||
height="18"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
width="18"
|
||||||
fill="none"
|
height="18"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="2"
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeWidth="2"
|
||||||
>
|
strokeLinecap="round"
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
strokeLinejoin="round"
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
>
|
||||||
</svg>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
</button>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
<button
|
</svg>
|
||||||
onClick={() =>
|
</button>
|
||||||
setResetConfirm({
|
<button
|
||||||
show: true,
|
onClick={() =>
|
||||||
userId,
|
setResetConfirm({
|
||||||
userName: balance.name,
|
show: true,
|
||||||
})
|
userId,
|
||||||
}
|
userName: balance.name,
|
||||||
className="admin-btn-icon danger"
|
})
|
||||||
title="Resetovat"
|
}
|
||||||
aria-label="Resetovat"
|
className="admin-btn-icon danger"
|
||||||
>
|
title="Resetovat"
|
||||||
<svg
|
aria-label="Resetovat"
|
||||||
width="18"
|
>
|
||||||
height="18"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
width="18"
|
||||||
fill="none"
|
height="18"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="2"
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeWidth="2"
|
||||||
>
|
strokeLinecap="round"
|
||||||
<polyline points="3 6 5 6 21 6" />
|
strokeLinejoin="round"
|
||||||
<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>
|
<polyline points="3 6 5 6 21 6" />
|
||||||
</button>
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</div>
|
||||||
);
|
</td>
|
||||||
})}
|
</tr>
|
||||||
</tbody>
|
);
|
||||||
</table>
|
},
|
||||||
</div>
|
)}
|
||||||
)}
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Monthly Fund Overview */}
|
{/* Monthly Fund Overview */}
|
||||||
{!fundLoading &&
|
{!fundPending &&
|
||||||
fundData.months &&
|
fundData?.months &&
|
||||||
Object.keys(fundData.months).length > 0 && (
|
Object.keys(fundData.months).length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
@@ -587,7 +535,7 @@ export default function AttendanceBalances() {
|
|||||||
gap: "0.375rem",
|
gap: "0.375rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fundData.users &&
|
{fundData?.users &&
|
||||||
fundData.users.map((user) => {
|
fundData.users.map((user) => {
|
||||||
const us = monthData.users?.[String(user.id)];
|
const us = monthData.users?.[String(user.id)];
|
||||||
if (!us) return null;
|
if (!us) return null;
|
||||||
@@ -668,23 +616,19 @@ export default function AttendanceBalances() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fundLoading && (
|
{fundPending && (
|
||||||
<div className="mt-6">
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances-fund"
|
||||||
{[0, 1, 2].map((i) => (
|
loading={fundPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<div className="mt-6" />
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
</Skeleton>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Monthly Project Overview */}
|
{/* Monthly Project Overview */}
|
||||||
{!projectLoading &&
|
{!projectPending &&
|
||||||
projectData.months &&
|
projectData?.months &&
|
||||||
Object.keys(projectData.months).length > 0 && (
|
Object.keys(projectData.months).length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
@@ -876,18 +820,14 @@ export default function AttendanceBalances() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projectLoading && (
|
{projectPending && (
|
||||||
<div className="mt-6">
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-balances-projects"
|
||||||
{[0, 1, 2].map((i) => (
|
loading={projectPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceBalancesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<div className="mt-6" />
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
</Skeleton>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { userListOptions } from "../lib/queries/users";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -8,6 +10,8 @@ import { motion } from "framer-motion";
|
|||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceCreateFixture from "../fixtures/AttendanceCreateFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -35,9 +39,9 @@ export default function AttendanceCreate() {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loading, setLoading] = useState(true);
|
const { data: usersData, isPending: loading } = useQuery(userListOptions());
|
||||||
|
const users = (usersData as unknown as User[] | undefined) ?? [];
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateForm>(() => {
|
const [form, setForm] = useState<CreateForm>(() => {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
const today = new Date().toISOString().split("T")[0];
|
||||||
@@ -58,26 +62,6 @@ export default function AttendanceCreate() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/users`);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setUsers(
|
|
||||||
Array.isArray(result.data) ? result.data : result.data?.items || [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst uživatele");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUsers();
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -125,247 +109,223 @@ export default function AttendanceCreate() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div 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 (
|
return (
|
||||||
<div>
|
<Skeleton
|
||||||
<motion.div
|
name="attendance-create"
|
||||||
className="admin-page-header"
|
loading={loading}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
fixture={<AttendanceCreateFixture />}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.25 }}
|
<div>
|
||||||
>
|
<motion.div
|
||||||
<div>
|
className="admin-page-header"
|
||||||
<h1 className="admin-page-title">Přidat záznam docházky</h1>
|
initial={{ opacity: 0, y: 12 }}
|
||||||
</div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="admin-page-actions">
|
transition={{ duration: 0.25 }}
|
||||||
<Link
|
>
|
||||||
to="/attendance/admin"
|
<div>
|
||||||
className="admin-btn admin-btn-secondary"
|
<h1 className="admin-page-title">Přidat záznam docházky</h1>
|
||||||
>
|
</div>
|
||||||
← Zpět na správu
|
<div className="admin-page-actions">
|
||||||
</Link>
|
<Link
|
||||||
</div>
|
to="/attendance/admin"
|
||||||
</motion.div>
|
className="admin-btn admin-btn-secondary"
|
||||||
|
>
|
||||||
|
← Zpět na správu
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
style={{ maxWidth: "600px" }}
|
style={{ maxWidth: "600px" }}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<form onSubmit={handleSubmit} className="admin-form">
|
<form onSubmit={handleSubmit} className="admin-form">
|
||||||
<div className="admin-form-row">
|
<div className="admin-form-row">
|
||||||
<FormField label="Zaměstnanec" required>
|
<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
|
<select
|
||||||
value={form.user_id}
|
value={form.leave_type}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, user_id: e.target.value })
|
setForm({ ...form, leave_type: e.target.value })
|
||||||
}
|
}
|
||||||
className="admin-form-select"
|
className="admin-form-select"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
<option value="">Vyberte zaměstnance</option>
|
<option value="work">Práce</option>
|
||||||
{users.map((user) => (
|
<option value="vacation">Dovolená</option>
|
||||||
<option key={user.id} value={user.id}>
|
<option value="sick">Nemoc</option>
|
||||||
{user.name}
|
<option value="holiday">Svátek</option>
|
||||||
</option>
|
<option value="unpaid">Neplacené volno</option>
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Datum směny" required>
|
|
||||||
<AdminDatePicker
|
{!isWorkType && (
|
||||||
mode="date"
|
<FormField label="Počet hodin">
|
||||||
value={form.shift_date}
|
<input
|
||||||
onChange={(val: string) => handleShiftDateChange(val)}
|
type="number"
|
||||||
required
|
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>
|
</FormField>
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField label="Typ záznamu" required>
|
<div className="admin-form-actions">
|
||||||
<select
|
<Link
|
||||||
value={form.leave_type}
|
to="/attendance/admin"
|
||||||
onChange={(e) =>
|
className="admin-btn admin-btn-secondary"
|
||||||
setForm({ ...form, leave_type: e.target.value })
|
>
|
||||||
}
|
Zrušit
|
||||||
className="admin-form-select"
|
</Link>
|
||||||
>
|
<button
|
||||||
<option value="work">Práce</option>
|
type="submit"
|
||||||
<option value="vacation">Dovolená</option>
|
disabled={submitting}
|
||||||
<option value="sick">Nemoc</option>
|
className="admin-btn admin-btn-primary"
|
||||||
<option value="holiday">Svátek</option>
|
>
|
||||||
<option value="unpaid">Neplacené volno</option>
|
{submitting ? "Ukládám..." : "Uložit"}
|
||||||
</select>
|
</button>
|
||||||
</FormField>
|
</div>
|
||||||
|
</form>
|
||||||
{!isWorkType && (
|
</div>
|
||||||
<FormField label="Počet hodin">
|
</motion.div>
|
||||||
<input
|
</div>
|
||||||
type="number"
|
</Skeleton>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
import { attendanceHistoryOptions } from "../lib/queries/attendance";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDatetime,
|
formatDatetime,
|
||||||
@@ -16,10 +18,8 @@ import {
|
|||||||
formatTimeOrDatetimePrint,
|
formatTimeOrDatetimePrint,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import apiFetch from "../utils/api";
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AttendanceHistoryFixture from "../fixtures/AttendanceHistoryFixture";
|
||||||
const API_BASE = "/api/admin";
|
|
||||||
|
|
||||||
interface ProjectLog {
|
interface ProjectLog {
|
||||||
id?: number;
|
id?: number;
|
||||||
project_id?: number;
|
project_id?: number;
|
||||||
@@ -193,48 +193,21 @@ const renderProjectCell = (record: AttendanceRecord) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AttendanceHistory() {
|
export default function AttendanceHistory() {
|
||||||
const alert = useAlert();
|
|
||||||
const { user, hasPermission } = useAuth();
|
const { user, hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [companyName, setCompanyName] = useState("");
|
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||||
|
const companyName =
|
||||||
|
((companySettings as Record<string, unknown> | undefined)
|
||||||
|
?.company_name as string) || "";
|
||||||
const printRef = useRef<HTMLDivElement>(null);
|
const printRef = useRef<HTMLDivElement>(null);
|
||||||
const [month, setMonth] = useState(() => {
|
const [month, setMonth] = useState(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||||
});
|
});
|
||||||
const [records, setRecords] = useState<AttendanceRecord[]>([]);
|
const { data, isPending } = useQuery(
|
||||||
|
attendanceHistoryOptions({ month, userId: user?.id }),
|
||||||
const fetchData = useCallback(async () => {
|
);
|
||||||
setLoading(true);
|
const records = (data as AttendanceRecord[] | undefined) ?? [];
|
||||||
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 computed = useMemo(() => {
|
const computed = useMemo(() => {
|
||||||
const [yearStr, monthStr] = month.split("-");
|
const [yearStr, monthStr] = month.split("-");
|
||||||
@@ -459,144 +432,123 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
transition={{ duration: 0.25, delay: 0.08 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "0.5rem" }}>
|
name="attendance-history-fund"
|
||||||
<div className="admin-skeleton-row" style={{ gap: "1rem" }}>
|
loading={isPending}
|
||||||
<div
|
fixture={<AttendanceHistoryFixture />}
|
||||||
className="admin-skeleton-line"
|
>
|
||||||
style={{
|
<>
|
||||||
width: "48px",
|
{computed.monthlyFund && (
|
||||||
height: "48px",
|
|
||||||
borderRadius: "12px",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/2"
|
|
||||||
style={{ marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-full"
|
|
||||||
style={{ height: "6px", borderRadius: "3px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/3"
|
|
||||||
style={{ height: "10px", marginTop: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading && computed.monthlyFund && (
|
|
||||||
<div
|
|
||||||
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" }}>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
alignItems: "center",
|
||||||
alignItems: "baseline",
|
gap: "1rem",
|
||||||
marginBottom: "0.375rem",
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<div className="admin-stat-icon info">
|
||||||
style={{
|
<svg
|
||||||
fontWeight: 600,
|
width="24"
|
||||||
fontSize: "1rem",
|
height="24"
|
||||||
color: "var(--text-primary)",
|
viewBox="0 0 24 24"
|
||||||
}}
|
fill="none"
|
||||||
>
|
stroke="currentColor"
|
||||||
Fond: {computed.monthlyFund.worked}h /{" "}
|
strokeWidth="2"
|
||||||
{computed.monthlyFund.fund}h
|
>
|
||||||
</span>
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||||
<span
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
className="text-secondary"
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
style={{ fontSize: "0.8125rem" }}
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
>
|
</svg>
|
||||||
{computed.monthlyFund.business_days} prac. dnů
|
</div>
|
||||||
</span>
|
<div style={{ flex: 1, minWidth: "200px" }}>
|
||||||
</div>
|
<div
|
||||||
<div className="attendance-balance-bar">
|
style={{
|
||||||
<div
|
display: "flex",
|
||||||
className="attendance-balance-progress"
|
justifyContent: "space-between",
|
||||||
style={{
|
alignItems: "baseline",
|
||||||
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`,
|
marginBottom: "0.375rem",
|
||||||
background:
|
}}
|
||||||
computed.monthlyFund.covered >=
|
>
|
||||||
computed.monthlyFund.fund
|
<span
|
||||||
? "linear-gradient(135deg, var(--success), #059669)"
|
style={{
|
||||||
: "var(--gradient)",
|
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>
|
</div>
|
||||||
|
)}
|
||||||
|
{!computed.monthlyFund && (
|
||||||
<div
|
<div
|
||||||
className="text-muted"
|
className="text-muted"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
fontSize: "0.875rem",
|
||||||
justifyContent: "space-between",
|
textAlign: "center",
|
||||||
fontSize: "0.75rem",
|
padding: "0.5rem 0",
|
||||||
marginTop: "0.375rem",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
Fond měsíce není k dispozici
|
||||||
{"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>
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
</Skeleton>
|
||||||
{!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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -608,91 +560,90 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="attendance-history-table"
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
loading={isPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<AttendanceHistoryFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
{records.length === 0 && (
|
||||||
|
<div className="admin-empty-state">
|
||||||
|
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
{records.length > 0 && (
|
||||||
)}
|
<div className="admin-table-responsive">
|
||||||
{!loading && records.length === 0 && (
|
<table className="admin-table">
|
||||||
<div className="admin-empty-state">
|
<thead>
|
||||||
<p>Za tento měsíc nejsou žádné záznamy.</p>
|
<tr>
|
||||||
</div>
|
<th>Datum</th>
|
||||||
)}
|
<th>Typ</th>
|
||||||
{!loading && records.length > 0 && (
|
<th>Příchod</th>
|
||||||
<div className="admin-table-responsive">
|
<th>Pauza</th>
|
||||||
<table className="admin-table">
|
<th>Odchod</th>
|
||||||
<thead>
|
<th>Hodiny</th>
|
||||||
<tr>
|
<th>Projekty</th>
|
||||||
<th>Datum</th>
|
<th>Poznámka</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
</thead>
|
||||||
})}
|
<tbody>
|
||||||
</tbody>
|
{records.map((record) => {
|
||||||
</table>
|
const leaveType = record.leave_type || "work";
|
||||||
</div>
|
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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -9,65 +10,35 @@ import L from "leaflet";
|
|||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import {
|
||||||
const API_BASE = "/api/admin";
|
attendanceLocationOptions,
|
||||||
|
type LocationRecord,
|
||||||
interface LocationRecord {
|
} from "../lib/queries/attendance";
|
||||||
user_name: string;
|
import { Skeleton } from "boneyard-js/react";
|
||||||
shift_date: string;
|
import AttendanceLocationFixture from "../fixtures/AttendanceLocationFixture";
|
||||||
arrival_time?: string | null;
|
|
||||||
departure_time?: string | null;
|
|
||||||
arrival_lat?: string | number | null;
|
|
||||||
arrival_lng?: string | number | null;
|
|
||||||
arrival_accuracy?: number | null;
|
|
||||||
arrival_address?: string | null;
|
|
||||||
departure_lat?: string | number | null;
|
|
||||||
departure_lng?: string | number | null;
|
|
||||||
departure_accuracy?: number | null;
|
|
||||||
departure_address?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AttendanceLocation() {
|
export default function AttendanceLocation() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [record, setRecord] = useState<LocationRecord | null>(null);
|
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
const mapInstanceRef = useRef<unknown>(null);
|
const mapInstanceRef = useRef<unknown>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const locationQuery = useQuery(attendanceLocationOptions(id));
|
||||||
const fetchData = async () => {
|
const record = locationQuery.data ?? null;
|
||||||
try {
|
const isPending = locationQuery.isPending;
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
// Navigate away on fetch error
|
||||||
}, [id, alert, navigate]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!record || loading) return;
|
if (!record || isPending) return;
|
||||||
|
|
||||||
const hasArrivalLocation = record.arrival_lat && record.arrival_lng;
|
const hasArrivalLocation = record.arrival_lat && record.arrival_lng;
|
||||||
const hasDepartureLocation = record.departure_lat && record.departure_lng;
|
const hasDepartureLocation = record.departure_lat && record.departure_lng;
|
||||||
@@ -175,7 +146,7 @@ export default function AttendanceLocation() {
|
|||||||
mapInstanceRef.current = null;
|
mapInstanceRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [record, loading]);
|
}, [record, isPending]);
|
||||||
|
|
||||||
const formatDatetimeLocal = (datetime: string | null | undefined): string => {
|
const formatDatetimeLocal = (datetime: string | null | undefined): string => {
|
||||||
if (!datetime) return "—";
|
if (!datetime) return "—";
|
||||||
@@ -185,56 +156,6 @@ export default function AttendanceLocation() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "100%", height: "300px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1.25rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[0, 1].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "50%" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line w-full" />
|
|
||||||
<div className="admin-skeleton-line w-3/4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -248,102 +169,70 @@ export default function AttendanceLocation() {
|
|||||||
const month = shiftDateStr.substring(0, 7);
|
const month = shiftDateStr.substring(0, 7);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Skeleton
|
||||||
<motion.div
|
name="attendance-location"
|
||||||
className="admin-page-header"
|
loading={isPending}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
fixture={<AttendanceLocationFixture />}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.25 }}
|
<div>
|
||||||
>
|
<motion.div
|
||||||
<div>
|
className="admin-page-header"
|
||||||
<h1 className="admin-page-title">Poloha záznamu</h1>
|
initial={{ opacity: 0, y: 12 }}
|
||||||
</div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="admin-page-actions">
|
transition={{ duration: 0.25 }}
|
||||||
<Link
|
>
|
||||||
to={`/attendance/admin?month=${month}`}
|
<div>
|
||||||
className="admin-btn admin-btn-secondary"
|
<h1 className="admin-page-title">Poloha záznamu</h1>
|
||||||
>
|
</div>
|
||||||
← Zpět na správu
|
<div className="admin-page-actions">
|
||||||
</Link>
|
<Link
|
||||||
</div>
|
to={`/attendance/admin?month=${month}`}
|
||||||
</motion.div>
|
className="admin-btn admin-btn-secondary"
|
||||||
|
|
||||||
<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" : ""}`}
|
|
||||||
>
|
>
|
||||||
<h3 className="attendance-location-title">Příchod</h3>
|
← Zpět na správu
|
||||||
<div className="attendance-location-time">
|
</Link>
|
||||||
{record.arrival_time
|
</div>
|
||||||
? formatDatetimeLocal(record.arrival_time)
|
</motion.div>
|
||||||
: "—"}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Departure */}
|
<motion.div
|
||||||
{(hasDepartureLocation || record.departure_time) && (
|
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
|
<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">
|
<div className="attendance-location-time">
|
||||||
{record.departure_time
|
{record.arrival_time
|
||||||
? formatDatetimeLocal(record.departure_time)
|
? formatDatetimeLocal(record.arrival_time)
|
||||||
: "—"}
|
: "—"}
|
||||||
</div>
|
</div>
|
||||||
{hasDepartureLocation ? (
|
{hasArrivalLocation ? (
|
||||||
<>
|
<>
|
||||||
<div className="attendance-location-address">
|
<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>
|
||||||
<div className="attendance-location-coords">
|
<div className="attendance-location-coords">
|
||||||
GPS: {record.departure_lat}, {record.departure_lng}
|
GPS: {record.arrival_lat}, {record.arrival_lng}
|
||||||
{record.departure_accuracy &&
|
{record.arrival_accuracy &&
|
||||||
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
|
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
|
||||||
@@ -357,10 +246,48 @@ export default function AttendanceLocation() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
</div>
|
</motion.div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
@@ -8,6 +9,8 @@ import FormField from "../components/FormField";
|
|||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import AuditLogFixture from "../fixtures/AuditLogFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -77,13 +80,6 @@ interface AuditLogEntry {
|
|||||||
user_ip: string | null;
|
user_ip: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginationData {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Filters {
|
interface Filters {
|
||||||
search: string;
|
search: string;
|
||||||
action: string;
|
action: string;
|
||||||
@@ -95,9 +91,7 @@ interface Filters {
|
|||||||
export default function AuditLog() {
|
export default function AuditLog() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [pagination, setPagination] = useState<PaginationData | null>(null);
|
|
||||||
const [filters, setFilters] = useState<Filters>({
|
const [filters, setFilters] = useState<Filters>({
|
||||||
search: "",
|
search: "",
|
||||||
action: "",
|
action: "",
|
||||||
@@ -105,53 +99,57 @@ export default function AuditLog() {
|
|||||||
date_from: "",
|
date_from: "",
|
||||||
date_to: "",
|
date_to: "",
|
||||||
});
|
});
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(50);
|
||||||
const [showCleanup, setShowCleanup] = useState(false);
|
const [showCleanup, setShowCleanup] = useState(false);
|
||||||
const [cleanupDays, setCleanupDays] = useState(90);
|
const [cleanupDays, setCleanupDays] = useState(90);
|
||||||
const [cleaning, setCleaning] = useState(false);
|
const [cleaning, setCleaning] = useState(false);
|
||||||
|
|
||||||
const fetchLogs = useCallback(
|
const { data: logsData, isPending } = useQuery({
|
||||||
async (page = 1, perPage = 50) => {
|
queryKey: [
|
||||||
setLoading(true);
|
"audit-log",
|
||||||
try {
|
{
|
||||||
const params = new URLSearchParams({
|
search: filters.search,
|
||||||
page: String(page),
|
action: filters.action,
|
||||||
per_page: String(perPage),
|
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);
|
const response = await apiFetch(
|
||||||
if (filters.action) params.set("action", filters.action);
|
`${API_BASE}/audit-log?${params.toString()}`,
|
||||||
if (filters.entity_type) params.set("entity_type", filters.entity_type);
|
);
|
||||||
if (filters.date_from) params.set("date_from", filters.date_from);
|
if (response.status === 401) throw new Error("Unauthorized");
|
||||||
if (filters.date_to) params.set("date_to", filters.date_to);
|
const result = await response.json();
|
||||||
|
if (!result.success)
|
||||||
const response = await apiFetch(
|
throw new Error(result.error || "Nepodařilo se načíst audit log");
|
||||||
`${API_BASE}/audit-log?${params.toString()}`,
|
return {
|
||||||
);
|
data: Array.isArray(result.data) ? result.data : [],
|
||||||
const data = await response.json();
|
pagination: {
|
||||||
|
total: result.pagination?.total ?? 0,
|
||||||
if (data.success) {
|
page: result.pagination?.page ?? 1,
|
||||||
setLogs(Array.isArray(data.data) ? data.data : []);
|
per_page: result.pagination?.limit ?? perPage,
|
||||||
setPagination({
|
total_pages: result.pagination?.total_pages ?? 1,
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[filters, alert],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const logs = logsData?.data ?? [];
|
||||||
fetchLogs();
|
const pagination = logsData?.pagination ?? null;
|
||||||
}, [fetchLogs]);
|
|
||||||
|
|
||||||
if (!hasPermission("settings.audit")) {
|
if (!hasPermission("settings.audit")) {
|
||||||
return <Forbidden />;
|
return <Forbidden />;
|
||||||
@@ -159,14 +157,16 @@ export default function AuditLog() {
|
|||||||
|
|
||||||
const handleFilterChange = (key: keyof Filters, value: string) => {
|
const handleFilterChange = (key: keyof Filters, value: string) => {
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
fetchLogs(newPage, pagination?.per_page || 50);
|
setPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePerPageChange = (newPerPage: number) => {
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
fetchLogs(1, newPerPage);
|
setPage(1);
|
||||||
|
setPerPage(newPerPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCleanup = async () => {
|
const handleCleanup = async () => {
|
||||||
@@ -181,7 +181,7 @@ export default function AuditLog() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success(data.message);
|
alert.success(data.message);
|
||||||
setShowCleanup(false);
|
setShowCleanup(false);
|
||||||
fetchLogs();
|
queryClient.invalidateQueries({ queryKey: ["audit-log"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error);
|
alert.error(data.error);
|
||||||
}
|
}
|
||||||
@@ -197,66 +197,15 @@ export default function AuditLog() {
|
|||||||
return new Date(dateString).toLocaleString("cs-CZ");
|
return new Date(dateString).toLocaleString("cs-CZ");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && logs.length === 0) {
|
if (isPending && logs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="audit-log"
|
||||||
className="admin-skeleton-row"
|
loading={isPending && logs.length === 0}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<AuditLogFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "160px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "100px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton"
|
|
||||||
style={{ gap: "0.75rem", padding: "1rem" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "100%", borderRadius: "4px" }}
|
|
||||||
/>
|
|
||||||
{Array.from({ length: 8 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "120px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "70px", borderRadius: "10px" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line flex-1" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "90px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,112 +391,152 @@ export default function AuditLog() {
|
|||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<table className="admin-table">
|
<Skeleton
|
||||||
<thead>
|
name="audit-log-rows"
|
||||||
<tr>
|
loading={isPending}
|
||||||
<th>Čas</th>
|
fixture={
|
||||||
<th>Uživatel</th>
|
<table className="admin-table">
|
||||||
<th>Akce</th>
|
<thead>
|
||||||
<th>Typ entity</th>
|
<tr>
|
||||||
<th>Popis</th>
|
<th>Čas</th>
|
||||||
<th>IP</th>
|
<th>Uživatel</th>
|
||||||
</tr>
|
<th>Akce</th>
|
||||||
</thead>
|
<th>Typ entity</th>
|
||||||
<tbody>
|
<th>Popis</th>
|
||||||
{loading &&
|
<th>IP</th>
|
||||||
Array.from({ length: 10 }, (_, i) => (
|
|
||||||
<tr key={`skeleton-${i}`}>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "110px", height: "14px" }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "80px", height: "14px" }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "70px",
|
|
||||||
height: "22px",
|
|
||||||
borderRadius: "10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
{!loading && logs.length === 0 && (
|
<tbody>
|
||||||
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 110,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 80,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 70,
|
||||||
|
height: 22,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 80,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 90,
|
||||||
|
height: 14,
|
||||||
|
background: "var(--bg-tertiary)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6}>
|
<th>Čas</th>
|
||||||
<div className="admin-empty-state">
|
<th>Uživatel</th>
|
||||||
<div className="admin-empty-icon">
|
<th>Akce</th>
|
||||||
<svg
|
<th>Typ entity</th>
|
||||||
width="28"
|
<th>Popis</th>
|
||||||
height="28"
|
<th>IP</th>
|
||||||
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>
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
{!loading &&
|
<tbody>
|
||||||
logs.map((log) => (
|
{logs.length === 0 && (
|
||||||
<tr key={log.id}>
|
<tr>
|
||||||
<td className="admin-mono">
|
<td colSpan={6}>
|
||||||
{formatDatetime(log.created_at)}
|
<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>
|
</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>
|
</tr>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
{logs.length > 0 &&
|
||||||
</table>
|
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>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import { companySettingsOptions } from "../lib/queries/settings";
|
||||||
|
import { bankAccountsOptions } from "../lib/queries/common";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import CompanySettingsFixture from "../fixtures/CompanySettingsFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
const DEFAULT_FIELD_ORDER = [
|
const DEFAULT_FIELD_ORDER = [
|
||||||
@@ -68,7 +73,7 @@ export default function CompanySettings({
|
|||||||
}: { embedded?: boolean } = {}) {
|
}: { embedded?: boolean } = {}) {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [uploadingLogo, setUploadingLogo] = useState(false);
|
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||||
const [uploadingLogoDark, setUploadingLogoDark] = useState(false);
|
const [uploadingLogoDark, setUploadingLogoDark] = useState(false);
|
||||||
@@ -89,14 +94,12 @@ export default function CompanySettings({
|
|||||||
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
const [fieldOrder, setFieldOrder] = useState<string[]>([
|
||||||
...DEFAULT_FIELD_ORDER,
|
...DEFAULT_FIELD_ORDER,
|
||||||
]);
|
]);
|
||||||
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
||||||
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
|
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([
|
||||||
"CZK",
|
"CZK",
|
||||||
"EUR",
|
"EUR",
|
||||||
"USD",
|
"USD",
|
||||||
"GBP",
|
"GBP",
|
||||||
]);
|
]);
|
||||||
const [bankLoading, setBankLoading] = useState(true);
|
|
||||||
const [bankSaving, setBankSaving] = useState(false);
|
const [bankSaving, setBankSaving] = useState(false);
|
||||||
const [editingBank, setEditingBank] = useState<number | null>(null);
|
const [editingBank, setEditingBank] = useState<number | null>(null);
|
||||||
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
|
const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{
|
||||||
@@ -182,84 +185,63 @@ export default function CompanySettings({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// ── TanStack Query: company settings ──
|
||||||
try {
|
const { data: settingsData, isPending: settingsLoading } = useQuery(
|
||||||
const response = await apiFetch(`${API_BASE}/company-settings`);
|
companySettingsOptions(),
|
||||||
if (response.status === 401) return;
|
);
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const fetchBankAccounts = useCallback(async () => {
|
// ── TanStack Query: bank accounts ──
|
||||||
try {
|
const { data: bankAccountsData, isPending: bankLoading } = useQuery(
|
||||||
const response = await apiFetch(`${API_BASE}/bank-accounts`);
|
bankAccountsOptions(),
|
||||||
if (response.status === 401) return;
|
);
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
const bankAccountsList: BankAccount[] = Array.isArray(bankAccountsData)
|
||||||
setBankAccounts(result.data);
|
? (bankAccountsData as unknown as BankAccount[])
|
||||||
}
|
: [];
|
||||||
} catch {
|
|
||||||
// ignore
|
// Populate form state when settings data arrives
|
||||||
} finally {
|
useEffect(() => {
|
||||||
setBankLoading(false);
|
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 = () => {
|
const resetBankForm = () => {
|
||||||
setEditingBank(null);
|
setEditingBank(null);
|
||||||
@@ -294,7 +276,7 @@ export default function CompanySettings({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
resetBankForm();
|
resetBankForm();
|
||||||
fetchBankAccounts();
|
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při ukládání");
|
alert.error(result.error || "Chyba při ukládání");
|
||||||
}
|
}
|
||||||
@@ -322,7 +304,7 @@ export default function CompanySettings({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||||
fetchBankAccounts();
|
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při mazání");
|
alert.error(result.error || "Chyba při mazání");
|
||||||
}
|
}
|
||||||
@@ -346,11 +328,6 @@ export default function CompanySettings({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
fetchBankAccounts();
|
|
||||||
}, [fetchData, fetchBankAccounts]);
|
|
||||||
|
|
||||||
// Cleanup blob URLs on unmount
|
// Cleanup blob URLs on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -377,6 +354,7 @@ export default function CompanySettings({
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Nastavení bylo uloženo");
|
alert.success(result.message || "Nastavení bylo uloženo");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||||
}
|
}
|
||||||
@@ -411,6 +389,7 @@ export default function CompanySettings({
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Logo bylo nahráno");
|
alert.success(result.message || "Logo bylo nahráno");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||||
fetchLogo(variant);
|
fetchLogo(variant);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se nahrát logo");
|
alert.error(result.error || "Nepodařilo se nahrát logo");
|
||||||
@@ -429,50 +408,15 @@ export default function CompanySettings({
|
|||||||
|
|
||||||
if (!embedded && !hasPermission("settings.manage")) return <Forbidden />;
|
if (!embedded && !hasPermission("settings.manage")) return <Forbidden />;
|
||||||
|
|
||||||
if (loading) {
|
if (settingsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="company-settings"
|
||||||
className="admin-skeleton-row"
|
loading={settingsLoading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<CompanySettingsFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "120px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: "1.25rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "60%" }}
|
|
||||||
/>
|
|
||||||
{[0, 1, 2].map((j) => (
|
|
||||||
<div key={j} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
|
||||||
<div className="admin-skeleton-line w-1/2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,18 +718,16 @@ export default function CompanySettings({
|
|||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{bankLoading ? (
|
{bankLoading ? (
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
<Skeleton
|
||||||
{[0, 1, 2].map((i) => (
|
name="company-settings-bank"
|
||||||
<div key={i} className="admin-skeleton-row">
|
loading={bankLoading}
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
fixture={<CompanySettingsFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
<div />
|
||||||
</div>
|
</Skeleton>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bankAccounts.length > 0 && (
|
{bankAccountsList.length > 0 && (
|
||||||
<div className="admin-table-responsive mb-4">
|
<div className="admin-table-responsive mb-4">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -801,7 +743,7 @@ export default function CompanySettings({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{bankAccounts.map((acc) => (
|
{bankAccountsList.map((acc) => (
|
||||||
<tr
|
<tr
|
||||||
key={acc.id}
|
key={acc.id}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { dashboardOptions } from "../lib/queries/dashboard";
|
||||||
|
import { require2FAOptions } from "../lib/queries/settings";
|
||||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||||
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||||
@@ -12,6 +15,8 @@ import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
|||||||
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
||||||
import DashProfile from "../components/dashboard/DashProfile";
|
import DashProfile from "../components/dashboard/DashProfile";
|
||||||
import DashSessions from "../components/dashboard/DashSessions";
|
import DashSessions from "../components/dashboard/DashSessions";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import DashboardFixture from "../fixtures/DashboardFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -69,13 +74,17 @@ export default function Dashboard() {
|
|||||||
const { user, updateUser, hasPermission } = useAuth();
|
const { user, updateUser, hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
|
||||||
const [dashData, setDashData] = useState<DashData | null>(null);
|
|
||||||
const [dashLoading, setDashLoading] = useState(true);
|
|
||||||
const [punching, setPunching] = useState(false);
|
const [punching, setPunching] = useState(false);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: dashDataRaw, isPending: dashLoading } =
|
||||||
|
useQuery(dashboardOptions());
|
||||||
|
const dashData = dashDataRaw as DashData | undefined;
|
||||||
|
const { data: totpData, isPending: totpLoading } =
|
||||||
|
useQuery(require2FAOptions());
|
||||||
|
const totpEnabled = totpData?.require_2fa ?? !!user?.totpEnabled;
|
||||||
|
|
||||||
// 2FA state - sdileny mezi profilem a bannerem
|
// 2FA state - sdileny mezi profilem a bannerem
|
||||||
const [totpEnabled, setTotpEnabled] = useState(false);
|
|
||||||
const [totpLoading, setTotpLoading] = useState(true);
|
|
||||||
const [show2FASetup, setShow2FASetup] = useState(false);
|
const [show2FASetup, setShow2FASetup] = useState(false);
|
||||||
const [show2FADisable, setShow2FADisable] = useState(false);
|
const [show2FADisable, setShow2FADisable] = useState(false);
|
||||||
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
||||||
@@ -88,46 +97,6 @@ export default function Dashboard() {
|
|||||||
useModalLock(show2FASetup);
|
useModalLock(show2FASetup);
|
||||||
useModalLock(show2FADisable);
|
useModalLock(show2FADisable);
|
||||||
|
|
||||||
const fetchDashboard = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/dashboard`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success !== false) {
|
|
||||||
setDashData(data.data || data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.error("Dashboard fetch error:", err);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setDashLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDashboard();
|
|
||||||
}, [fetchDashboard]);
|
|
||||||
|
|
||||||
// 2FA status fetch
|
|
||||||
const fetch2FAStatus = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/totp/setup`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setTotpEnabled(!!user?.totpEnabled);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 2FA status fetch failed silently
|
|
||||||
setTotpEnabled(!!user?.totpEnabled);
|
|
||||||
} finally {
|
|
||||||
setTotpLoading(false);
|
|
||||||
}
|
|
||||||
}, [user?.totpEnabled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch2FAStatus();
|
|
||||||
}, [fetch2FAStatus]);
|
|
||||||
|
|
||||||
// Punch (prichod/odchod) primo z dashboardu
|
// Punch (prichod/odchod) primo z dashboardu
|
||||||
const handleQuickPunch = useCallback(() => {
|
const handleQuickPunch = useCallback(() => {
|
||||||
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
||||||
@@ -143,7 +112,7 @@ export default function Dashboard() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
alert.success(result.data?.message || "Docházka zaznamenána");
|
||||||
fetchDashboard();
|
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Chyba při záznamu docházky");
|
alert.error(result.error || "Chyba při záznamu docházky");
|
||||||
}
|
}
|
||||||
@@ -167,7 +136,7 @@ export default function Dashboard() {
|
|||||||
() => submitPunch({}),
|
() => submitPunch({}),
|
||||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||||
);
|
);
|
||||||
}, [dashData, alert, fetchDashboard]);
|
}, [dashData, alert, queryClient]);
|
||||||
|
|
||||||
// 2FA handlery
|
// 2FA handlery
|
||||||
const handleStart2FASetup = async () => {
|
const handleStart2FASetup = async () => {
|
||||||
@@ -202,7 +171,7 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTotpEnabled(true);
|
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||||
setBackupCodes(data.data?.backup_codes || null);
|
setBackupCodes(data.data?.backup_codes || null);
|
||||||
setTotpSecret(null);
|
setTotpSecret(null);
|
||||||
setTotpQrUri(null);
|
setTotpQrUri(null);
|
||||||
@@ -230,7 +199,7 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTotpEnabled(false);
|
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||||
setShow2FADisable(false);
|
setShow2FADisable(false);
|
||||||
setDisableCode("");
|
setDisableCode("");
|
||||||
updateUser({ totpEnabled: false });
|
updateUser({ totpEnabled: false });
|
||||||
@@ -337,62 +306,13 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Skeleton loading */}
|
{/* Skeleton loading */}
|
||||||
{dashLoading && (
|
{dashLoading && (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.25rem" }}>
|
<Skeleton
|
||||||
<div className="admin-kpi-grid admin-kpi-4">
|
name="dashboard"
|
||||||
{[0, 1, 2, 3].map((i) => (
|
loading={dashLoading}
|
||||||
<div
|
fixture={<DashboardFixture />}
|
||||||
key={i}
|
>
|
||||||
className="admin-skeleton-line h-24"
|
<div />
|
||||||
style={{ borderRadius: "10px" }}
|
</Skeleton>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
{/* 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 {
|
import { useState, useEffect, useRef, lazy, Suspense } from "react";
|
||||||
useState,
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
lazy,
|
|
||||||
Suspense,
|
|
||||||
} from "react";
|
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link, useSearchParams } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
@@ -17,8 +11,18 @@ import apiFetch from "../utils/api";
|
|||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import {
|
||||||
|
invoiceListOptions,
|
||||||
|
invoiceStatsOptions,
|
||||||
|
type Invoice,
|
||||||
|
type InvoiceStats,
|
||||||
|
type CurrencyAmount,
|
||||||
|
} from "../lib/queries/invoices";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import InvoicesFixture from "../fixtures/InvoicesFixture";
|
||||||
|
import ReceivedInvoicesFixture from "../fixtures/ReceivedInvoicesFixture";
|
||||||
|
|
||||||
const ReceivedInvoices = lazy(() => import("./ReceivedInvoices"));
|
const ReceivedInvoices = lazy(() => import("./ReceivedInvoices"));
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -39,11 +43,6 @@ const MONTH_NAMES = [
|
|||||||
"prosinec",
|
"prosinec",
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CurrencyAmount {
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMultiCurrency(amounts: CurrencyAmount[]): string {
|
function formatMultiCurrency(amounts: CurrencyAmount[]): string {
|
||||||
if (!Array.isArray(amounts) || amounts.length === 0) return "0 Kč";
|
if (!Array.isArray(amounts) || amounts.length === 0) return "0 Kč";
|
||||||
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
|
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
|
||||||
@@ -84,31 +83,6 @@ const STATUS_FILTERS = [
|
|||||||
{ value: "overdue", label: "Po splatnosti" },
|
{ value: "overdue", label: "Po splatnosti" },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Invoice {
|
|
||||||
id: number;
|
|
||||||
invoice_number: string;
|
|
||||||
customer_name: string | null;
|
|
||||||
status: string;
|
|
||||||
issue_date: string;
|
|
||||||
due_date: string;
|
|
||||||
total: number;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InvoiceStats {
|
|
||||||
paid_month: CurrencyAmount[];
|
|
||||||
paid_month_czk: number;
|
|
||||||
paid_month_count: number;
|
|
||||||
awaiting: CurrencyAmount[];
|
|
||||||
awaiting_czk: number;
|
|
||||||
awaiting_count: number;
|
|
||||||
overdue: CurrencyAmount[];
|
|
||||||
overdue_czk: number;
|
|
||||||
overdue_count: number;
|
|
||||||
vat_month: CurrencyAmount[];
|
|
||||||
vat_month_czk: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DraftData {
|
interface DraftData {
|
||||||
form: Record<string, unknown>;
|
form: Record<string, unknown>;
|
||||||
items: Record<string, unknown>[];
|
items: Record<string, unknown>[];
|
||||||
@@ -134,8 +108,6 @@ export default function Invoices() {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1);
|
const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1);
|
||||||
const [statsYear, setStatsYear] = useState(now.getFullYear());
|
const [statsYear, setStatsYear] = useState(now.getFullYear());
|
||||||
const [stats, setStats] = useState<InvoiceStats | null>(null);
|
|
||||||
const [statsLoading, setStatsLoading] = useState(true);
|
|
||||||
const hasLoadedOnce = useRef(false);
|
const hasLoadedOnce = useRef(false);
|
||||||
const slideDirection = useRef(0);
|
const slideDirection = useRef(0);
|
||||||
const blobUrlRef = useRef<string | null>(null);
|
const blobUrlRef = useRef<string | null>(null);
|
||||||
@@ -154,28 +126,15 @@ export default function Invoices() {
|
|||||||
statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear();
|
statsMonth === now.getMonth() + 1 && statsYear === now.getFullYear();
|
||||||
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`;
|
const monthLabel = `${MONTH_NAMES[statsMonth - 1]} ${statsYear}`;
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
const statsQuery = useQuery(invoiceStatsOptions(statsMonth, statsYear));
|
||||||
setStatsLoading(true);
|
const stats = statsQuery.data ?? null;
|
||||||
try {
|
|
||||||
const res = await apiFetch(
|
|
||||||
`${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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
if (statsQuery.data) {
|
||||||
}, [fetchStats]);
|
hasLoadedOnce.current = true;
|
||||||
|
setSlideKey((k) => k + 1);
|
||||||
|
}
|
||||||
|
}, [statsQuery.data]);
|
||||||
|
|
||||||
const prevMonth = () => {
|
const prevMonth = () => {
|
||||||
slideDirection.current = -1;
|
slideDirection.current = -1;
|
||||||
@@ -225,24 +184,23 @@ export default function Invoices() {
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: invoices,
|
items: invoices,
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
refetch: fetchData,
|
isPending: initialLoad,
|
||||||
} = useListData<Invoice>("invoices", {
|
isFetching: loading,
|
||||||
search,
|
} = usePaginatedQuery<Invoice>(
|
||||||
sort,
|
invoiceListOptions({
|
||||||
order,
|
search,
|
||||||
page,
|
sort,
|
||||||
extraParams: {
|
order,
|
||||||
month: String(statsMonth),
|
page,
|
||||||
year: String(statsYear),
|
month: statsMonth,
|
||||||
...(statusFilter ? { status: statusFilter } : {}),
|
year: statsYear,
|
||||||
},
|
status: statusFilter || undefined,
|
||||||
errorMsg: "Nepodařilo se načíst faktury",
|
}),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!hasPermission("invoices.view")) return <Forbidden />;
|
if (!hasPermission("invoices.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -260,8 +218,8 @@ export default function Invoices() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, invoice: null });
|
setDeleteConfirm({ show: false, invoice: null });
|
||||||
alert.success(result.message || "Faktura byla smazána");
|
alert.success(result.message || "Faktura byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
fetchStats();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
||||||
}
|
}
|
||||||
@@ -283,8 +241,8 @@ export default function Invoices() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success("Faktura označena jako zaplacená");
|
alert.success("Faktura označena jako zaplacená");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
fetchStats();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se změnit stav");
|
alert.error(data.error || "Nepodařilo se změnit stav");
|
||||||
}
|
}
|
||||||
@@ -323,81 +281,13 @@ export default function Invoices() {
|
|||||||
|
|
||||||
if (initialLoad) {
|
if (initialLoad) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
name="invoices"
|
||||||
<div
|
loading={initialLoad}
|
||||||
className="admin-skeleton-row"
|
fixture={<InvoicesFixture />}
|
||||||
style={{ justifyContent: "space-between" }}
|
>
|
||||||
>
|
<div />
|
||||||
<div>
|
</Skeleton>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,35 +418,13 @@ export default function Invoices() {
|
|||||||
>
|
>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-kpi-grid admin-kpi-4"
|
name="invoices-received-kpi"
|
||||||
style={{ marginBottom: "1.5rem" }}
|
loading={true}
|
||||||
|
fixture={<ReceivedInvoicesFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2, 3].map((i) => (
|
<div />
|
||||||
<div key={i} className="admin-stat-card">
|
</Skeleton>
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "60%",
|
|
||||||
height: "11px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
height: "28px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "50%", height: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ReceivedInvoices
|
<ReceivedInvoices
|
||||||
@@ -574,36 +442,14 @@ export default function Invoices() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.1 }}
|
transition={{ duration: 0.25, delay: 0.1 }}
|
||||||
>
|
>
|
||||||
{!hasLoadedOnce.current && statsLoading ? (
|
{statsQuery.isPending && !hasLoadedOnce.current ? (
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-kpi-grid admin-kpi-4"
|
name="invoices-kpi"
|
||||||
style={{ marginBottom: "1.5rem" }}
|
loading={statsQuery.isPending && !hasLoadedOnce.current}
|
||||||
|
fixture={<InvoicesFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2, 3].map((i) => (
|
<div />
|
||||||
<div key={i} className="admin-stat-card">
|
</Skeleton>
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "60%",
|
|
||||||
height: "11px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{
|
|
||||||
width: "40%",
|
|
||||||
height: "28px",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line"
|
|
||||||
style={{ width: "50%", height: "12px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
stats && (
|
stats && (
|
||||||
<div style={{ overflow: "hidden", marginBottom: "1.5rem" }}>
|
<div style={{ overflow: "hidden", marginBottom: "1.5rem" }}>
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
|
import {
|
||||||
|
leavePendingOptions,
|
||||||
|
leaveProcessedOptions,
|
||||||
|
} from "../lib/queries/leave";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import LeaveApprovalFixture from "../fixtures/LeaveApprovalFixture";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -101,15 +108,24 @@ function mapLeaveRequest(raw: RawLeaveRequest): LeaveRequest {
|
|||||||
export default function LeaveApproval() {
|
export default function LeaveApproval() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
||||||
"pending",
|
"pending",
|
||||||
);
|
);
|
||||||
const [pendingRequests, setPendingRequests] = useState<LeaveRequest[]>([]);
|
const { data: pendingData, isPending: loading } = useQuery(
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
leavePendingOptions(),
|
||||||
const [processedRequests, setProcessedRequests] = useState<LeaveRequest[]>(
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
const { data: processedData } = useQuery({
|
||||||
|
...leaveProcessedOptions(),
|
||||||
|
enabled: activeTab === "processed",
|
||||||
|
});
|
||||||
|
const pendingRequests =
|
||||||
|
(pendingData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ?? [];
|
||||||
|
const pendingCount = pendingRequests.length;
|
||||||
|
const processedRequests =
|
||||||
|
(processedData as RawLeaveRequest[] | undefined)?.map(mapLeaveRequest) ??
|
||||||
|
[];
|
||||||
|
|
||||||
const [approveModal, setApproveModal] = useState<{
|
const [approveModal, setApproveModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
request: LeaveRequest | null;
|
request: LeaveRequest | null;
|
||||||
@@ -123,67 +139,6 @@ export default function LeaveApproval() {
|
|||||||
|
|
||||||
useModalLock(rejectModal.open);
|
useModalLock(rejectModal.open);
|
||||||
|
|
||||||
const fetchPending = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=pending`,
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
const mapped = (result.data as RawLeaveRequest[]).map(mapLeaveRequest);
|
|
||||||
setPendingRequests(mapped);
|
|
||||||
setPendingCount(result.pagination?.total ?? mapped.length);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst žádosti");
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
const fetchProcessed = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=approved`,
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const resultApproved = await response.json();
|
|
||||||
|
|
||||||
const response2 = await apiFetch(
|
|
||||||
`${API_BASE}/leave-requests?status=rejected`,
|
|
||||||
);
|
|
||||||
if (response2.status === 401) return;
|
|
||||||
const resultRejected = await response2.json();
|
|
||||||
|
|
||||||
const all = [
|
|
||||||
...(resultApproved.success
|
|
||||||
? (resultApproved.data as RawLeaveRequest[]).map(mapLeaveRequest)
|
|
||||||
: []),
|
|
||||||
...(resultRejected.success
|
|
||||||
? (resultRejected.data as RawLeaveRequest[]).map(mapLeaveRequest)
|
|
||||||
: []),
|
|
||||||
].sort(
|
|
||||||
(a: LeaveRequest, b: LeaveRequest) =>
|
|
||||||
(b.reviewed_at ? new Date(b.reviewed_at).getTime() : 0) -
|
|
||||||
(a.reviewed_at ? new Date(a.reviewed_at).getTime() : 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
setProcessedRequests(all);
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst vyřízené žádosti");
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
fetchPending().finally(() => setLoading(false));
|
|
||||||
}, [fetchPending]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab === "processed" && processedRequests.length === 0) {
|
|
||||||
fetchProcessed();
|
|
||||||
}
|
|
||||||
}, [activeTab, processedRequests.length, fetchProcessed]);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = async () => {
|
||||||
@@ -202,8 +157,7 @@ export default function LeaveApproval() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setApproveModal({ open: false, request: null });
|
setApproveModal({ open: false, request: null });
|
||||||
await fetchPending();
|
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||||
setProcessedRequests([]);
|
|
||||||
alert.success("Žádost byla schválena");
|
alert.success("Žádost byla schválena");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -240,8 +194,7 @@ export default function LeaveApproval() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setRejectModal({ open: false, request: null });
|
setRejectModal({ open: false, request: null });
|
||||||
setRejectNote("");
|
setRejectNote("");
|
||||||
await fetchPending();
|
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||||
setProcessedRequests([]);
|
|
||||||
alert.success("Žádost byla zamítnuta");
|
alert.success("Žádost byla zamítnuta");
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -253,402 +206,378 @@ export default function LeaveApproval() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
return (
|
||||||
return (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
name="leave-approval"
|
||||||
<div
|
loading={loading}
|
||||||
className="admin-skeleton-row"
|
fixture={<LeaveApprovalFixture />}
|
||||||
style={{ justifyContent: "space-between" }}
|
>
|
||||||
|
<div>
|
||||||
|
<motion.div
|
||||||
|
className="admin-page-header"
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<h1 className="admin-page-title">Schvalování nepřítomnosti</h1>
|
||||||
className="admin-skeleton-line h-8"
|
<p className="admin-page-subtitle">
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
{pendingCount > 0
|
||||||
/>
|
? `${pendingCount} ${czechPlural(pendingCount, "žádost čeká", "žádosti čekají", "žádostí čeká")} na schválení`
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
: "Žádné čekající žádosti"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="admin-skeleton-line w-1/3 mb-2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
{/* Tabs */}
|
||||||
<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" && (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
{pendingRequests.length === 0 ? (
|
<div className="admin-tabs mb-6">
|
||||||
<div className="admin-card">
|
<button
|
||||||
<div className="admin-card-body">
|
className={`admin-tab ${activeTab === "pending" ? "active" : ""}`}
|
||||||
<div className="admin-empty-state">
|
onClick={() => setActiveTab("pending")}
|
||||||
<svg
|
>
|
||||||
width="48"
|
Ke schválení
|
||||||
height="48"
|
{pendingCount > 0 && (
|
||||||
viewBox="0 0 24 24"
|
<span
|
||||||
fill="none"
|
className="admin-badge badge-pending"
|
||||||
stroke="currentColor"
|
style={{
|
||||||
strokeWidth="1.5"
|
marginLeft: "0.5rem",
|
||||||
className="text-muted mb-4"
|
fontSize: "0.7rem",
|
||||||
>
|
padding: "0.15rem 0.5rem",
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
}}
|
||||||
<polyline points="22 4 12 14.01 9 11.01" />
|
>
|
||||||
</svg>
|
{pendingCount}
|
||||||
<p>Žádné čekající žádosti</p>
|
</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>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
display: "flex",
|
||||||
>
|
flexDirection: "column",
|
||||||
{pendingRequests.map((req) => (
|
gap: "1rem",
|
||||||
<div key={req.id} className="admin-card">
|
}}
|
||||||
<div
|
>
|
||||||
className="admin-card-body"
|
{pendingRequests.map((req) => (
|
||||||
style={{ padding: "1.25rem" }}
|
<div key={req.id} className="admin-card">
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
className="admin-card-body"
|
||||||
display: "flex",
|
style={{ padding: "1.25rem" }}
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<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
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "0.5rem",
|
justifyContent: "space-between",
|
||||||
flexShrink: 0,
|
alignItems: "flex-start",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "1rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<div className="flex-1">
|
||||||
onClick={() =>
|
<div className="flex-row-gap mb-2">
|
||||||
setApproveModal({ open: true, request: req })
|
<strong style={{ fontSize: "1rem" }}>
|
||||||
}
|
{req.employee_name}
|
||||||
className="admin-btn admin-btn-sm"
|
</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={{
|
style={{
|
||||||
background: "var(--success-light)",
|
display: "flex",
|
||||||
color: "var(--success)",
|
gap: "0.5rem",
|
||||||
border: "none",
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Schválit
|
<button
|
||||||
</button>
|
onClick={() =>
|
||||||
<button
|
setApproveModal({ open: true, request: req })
|
||||||
onClick={() =>
|
}
|
||||||
setRejectModal({ open: true, request: req })
|
className="admin-btn admin-btn-sm"
|
||||||
}
|
style={{
|
||||||
className="admin-btn admin-btn-sm"
|
background: "var(--success-light)",
|
||||||
style={{
|
color: "var(--success)",
|
||||||
background: "var(--danger-light)",
|
border: "none",
|
||||||
color: "var(--danger)",
|
}}
|
||||||
border: "none",
|
>
|
||||||
}}
|
Schválit
|
||||||
>
|
</button>
|
||||||
Zamítnout
|
<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>
|
</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>
|
||||||
)}
|
)}
|
||||||
</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>
|
</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 { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import LeaveRequestsFixture from "../fixtures/LeaveRequestsFixture";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import { leaveRequestsOptions } from "../lib/queries/leave";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -51,33 +55,16 @@ interface LeaveRequest {
|
|||||||
export default function LeaveRequests() {
|
export default function LeaveRequests() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [requests, setRequests] = useState<LeaveRequest[]>([]);
|
const { data: requests = [], isPending } = useQuery(
|
||||||
|
leaveRequestsOptions(true),
|
||||||
|
) as { data: LeaveRequest[]; isPending: boolean };
|
||||||
const [cancelModal, setCancelModal] = useState<{
|
const [cancelModal, setCancelModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
id: number | null;
|
id: number | null;
|
||||||
}>({ open: false, id: null });
|
}>({ open: false, id: null });
|
||||||
const [cancelling, setCancelling] = useState(false);
|
const [cancelling, setCancelling] = useState(false);
|
||||||
|
|
||||||
const fetchRequests = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiFetch(`${API_BASE}/leave-requests?mine=1`);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setRequests(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Nepodařilo se načíst žádosti");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [alert]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRequests();
|
|
||||||
}, [fetchRequests]);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.record")) return <Forbidden />;
|
if (!hasPermission("attendance.record")) return <Forbidden />;
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
@@ -94,7 +81,7 @@ export default function LeaveRequests() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setCancelModal({ open: false, id: null });
|
setCancelModal({ open: false, id: null });
|
||||||
await fetchRequests();
|
queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -106,48 +93,6 @@ export default function LeaveRequests() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-row"
|
|
||||||
style={{ justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line h-10"
|
|
||||||
style={{ width: "140px", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="admin-skeleton-row">
|
|
||||||
<div className="admin-skeleton-line circle" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="admin-skeleton-line w-1/3 mb-2" />
|
|
||||||
<div
|
|
||||||
className="admin-skeleton-line w-1/4"
|
|
||||||
style={{ height: "10px" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNoteCell(req: LeaveRequest) {
|
function renderNoteCell(req: LeaveRequest) {
|
||||||
const truncate = (text: string) =>
|
const truncate = (text: string) =>
|
||||||
text.length > 40 ? `${text.substring(0, 40)}...` : text;
|
text.length > 40 ? `${text.substring(0, 40)}...` : text;
|
||||||
@@ -176,129 +121,139 @@ export default function LeaveRequests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Skeleton
|
||||||
<motion.div
|
name="leave-requests"
|
||||||
className="admin-page-header"
|
loading={isPending}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
fixture={<LeaveRequestsFixture />}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.25 }}
|
<div>
|
||||||
>
|
<motion.div
|
||||||
<div>
|
className="admin-page-header"
|
||||||
<h1 className="admin-page-title">Moje žádosti</h1>
|
initial={{ opacity: 0, y: 12 }}
|
||||||
<p className="admin-page-subtitle">Přehled žádostí o nepřítomnost</p>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</div>
|
transition={{ duration: 0.25 }}
|
||||||
</motion.div>
|
>
|
||||||
|
<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
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{requests.length === 0 ? (
|
{requests.length === 0 ? (
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
<div className="admin-empty-icon">
|
<div className="admin-empty-icon">
|
||||||
<svg
|
<svg
|
||||||
width="28"
|
width="28"
|
||||||
height="28"
|
height="28"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="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="16" y1="2" x2="16" y2="6" />
|
||||||
<line x1="8" y1="2" x2="8" y2="6" />
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
</svg>
|
</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>
|
</div>
|
||||||
<p>Zatím nemáte žádné žádosti</p>
|
) : (
|
||||||
<p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
|
<div className="admin-table-responsive">
|
||||||
Novou žádost můžete podat na stránce Docházka
|
<table className="admin-table">
|
||||||
</p>
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
) : (
|
<th>Typ</th>
|
||||||
<div className="admin-table-responsive">
|
<th>Od</th>
|
||||||
<table className="admin-table">
|
<th>Do</th>
|
||||||
<thead>
|
<th>Dny</th>
|
||||||
<tr>
|
<th>Hodiny</th>
|
||||||
<th>Typ</th>
|
<th>Stav</th>
|
||||||
<th>Od</th>
|
<th>Poznámka</th>
|
||||||
<th>Do</th>
|
<th>Podáno</th>
|
||||||
<th>Dny</th>
|
<th></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>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{requests.map((req) => (
|
||||||
</div>
|
<tr key={req.id}>
|
||||||
)}
|
<td>
|
||||||
</div>
|
<span
|
||||||
</motion.div>
|
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
|
<ConfirmModal
|
||||||
isOpen={cancelModal.open}
|
isOpen={cancelModal.open}
|
||||||
onClose={() => setCancelModal({ open: false, id: null })}
|
onClose={() => setCancelModal({ open: false, id: null })}
|
||||||
onConfirm={handleCancel}
|
onConfirm={handleCancel}
|
||||||
title="Zrušit žádost"
|
title="Zrušit žádost"
|
||||||
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
||||||
confirmText="Zrušit žádost"
|
confirmText="Zrušit žádost"
|
||||||
type="warning"
|
type="warning"
|
||||||
loading={cancelling}
|
loading={cancelling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import OrdersFixture from "../fixtures/OrdersFixture";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
|
||||||
@@ -10,7 +12,9 @@ import apiFetch from "../utils/api";
|
|||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import { orderListOptions } from "../lib/queries/orders";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -57,19 +61,13 @@ export default function Orders() {
|
|||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: orders,
|
items: orders,
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
refetch: fetchData,
|
isPending,
|
||||||
} = useListData("orders", {
|
isFetching,
|
||||||
search,
|
} = usePaginatedQuery<Order>(orderListOptions({ search, sort, order, page }));
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
page,
|
|
||||||
errorMsg: "Nepodařilo se načíst objednávky",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasPermission("orders.view")) return <Forbidden />;
|
if (!hasPermission("orders.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -90,7 +88,9 @@ export default function Orders() {
|
|||||||
setDeleteConfirm({ show: false, order: null });
|
setDeleteConfirm({ show: false, order: null });
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
alert.success(result.message || "Objednávka byla smazána");
|
alert.success(result.message || "Objednávka byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
||||||
}
|
}
|
||||||
@@ -101,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 (
|
return (
|
||||||
<div>
|
<Skeleton name="orders" loading={isPending} fixture={<OrdersFixture />}>
|
||||||
<motion.div
|
<div>
|
||||||
className="admin-page-header"
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
className="admin-page-header"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
transition={{ duration: 0.25 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
transition={{ duration: 0.25 }}
|
||||||
<div>
|
>
|
||||||
<h1 className="admin-page-title">Objednávky</h1>
|
<div>
|
||||||
<p className="admin-page-subtitle">
|
<h1 className="admin-page-title">Objednávky</h1>
|
||||||
{pagination?.total ?? orders.length}{" "}
|
<p className="admin-page-subtitle">
|
||||||
{czechPlural(
|
{pagination?.total ?? orders.length}{" "}
|
||||||
pagination?.total ?? orders.length,
|
{czechPlural(
|
||||||
"objednávka",
|
pagination?.total ?? orders.length,
|
||||||
"objednávky",
|
"objednávka",
|
||||||
"objednávek",
|
"objednávky",
|
||||||
)}
|
"objednávek",
|
||||||
</p>
|
)}
|
||||||
</div>
|
</p>
|
||||||
</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..."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{orders.length === 0 ? (
|
<motion.div
|
||||||
<div className="admin-empty-state">
|
className="admin-card"
|
||||||
<div className="admin-empty-icon">
|
initial={{ opacity: 0, y: 12 }}
|
||||||
<svg
|
animate={{ opacity: 1, y: 0 }}
|
||||||
width="28"
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
height="28"
|
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
fill="none"
|
<div className="admin-card-body">
|
||||||
stroke="currentColor"
|
<div className="admin-search-bar mb-4">
|
||||||
strokeWidth="1.5"
|
<input
|
||||||
strokeLinecap="round"
|
type="text"
|
||||||
strokeLinejoin="round"
|
value={search}
|
||||||
>
|
onChange={(e) => {
|
||||||
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
setSearch(e.target.value);
|
||||||
<line x1="3" y1="6" x2="21" y2="6" />
|
setPage(1);
|
||||||
<path d="M16 10a4 4 0 0 1-8 0" />
|
}}
|
||||||
</svg>
|
className="admin-form-input"
|
||||||
</div>
|
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
|
||||||
<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>
|
||||||
) : (
|
|
||||||
<div className="admin-table-responsive">
|
{orders.length === 0 ? (
|
||||||
<table className="admin-table">
|
<div className="admin-empty-state">
|
||||||
<thead>
|
<div className="admin-empty-icon">
|
||||||
<tr>
|
<svg
|
||||||
<th
|
width="28"
|
||||||
style={{ cursor: "pointer" }}
|
height="28"
|
||||||
onClick={() => handleSort("order_number")}
|
viewBox="0 0 24 24"
|
||||||
>
|
fill="none"
|
||||||
Číslo{" "}
|
stroke="currentColor"
|
||||||
<SortIcon
|
strokeWidth="1.5"
|
||||||
column="order_number"
|
strokeLinecap="round"
|
||||||
sort={activeSort}
|
strokeLinejoin="round"
|
||||||
order={order}
|
>
|
||||||
/>
|
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
||||||
</th>
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
<th>Nabídka</th>
|
<path d="M16 10a4 4 0 0 1-8 0" />
|
||||||
<th>Zákazník</th>
|
</svg>
|
||||||
<th
|
</div>
|
||||||
style={{ cursor: "pointer" }}
|
<p>Zatím nejsou žádné objednávky.</p>
|
||||||
onClick={() => handleSort("status")}
|
<p className="text-tertiary" style={{ fontSize: "0.875rem" }}>
|
||||||
>
|
Objednávky se vytvářejí z nabídek.
|
||||||
Stav{" "}
|
</p>
|
||||||
<SortIcon
|
</div>
|
||||||
column="status"
|
) : (
|
||||||
sort={activeSort}
|
<div className="admin-table-responsive">
|
||||||
order={order}
|
<table className="admin-table">
|
||||||
/>
|
<thead>
|
||||||
</th>
|
<tr>
|
||||||
<th
|
<th
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => handleSort("created_at")}
|
onClick={() => handleSort("order_number")}
|
||||||
>
|
>
|
||||||
Datum{" "}
|
Číslo{" "}
|
||||||
<SortIcon
|
<SortIcon
|
||||||
column="created_at"
|
column="order_number"
|
||||||
sort={activeSort}
|
sort={activeSort}
|
||||||
order={order}
|
order={order}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right">Celkem</th>
|
<th>Nabídka</th>
|
||||||
<th>Akce</th>
|
<th>Zákazník</th>
|
||||||
</tr>
|
<th
|
||||||
</thead>
|
style={{ cursor: "pointer" }}
|
||||||
<tbody>
|
onClick={() => handleSort("status")}
|
||||||
{(orders as Order[]).map((o) => (
|
>
|
||||||
<tr key={o.id}>
|
Stav{" "}
|
||||||
<td className="admin-mono">
|
<SortIcon
|
||||||
<Link to={`/orders/${o.id}`} className="link-accent">
|
column="status"
|
||||||
{o.order_number}
|
sort={activeSort}
|
||||||
</Link>
|
order={order}
|
||||||
</td>
|
/>
|
||||||
<td>
|
</th>
|
||||||
<Link
|
<th
|
||||||
to={`/offers/${o.quotation_id}`}
|
style={{ cursor: "pointer" }}
|
||||||
className="text-secondary"
|
onClick={() => handleSort("created_at")}
|
||||||
style={{ textDecoration: "none" }}
|
>
|
||||||
>
|
Datum{" "}
|
||||||
{o.quotation_number}
|
<SortIcon
|
||||||
</Link>
|
column="created_at"
|
||||||
</td>
|
sort={activeSort}
|
||||||
<td>{o.customer_name || "—"}</td>
|
order={order}
|
||||||
<td>
|
/>
|
||||||
<span
|
</th>
|
||||||
className={`admin-badge ${STATUS_CLASSES[o.status] || ""}`}
|
<th className="text-right">Celkem</th>
|
||||||
>
|
<th>Akce</th>
|
||||||
{STATUS_LABELS[o.status] || o.status}
|
</tr>
|
||||||
</span>
|
</thead>
|
||||||
</td>
|
<tbody>
|
||||||
<td className="admin-mono">{formatDate(o.created_at)}</td>
|
{(orders as Order[]).map((o) => (
|
||||||
<td className="admin-mono text-right fw-500">
|
<tr key={o.id}>
|
||||||
{formatCurrency(o.total, o.currency)}
|
<td className="admin-mono">
|
||||||
</td>
|
<Link to={`/orders/${o.id}`} className="link-accent">
|
||||||
<td>
|
{o.order_number}
|
||||||
<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>
|
|
||||||
</Link>
|
</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
|
<Link
|
||||||
to={`/invoices/${o.invoice_id}`}
|
to={`/orders/${o.id}`}
|
||||||
className="admin-btn-icon accent"
|
className="admin-btn-icon"
|
||||||
title="Zobrazit fakturu"
|
title="Detail"
|
||||||
aria-label="Zobrazit fakturu"
|
aria-label="Detail"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
@@ -320,28 +259,16 @@ export default function Orders() {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
>
|
>
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
<polyline points="14 2 14 8 20 8" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
<text
|
|
||||||
x="12"
|
|
||||||
y="16.5"
|
|
||||||
textAnchor="middle"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="none"
|
|
||||||
fontSize="9"
|
|
||||||
fontWeight="700"
|
|
||||||
>
|
|
||||||
F
|
|
||||||
</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
{o.invoice_id ? (
|
||||||
hasPermission("invoices.create") && (
|
|
||||||
<Link
|
<Link
|
||||||
to={`/invoices/new?fromOrder=${o.id}`}
|
to={`/invoices/${o.invoice_id}`}
|
||||||
className="admin-btn-icon"
|
className="admin-btn-icon accent"
|
||||||
title="Vytvořit fakturu"
|
title="Zobrazit fakturu"
|
||||||
aria-label="Vytvořit fakturu"
|
aria-label="Zobrazit fakturu"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
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" />
|
<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" />
|
<polyline points="14 2 14 8 20 8" />
|
||||||
<line x1="12" y1="11" x2="12" y2="17" />
|
<text
|
||||||
<line x1="9" y1="14" x2="15" y2="14" />
|
x="12"
|
||||||
|
y="16.5"
|
||||||
|
textAnchor="middle"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
fontSize="9"
|
||||||
|
fontWeight="700"
|
||||||
|
>
|
||||||
|
F
|
||||||
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
) : (
|
||||||
)}
|
hasPermission("invoices.create") && (
|
||||||
{hasPermission("orders.delete") && (
|
<Link
|
||||||
<button
|
to={`/invoices/new?fromOrder=${o.id}`}
|
||||||
onClick={() =>
|
className="admin-btn-icon"
|
||||||
setDeleteConfirm({ show: true, order: o })
|
title="Vytvořit fakturu"
|
||||||
}
|
aria-label="Vytvořit fakturu"
|
||||||
className="admin-btn-icon danger"
|
>
|
||||||
title="Smazat"
|
<svg
|
||||||
>
|
width="18"
|
||||||
<svg
|
height="18"
|
||||||
width="18"
|
viewBox="0 0 24 24"
|
||||||
height="18"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
fill="none"
|
strokeWidth="2"
|
||||||
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" />
|
<svg
|
||||||
<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" />
|
width="18"
|
||||||
</svg>
|
height="18"
|
||||||
</button>
|
viewBox="0 0 24 24"
|
||||||
)}
|
fill="none"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
</td>
|
strokeWidth="2"
|
||||||
</tr>
|
>
|
||||||
))}
|
<polyline points="3 6 5 6 21 6" />
|
||||||
</tbody>
|
<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" />
|
||||||
</table>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</motion.div>
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Pagination pagination={pagination} onPageChange={setPage} />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDeleteConfirm({ show: false, order: null });
|
setDeleteConfirm({ show: false, order: null });
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
}}
|
}}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
title="Smazat objednávku"
|
title="Smazat objednávku"
|
||||||
message={
|
message={
|
||||||
<>
|
<>
|
||||||
Opravdu chcete smazat objednávku "
|
Opravdu chcete smazat objednávku "
|
||||||
{deleteConfirm.order?.order_number}"? Bude smazán i přidružený
|
{deleteConfirm.order?.order_number}"? Bude smazán i
|
||||||
projekt. Tato akce je nevratná.
|
přidružený projekt. Tato akce je nevratná.
|
||||||
<label
|
<label
|
||||||
className="admin-form-checkbox"
|
className="admin-form-checkbox"
|
||||||
style={{ marginTop: "1rem", display: "flex" }}
|
style={{ marginTop: "1rem", display: "flex" }}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={deleteFiles}
|
checked={deleteFiles}
|
||||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Smazat i soubory projektu na disku</span>
|
<span>Smazat i soubory projektu na disku</span>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
cancelText="Zrušit"
|
cancelText="Zrušit"
|
||||||
type="danger"
|
type="danger"
|
||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import ProjectsFixture from "../fixtures/ProjectsFixture";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
|
||||||
@@ -10,7 +12,9 @@ import apiFetch from "../utils/api";
|
|||||||
import { formatDate, czechPlural } from "../utils/formatters";
|
import { formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useListData from "../hooks/useListData";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
|
import { projectListOptions } from "../lib/queries/projects";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -52,19 +56,15 @@ export default function Projects() {
|
|||||||
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
items: projects,
|
items: projects,
|
||||||
setItems: setProjects,
|
|
||||||
loading,
|
|
||||||
initialLoad,
|
|
||||||
pagination,
|
pagination,
|
||||||
} = useListData<Project>("projects", {
|
isPending,
|
||||||
search,
|
isFetching,
|
||||||
sort,
|
} = usePaginatedQuery<Project>(
|
||||||
order,
|
projectListOptions({ search, sort, order, page }),
|
||||||
page,
|
);
|
||||||
errorMsg: "Nepodařilo se načíst projekty",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasPermission("projects.view")) return <Forbidden />;
|
if (!hasPermission("projects.view")) return <Forbidden />;
|
||||||
|
|
||||||
@@ -80,9 +80,9 @@ export default function Projects() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success(data.message || "Projekt byl smazán");
|
alert.success(data.message || "Projekt byl smazán");
|
||||||
setProjects((prev: Project[]) =>
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
prev.filter((p) => p.id !== deleteTarget.id),
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
);
|
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se smazat projekt");
|
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 (
|
return (
|
||||||
<div>
|
<Skeleton name="projects" loading={isPending} fixture={<ProjectsFixture />}>
|
||||||
<motion.div
|
<div>
|
||||||
className="admin-page-header"
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
className="admin-page-header"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
transition={{ duration: 0.25 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
transition={{ duration: 0.25 }}
|
||||||
<div>
|
>
|
||||||
<h1 className="admin-page-title">Projekty</h1>
|
<div>
|
||||||
<p className="admin-page-subtitle">
|
<h1 className="admin-page-title">Projekty</h1>
|
||||||
{pagination?.total ?? projects.length}{" "}
|
<p className="admin-page-subtitle">
|
||||||
{czechPlural(
|
{pagination?.total ?? projects.length}{" "}
|
||||||
pagination?.total ?? projects.length,
|
{czechPlural(
|
||||||
"projekt",
|
pagination?.total ?? projects.length,
|
||||||
"projekty",
|
"projekt",
|
||||||
"projektů",
|
"projekty",
|
||||||
)}
|
"projektů",
|
||||||
</p>
|
)}
|
||||||
</div>
|
</p>
|
||||||
</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..."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{projects.length === 0 ? (
|
<motion.div
|
||||||
<div className="admin-empty-state">
|
className="admin-card"
|
||||||
<div className="admin-empty-icon">
|
initial={{ opacity: 0, y: 12 }}
|
||||||
<svg
|
animate={{ opacity: 1, y: 0 }}
|
||||||
width="28"
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
height="28"
|
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
fill="none"
|
<div className="admin-card-body">
|
||||||
stroke="currentColor"
|
<div className="admin-search-bar mb-4">
|
||||||
strokeWidth="1.5"
|
<input
|
||||||
strokeLinecap="round"
|
type="text"
|
||||||
strokeLinejoin="round"
|
value={search}
|
||||||
>
|
onChange={(e) => {
|
||||||
<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" />
|
setSearch(e.target.value);
|
||||||
</svg>
|
setPage(1);
|
||||||
</div>
|
}}
|
||||||
<p>Zatím nejsou žádné projekty.</p>
|
className="admin-form-input"
|
||||||
<p
|
placeholder="Hledat podle čísla, názvu nebo zákazníka..."
|
||||||
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>
|
||||||
) : (
|
|
||||||
<div className="admin-table-responsive">
|
{projects.length === 0 ? (
|
||||||
<table className="admin-table">
|
<div className="admin-empty-state">
|
||||||
<thead>
|
<div className="admin-empty-icon">
|
||||||
<tr>
|
<svg
|
||||||
<th
|
width="28"
|
||||||
style={{ cursor: "pointer" }}
|
height="28"
|
||||||
onClick={() => handleSort("project_number")}
|
viewBox="0 0 24 24"
|
||||||
>
|
fill="none"
|
||||||
Číslo{" "}
|
stroke="currentColor"
|
||||||
<SortIcon
|
strokeWidth="1.5"
|
||||||
column="project_number"
|
strokeLinecap="round"
|
||||||
sort={activeSort}
|
strokeLinejoin="round"
|
||||||
order={order}
|
>
|
||||||
/>
|
<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" />
|
||||||
</th>
|
</svg>
|
||||||
<th
|
</div>
|
||||||
style={{ cursor: "pointer" }}
|
<p>Zatím nejsou žádné projekty.</p>
|
||||||
onClick={() => handleSort("name")}
|
<p
|
||||||
>
|
style={{
|
||||||
Název{" "}
|
color: "var(--text-tertiary)",
|
||||||
<SortIcon column="name" sort={activeSort} order={order} />
|
fontSize: "0.875rem",
|
||||||
</th>
|
}}
|
||||||
<th>Zákazník</th>
|
>
|
||||||
<th>Zodpovědná osoba</th>
|
Vytvořte první projekt tlačítkem výše nebo automaticky při
|
||||||
<th
|
vytvoření objednávky.
|
||||||
style={{ cursor: "pointer" }}
|
</p>
|
||||||
onClick={() => handleSort("status")}
|
</div>
|
||||||
>
|
) : (
|
||||||
Stav{" "}
|
<div className="admin-table-responsive">
|
||||||
<SortIcon
|
<table className="admin-table">
|
||||||
column="status"
|
<thead>
|
||||||
sort={activeSort}
|
<tr>
|
||||||
order={order}
|
<th
|
||||||
/>
|
style={{ cursor: "pointer" }}
|
||||||
</th>
|
onClick={() => handleSort("project_number")}
|
||||||
<th
|
>
|
||||||
style={{ cursor: "pointer" }}
|
Číslo{" "}
|
||||||
onClick={() => handleSort("start_date")}
|
<SortIcon
|
||||||
>
|
column="project_number"
|
||||||
Začátek{" "}
|
sort={activeSort}
|
||||||
<SortIcon
|
order={order}
|
||||||
column="start_date"
|
/>
|
||||||
sort={activeSort}
|
</th>
|
||||||
order={order}
|
<th
|
||||||
/>
|
style={{ cursor: "pointer" }}
|
||||||
</th>
|
onClick={() => handleSort("name")}
|
||||||
<th
|
>
|
||||||
style={{ cursor: "pointer" }}
|
Název{" "}
|
||||||
onClick={() => handleSort("end_date")}
|
<SortIcon
|
||||||
>
|
column="name"
|
||||||
Konec{" "}
|
sort={activeSort}
|
||||||
<SortIcon
|
order={order}
|
||||||
column="end_date"
|
/>
|
||||||
sort={activeSort}
|
</th>
|
||||||
order={order}
|
<th>Zákazník</th>
|
||||||
/>
|
<th>Zodpovědná osoba</th>
|
||||||
</th>
|
<th
|
||||||
<th>Objednávka</th>
|
style={{ cursor: "pointer" }}
|
||||||
<th>Akce</th>
|
onClick={() => handleSort("status")}
|
||||||
</tr>
|
>
|
||||||
</thead>
|
Stav{" "}
|
||||||
<tbody>
|
<SortIcon
|
||||||
{(projects as Project[]).map((p) => (
|
column="status"
|
||||||
<tr key={p.id}>
|
sort={activeSort}
|
||||||
<td className="admin-mono">
|
order={order}
|
||||||
<Link to={`/projects/${p.id}`} className="link-accent">
|
/>
|
||||||
{p.project_number}
|
</th>
|
||||||
</Link>
|
<th
|
||||||
</td>
|
style={{ cursor: "pointer" }}
|
||||||
<td className="fw-500">{p.name || "—"}</td>
|
onClick={() => handleSort("start_date")}
|
||||||
<td>{p.customer_name || "—"}</td>
|
>
|
||||||
<td>{p.responsible_user_name || "—"}</td>
|
Začátek{" "}
|
||||||
<td>
|
<SortIcon
|
||||||
<span
|
column="start_date"
|
||||||
className={`admin-badge ${STATUS_CLASSES[p.status] || ""}`}
|
sort={activeSort}
|
||||||
>
|
order={order}
|
||||||
{STATUS_LABELS[p.status] || p.status}
|
/>
|
||||||
</span>
|
</th>
|
||||||
</td>
|
<th
|
||||||
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
style={{ cursor: "pointer" }}
|
||||||
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
onClick={() => handleSort("end_date")}
|
||||||
<td>
|
>
|
||||||
{p.order_id ? (
|
Konec{" "}
|
||||||
<Link
|
<SortIcon
|
||||||
to={`/orders/${p.order_id}`}
|
column="end_date"
|
||||||
className="text-secondary"
|
sort={activeSort}
|
||||||
style={{ textDecoration: "none" }}
|
order={order}
|
||||||
>
|
/>
|
||||||
{p.order_number}
|
</th>
|
||||||
</Link>
|
<th>Objednávka</th>
|
||||||
) : (
|
<th>Akce</th>
|
||||||
"—"
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
</td>
|
<tbody>
|
||||||
<td>
|
{(projects as Project[]).map((p) => (
|
||||||
<div className="admin-table-actions">
|
<tr key={p.id}>
|
||||||
|
<td className="admin-mono">
|
||||||
<Link
|
<Link
|
||||||
to={`/projects/${p.id}`}
|
to={`/projects/${p.id}`}
|
||||||
className="admin-btn-icon"
|
className="link-accent"
|
||||||
title="Upravit"
|
|
||||||
aria-label="Upravit"
|
|
||||||
>
|
>
|
||||||
<svg
|
{p.project_number}
|
||||||
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>
|
</Link>
|
||||||
{!p.order_id && hasPermission("projects.create") && (
|
</td>
|
||||||
<button
|
<td className="fw-500">{p.name || "—"}</td>
|
||||||
onClick={() => setDeleteTarget(p)}
|
<td>{p.customer_name || "—"}</td>
|
||||||
className="admin-btn-icon danger"
|
<td>{p.responsible_user_name || "—"}</td>
|
||||||
title="Smazat projekt"
|
<td>
|
||||||
disabled={deletingId === p.id}
|
<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 ? (
|
{p.order_number}
|
||||||
<div className="admin-spinner admin-spinner-sm" />
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
||||||
</td>
|
<td>
|
||||||
</tr>
|
<div className="admin-table-actions">
|
||||||
))}
|
<Link
|
||||||
</tbody>
|
to={`/projects/${p.id}`}
|
||||||
</table>
|
className="admin-btn-icon"
|
||||||
</div>
|
title="Upravit"
|
||||||
)}
|
aria-label="Upravit"
|
||||||
<Pagination pagination={pagination} onPageChange={setPage} />
|
>
|
||||||
</div>
|
<svg
|
||||||
</motion.div>
|
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
|
<ConfirmModal
|
||||||
isOpen={!!deleteTarget}
|
isOpen={!!deleteTarget}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
}}
|
}}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
title="Smazat projekt"
|
title="Smazat projekt"
|
||||||
message={
|
message={
|
||||||
<>
|
<>
|
||||||
Opravdu chcete smazat projekt {deleteTarget?.project_number}?
|
Opravdu chcete smazat projekt {deleteTarget?.project_number}?
|
||||||
<label
|
<label
|
||||||
className="admin-form-checkbox"
|
className="admin-form-checkbox"
|
||||||
style={{ marginTop: "1rem", display: "flex" }}
|
style={{ marginTop: "1rem", display: "flex" }}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={deleteFiles}
|
checked={deleteFiles}
|
||||||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Smazat i soubory na disku</span>
|
<span>Smazat i soubory na disku</span>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
type="danger"
|
type="danger"
|
||||||
loading={!!deletingId}
|
loading={!!deletingId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -10,6 +11,14 @@ import SortIcon from "../components/SortIcon";
|
|||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
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";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -131,7 +140,7 @@ interface CompanySettings {
|
|||||||
available_vat_rates: number[];
|
available_vat_rates: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyMeta(settings: CompanySettings | null): UploadMeta {
|
function emptyMeta(settings: CompanySettings | null | undefined): UploadMeta {
|
||||||
return {
|
return {
|
||||||
supplier_name: "",
|
supplier_name: "",
|
||||||
invoice_number: "",
|
invoice_number: "",
|
||||||
@@ -155,10 +164,16 @@ export default function ReceivedInvoices({
|
|||||||
const { sort, order, handleSort, activeSort } = useTableSort("created_at");
|
const { sort, order, handleSort, activeSort } = useTableSort("created_at");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const [invoices, setInvoices] = useState<ReceivedInvoice[]>([]);
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [stats, setStats] = useState<ReceivedStats | null>(null);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [statsLoading, setStatsLoading] = useState(true);
|
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 hasLoadedOnce = useRef(false);
|
||||||
const slideDirection = useRef(0);
|
const slideDirection = useRef(0);
|
||||||
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
@@ -166,18 +181,42 @@ export default function ReceivedInvoices({
|
|||||||
const prevMonth = useRef(statsMonth);
|
const prevMonth = useRef(statsMonth);
|
||||||
const prevYear = useRef(statsYear);
|
const prevYear = useRef(statsYear);
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const { data: supplierNames = [] } = useQuery(supplierListOptions());
|
||||||
const [editInvoice, setEditInvoice] = useState<EditInvoice | null>(null);
|
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
| CompanySettings
|
||||||
show: boolean;
|
| undefined;
|
||||||
invoice: ReceivedInvoice | null;
|
|
||||||
}>({ show: false, invoice: null });
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
|
|
||||||
const [supplierNames, setSupplierNames] = useState<string[]>([]);
|
// List query — auto-refetches when filters change
|
||||||
const [companySettings, setCompanySettings] =
|
const listQuery = useQuery(
|
||||||
useState<CompanySettings | null>(null);
|
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 [uploadFiles, setUploadFiles] = useState<File[]>([]);
|
||||||
const [uploadMeta, setUploadMeta] = useState<UploadMeta[]>([]);
|
const [uploadMeta, setUploadMeta] = useState<UploadMeta[]>([]);
|
||||||
@@ -201,57 +240,6 @@ export default function ReceivedInvoices({
|
|||||||
prevMonth.current = statsMonth;
|
prevMonth.current = statsMonth;
|
||||||
prevYear.current = statsYear;
|
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 =
|
const currencyOptions =
|
||||||
companySettings?.available_currencies || DEFAULT_CURRENCIES;
|
companySettings?.available_currencies || DEFAULT_CURRENCIES;
|
||||||
const vatRateOptions =
|
const vatRateOptions =
|
||||||
@@ -259,45 +247,6 @@ export default function ReceivedInvoices({
|
|||||||
const defaultCurrency = companySettings?.default_currency || "CZK";
|
const defaultCurrency = companySettings?.default_currency || "CZK";
|
||||||
const defaultVatRate = String(companySettings?.default_vat_rate ?? 21);
|
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 handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selected = Array.from(e.target.files || []);
|
const selected = Array.from(e.target.files || []);
|
||||||
if (selected.length === 0) {
|
if (selected.length === 0) {
|
||||||
@@ -395,8 +344,9 @@ export default function ReceivedInvoices({
|
|||||||
setUploadFiles([]);
|
setUploadFiles([]);
|
||||||
setUploadMeta([]);
|
setUploadMeta([]);
|
||||||
setUploadErrors({});
|
setUploadErrors({});
|
||||||
fetchList();
|
queryClient.invalidateQueries({
|
||||||
refreshStats();
|
queryKey: ["invoices", "received"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Chyba při nahrávání");
|
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");
|
alert.success(data.message || "Faktura byla aktualizována");
|
||||||
setEditOpen(false);
|
setEditOpen(false);
|
||||||
setEditInvoice(null);
|
setEditInvoice(null);
|
||||||
fetchList();
|
queryClient.invalidateQueries({
|
||||||
refreshStats();
|
queryKey: ["invoices", "received"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Chyba při ukládání");
|
alert.error(data.error || "Chyba při ukládání");
|
||||||
}
|
}
|
||||||
@@ -493,8 +444,9 @@ export default function ReceivedInvoices({
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success(data.message || "Faktura byla smazána");
|
alert.success(data.message || "Faktura byla smazána");
|
||||||
setDeleteConfirm({ show: false, invoice: null });
|
setDeleteConfirm({ show: false, invoice: null });
|
||||||
fetchList();
|
queryClient.invalidateQueries({
|
||||||
refreshStats();
|
queryKey: ["invoices", "received"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Chyba při mazání");
|
alert.error(data.error || "Chyba při mazání");
|
||||||
}
|
}
|
||||||
@@ -538,8 +490,9 @@ export default function ReceivedInvoices({
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert.success("Faktura označena jako uhrazená");
|
alert.success("Faktura označena jako uhrazená");
|
||||||
fetchList();
|
queryClient.invalidateQueries({
|
||||||
refreshStats();
|
queryKey: ["invoices", "received"],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert.error(data.error || "Nepodařilo se změnit stav");
|
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 monthLabel = `${MONTH_NAMES[statsMonth - 1]}`;
|
||||||
|
|
||||||
const renderKpi = () => {
|
const renderKpi = () => {
|
||||||
if (!hasLoadedOnce.current && statsLoading) {
|
if (statsQuery.isPending && !hasLoadedOnce.current) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-kpi-grid admin-kpi-4 mb-6">
|
<Skeleton
|
||||||
{[0, 1, 2, 3].map((i) => (
|
name="received-invoices-kpi"
|
||||||
<div key={i} className="admin-stat-card">
|
loading={statsQuery.isPending && !hasLoadedOnce.current}
|
||||||
<div
|
fixture={<ReceivedInvoicesFixture />}
|
||||||
className="admin-skeleton-line"
|
>
|
||||||
style={{ width: "60%", height: "11px", marginBottom: "0.5rem" }}
|
<div />
|
||||||
/>
|
</Skeleton>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
@@ -680,18 +622,16 @@ export default function ReceivedInvoices({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && (
|
{showListSkeleton && (
|
||||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
<Skeleton
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
name="received-invoices-list"
|
||||||
<div key={i} className="admin-skeleton-row">
|
loading={showListSkeleton}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
fixture={<ReceivedInvoicesFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
<div />
|
||||||
</div>
|
</Skeleton>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{!loading && invoices.length === 0 && (
|
{!showListSkeleton && invoices.length === 0 && (
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
<div className="admin-empty-icon">
|
<div className="admin-empty-icon">
|
||||||
<svg
|
<svg
|
||||||
@@ -723,7 +663,7 @@ export default function ReceivedInvoices({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && invoices.length > 0 && (
|
{!showListSkeleton && invoices.length > 0 && (
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<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 { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
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 { motion, AnimatePresence } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import CompanySettings from "./CompanySettings";
|
import CompanySettings from "./CompanySettings";
|
||||||
|
import {
|
||||||
|
companySettingsOptions,
|
||||||
|
systemInfoOptions,
|
||||||
|
require2FAOptions,
|
||||||
|
} from "../lib/queries/settings";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import SettingsFixture from "../fixtures/SettingsFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface SystemSettingsData {
|
interface SystemSettingsData {
|
||||||
@@ -70,20 +78,108 @@ interface RoleForm {
|
|||||||
permissions: string[];
|
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() {
|
export default function Settings() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const queryClient = useQueryClient();
|
||||||
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 [require2FA, setRequire2FA] = useState(false);
|
const canManage = hasPermission("settings.manage");
|
||||||
const [require2FALoading, setRequire2FALoading] = useState(true);
|
|
||||||
|
// ── 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 [require2FASaving, setRequire2FASaving] = useState(false);
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
@@ -102,194 +198,56 @@ export default function Settings() {
|
|||||||
}>({ show: false, role: null });
|
}>({ show: false, role: null });
|
||||||
const [deleting, setDeleting] = useState(false);
|
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 [sysSettingsSaving, setSysSettingsSaving] = useState(false);
|
||||||
const [systemInfo, setSystemInfo] = useState<Record<string, any> | null>(
|
const [sysForm, setSysForm] = useState(DEFAULT_SYS_FORM);
|
||||||
null,
|
const [sysFormInitialized, setSysFormInitialized] = useState(false);
|
||||||
);
|
|
||||||
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 canManage = hasPermission("settings.manage");
|
// ── Populate sysForm from query data ──
|
||||||
|
useEffect(() => {
|
||||||
if (!canManage) {
|
if (!sysSettingsData || sysFormInitialized) return;
|
||||||
return <Navigate to="/" replace />;
|
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);
|
useModalLock(showModal);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
// ── Early return after all hooks ──
|
||||||
if (!canManage) {
|
if (!canManage) {
|
||||||
setLoading(false);
|
return <Navigate to="/" replace />;
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleSaveSystemSettings = async () => {
|
const handleSaveSystemSettings = async () => {
|
||||||
setSysSettingsSaving(true);
|
setSysSettingsSaving(true);
|
||||||
@@ -302,7 +260,7 @@ export default function Settings() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert.success(result.message || "Systémová nastavení byla uložena");
|
alert.success(result.message || "Systémová nastavení byla uložena");
|
||||||
fetchSystemSettings();
|
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||||
}
|
}
|
||||||
@@ -323,8 +281,8 @@ export default function Settings() {
|
|||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setRequire2FA(!require2FA);
|
|
||||||
alert.success(result.message || "2FA nastavení uloženo");
|
alert.success(result.message || "2FA nastavení uloženo");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||||
}
|
}
|
||||||
@@ -339,7 +297,7 @@ export default function Settings() {
|
|||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.normalize("NFD")
|
.normalize("NFD")
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
.replace(/[̀-ͯ]/g, "")
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
.replace(/^-+|-+$/g, "");
|
.replace(/^-+|-+$/g, "");
|
||||||
};
|
};
|
||||||
@@ -443,7 +401,7 @@ export default function Settings() {
|
|||||||
result.message ||
|
result.message ||
|
||||||
(editingRole ? "Role byla aktualizována" : "Role byla vytvořena"),
|
(editingRole ? "Role byla aktualizována" : "Role byla vytvořena"),
|
||||||
);
|
);
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se uložit roli");
|
alert.error(result.error || "Nepodařilo se uložit roli");
|
||||||
}
|
}
|
||||||
@@ -471,7 +429,7 @@ export default function Settings() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, role: null });
|
setDeleteConfirm({ show: false, role: null });
|
||||||
alert.success(result.message || "Role byla smazána");
|
alert.success(result.message || "Role byla smazána");
|
||||||
fetchData();
|
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error || "Nepodařilo se smazat roli");
|
alert.error(result.error || "Nepodařilo se smazat roli");
|
||||||
}
|
}
|
||||||
@@ -482,39 +440,15 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (rolesLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<Skeleton
|
||||||
<div
|
name="settings"
|
||||||
className="admin-skeleton-row"
|
loading={rolesLoading}
|
||||||
style={{ justifyContent: "space-between" }}
|
fixture={<SettingsFixture />}
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
className="admin-skeleton-line h-8"
|
|
||||||
style={{ width: "200px", marginBottom: "0.5rem" }}
|
|
||||||
/>
|
|
||||||
<div className="admin-skeleton-line" style={{ width: "140px" }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,10 +457,13 @@ export default function Settings() {
|
|||||||
const get2FADescription = (): React.ReactNode => {
|
const get2FADescription = (): React.ReactNode => {
|
||||||
if (require2FALoading) {
|
if (require2FALoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-skeleton-line"
|
name="settings-2fa"
|
||||||
style={{ width: "200px", height: "12px" }}
|
loading={require2FALoading}
|
||||||
/>
|
fixture={<SettingsFixture />}
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (require2FA)
|
if (require2FA)
|
||||||
@@ -783,7 +720,7 @@ export default function Settings() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{roles.map((role) => (
|
{roles.map((role: Role) => (
|
||||||
<tr key={role.id}>
|
<tr key={role.id}>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
@@ -804,7 +741,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ color: "var(--text-secondary)" }}>
|
<td style={{ color: "var(--text-secondary)" }}>
|
||||||
{role.description || "\u2014"}
|
{role.description || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="admin-badge admin-badge-info">
|
<span className="admin-badge admin-badge-info">
|
||||||
@@ -815,7 +752,11 @@ export default function Settings() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="admin-badge admin-badge-secondary">
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -845,20 +786,26 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
className="admin-btn-icon danger"
|
className="admin-btn-icon danger"
|
||||||
title={
|
title={
|
||||||
users.filter((u) => u.role_id === role.id)
|
users.filter(
|
||||||
.length > 0
|
(u: { role_id: number }) =>
|
||||||
|
u.role_id === role.id,
|
||||||
|
).length > 0
|
||||||
? "Nelze smazat roli s přiřazenými uživateli"
|
? "Nelze smazat roli s přiřazenými uživateli"
|
||||||
: "Smazat"
|
: "Smazat"
|
||||||
}
|
}
|
||||||
aria-label={
|
aria-label={
|
||||||
users.filter((u) => u.role_id === role.id)
|
users.filter(
|
||||||
.length > 0
|
(u: { role_id: number }) =>
|
||||||
|
u.role_id === role.id,
|
||||||
|
).length > 0
|
||||||
? "Nelze smazat roli s přiřazenými uživateli"
|
? "Nelze smazat roli s přiřazenými uživateli"
|
||||||
: "Smazat"
|
: "Smazat"
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
users.filter((u) => u.role_id === role.id)
|
users.filter(
|
||||||
.length > 0
|
(u: { role_id: number }) =>
|
||||||
|
u.role_id === role.id,
|
||||||
|
).length > 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -888,21 +835,14 @@ export default function Settings() {
|
|||||||
{/* System Settings Tab */}
|
{/* System Settings Tab */}
|
||||||
{activeTab === "system" && canManage && (
|
{activeTab === "system" && canManage && (
|
||||||
<>
|
<>
|
||||||
{sysSettingsLoading ? (
|
{sysSettingsLoading && !sysFormInitialized ? (
|
||||||
<div
|
<Skeleton
|
||||||
className="admin-skeleton"
|
name="settings-system"
|
||||||
style={{ padding: 0, gap: "1.5rem" }}
|
loading={sysSettingsLoading && !sysFormInitialized}
|
||||||
|
fixture={<SettingsFixture />}
|
||||||
>
|
>
|
||||||
{[0, 1, 2].map((i) => (
|
<div />
|
||||||
<div key={i} className="admin-card">
|
</Skeleton>
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Section 1: Docházka */}
|
{/* Section 1: Docházka */}
|
||||||
@@ -1374,12 +1314,33 @@ export default function Settings() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["Verze", systemInfo.app_version],
|
[
|
||||||
["Node.js", systemInfo.node_version],
|
"Verze",
|
||||||
["Platforma", systemInfo.platform],
|
(systemInfo as Record<string, unknown>)
|
||||||
["Uptime", systemInfo.uptime],
|
.app_version,
|
||||||
["Prostředí", systemInfo.environment],
|
],
|
||||||
["Časová zóna", systemInfo.timezone],
|
[
|
||||||
|
"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][]
|
] as [string, string][]
|
||||||
).map(([label, val]) => (
|
).map(([label, val]) => (
|
||||||
<tr key={label}>
|
<tr key={label}>
|
||||||
@@ -1415,14 +1376,22 @@ export default function Settings() {
|
|||||||
</tr>
|
</tr>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["Proces (RSS)", systemInfo.memory?.rss],
|
[
|
||||||
|
"Proces (RSS)",
|
||||||
|
(
|
||||||
|
systemInfo as Record<
|
||||||
|
string,
|
||||||
|
Record<string, unknown>
|
||||||
|
>
|
||||||
|
).memory?.rss as string,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"Heap",
|
"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",
|
"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][]
|
] as [string, string][]
|
||||||
).map(([label, val]) => (
|
).map(([label, val]) => (
|
||||||
@@ -1464,9 +1433,14 @@ export default function Settings() {
|
|||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "4px 0" }}>
|
<td style={{ padding: "4px 0" }}>
|
||||||
<span
|
<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"
|
? "Připojeno"
|
||||||
: "Chyba"}
|
: "Chyba"}
|
||||||
</span>
|
</span>
|
||||||
@@ -1482,7 +1456,14 @@ export default function Settings() {
|
|||||||
Migrace
|
Migrace
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "4px 0" }}>
|
<td style={{ padding: "4px 0" }}>
|
||||||
{systemInfo.database?.migrations_applied}
|
{
|
||||||
|
(
|
||||||
|
systemInfo as Record<
|
||||||
|
string,
|
||||||
|
Record<string, unknown>
|
||||||
|
>
|
||||||
|
).database?.migrations_applied as string
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1502,9 +1483,33 @@ export default function Settings() {
|
|||||||
</tr>
|
</tr>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["Projekty", systemInfo.nas?.projects],
|
[
|
||||||
["Finance", systemInfo.nas?.financials],
|
"Projekty",
|
||||||
["Nabídky", systemInfo.nas?.offers],
|
(
|
||||||
|
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>][]
|
] as [string, Record<string, any>][]
|
||||||
).map(([label, info]) => (
|
).map(([label, info]) => (
|
||||||
<tr key={label}>
|
<tr key={label}>
|
||||||
@@ -1542,9 +1547,30 @@ export default function Settings() {
|
|||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="admin-skeleton-line"
|
style={{
|
||||||
style={{ width: "60%", height: 14 }}
|
padding: "1rem",
|
||||||
/>
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "0.75rem",
|
||||||
|
background: "#e0e0e0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
width: "60%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "0.75rem",
|
||||||
|
background: "#e0e0e0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
width: "40%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.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 { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -12,6 +13,9 @@ import Forbidden from "../components/Forbidden";
|
|||||||
import { formatDate } from "../utils/attendanceHelpers";
|
import { formatDate } from "../utils/attendanceHelpers";
|
||||||
import { formatKm } from "../utils/formatters";
|
import { formatKm } from "../utils/formatters";
|
||||||
import apiFetch from "../utils/api";
|
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";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface Vehicle {
|
interface Vehicle {
|
||||||
@@ -49,10 +53,20 @@ interface TripForm {
|
|||||||
export default function Trips() {
|
export default function Trips() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
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 [submitting, setSubmitting] = useState(false);
|
||||||
const [trips, setTrips] = useState<Trip[]>([]);
|
|
||||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingTrip, setEditingTrip] = useState<Trip | null>(null);
|
const [editingTrip, setEditingTrip] = useState<Trip | null>(null);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||||
@@ -72,37 +86,6 @@ export default function Trips() {
|
|||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [, setLastKm] = useState(0);
|
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);
|
useModalLock(showModal);
|
||||||
|
|
||||||
if (!hasPermission("trips.record")) return <Forbidden />;
|
if (!hasPermission("trips.record")) return <Forbidden />;
|
||||||
@@ -208,8 +191,7 @@ export default function Trips() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
await fetchData(false);
|
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -230,7 +212,7 @@ export default function Trips() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await fetchData(false);
|
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -248,65 +230,11 @@ export default function Trips() {
|
|||||||
return end > start ? end - start : 0;
|
return end > start ? end - start : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (tripsLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Skeleton name="trips" loading={tripsLoading} fixture={<TripsFixture />}>
|
||||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
<div />
|
||||||
<div
|
</Skeleton>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
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 AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
@@ -12,6 +19,8 @@ import useModalLock from "../hooks/useModalLock";
|
|||||||
import { formatDate } from "../utils/attendanceHelpers";
|
import { formatDate } from "../utils/attendanceHelpers";
|
||||||
import { formatKm } from "../utils/formatters";
|
import { formatKm } from "../utils/formatters";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { Skeleton } from "boneyard-js/react";
|
||||||
|
import TripsAdminFixture from "../fixtures/TripsAdminFixture";
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface Vehicle {
|
interface Vehicle {
|
||||||
@@ -88,8 +97,7 @@ function mapTrip(bt: BackendTrip): Trip {
|
|||||||
export default function TripsAdmin() {
|
export default function TripsAdmin() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [companyName, setCompanyName] = useState("");
|
|
||||||
const [filterMonth, setFilterMonth] = useState(() =>
|
const [filterMonth, setFilterMonth] = useState(() =>
|
||||||
String(new Date().getMonth() + 1),
|
String(new Date().getMonth() + 1),
|
||||||
);
|
);
|
||||||
@@ -98,9 +106,6 @@ export default function TripsAdmin() {
|
|||||||
);
|
);
|
||||||
const [filterVehicleId, setFilterVehicleId] = useState("");
|
const [filterVehicleId, setFilterVehicleId] = useState("");
|
||||||
const [filterUserId, setFilterUserId] = 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 printRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
@@ -121,56 +126,27 @@ export default function TripsAdmin() {
|
|||||||
trip: Trip | null;
|
trip: Trip | null;
|
||||||
}>({ show: false, trip: null });
|
}>({ show: false, trip: null });
|
||||||
|
|
||||||
// Fetch vehicles and users once on mount
|
const { data: vehiclesData = [] } = useQuery(tripVehiclesOptions());
|
||||||
useEffect(() => {
|
const vehicles = vehiclesData as Vehicle[];
|
||||||
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 fetchData = useCallback(
|
const { data: tripUsersData = [] } = useQuery(tripUsersOptions());
|
||||||
async (showLoading = true) => {
|
const tripUsers = tripUsersData as UserShort[];
|
||||||
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 response = await apiFetch(url);
|
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||||
const result = await response.json();
|
const companyName =
|
||||||
if (result.success) {
|
((companySettings as Record<string, unknown> | undefined)
|
||||||
const mapped = (result.data as BackendTrip[]).map(mapTrip);
|
?.company_name as string) ?? "";
|
||||||
setTrips(mapped);
|
|
||||||
}
|
const { data: tripsData, isPending } = useQuery(
|
||||||
} catch {
|
tripListOptions({
|
||||||
alert.error("Nepodařilo se načíst data");
|
month: Number(filterMonth) || undefined,
|
||||||
} finally {
|
year: Number(filterYear) || undefined,
|
||||||
if (showLoading) setLoading(false);
|
vehicleId: filterVehicleId ? Number(filterVehicleId) : undefined,
|
||||||
}
|
userId: filterUserId ? Number(filterUserId) : undefined,
|
||||||
},
|
perPage: 100,
|
||||||
[filterMonth, filterYear, filterVehicleId, filterUserId, alert],
|
}),
|
||||||
);
|
);
|
||||||
|
const trips = ((tripsData ?? []) as BackendTrip[]).map(mapTrip);
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
useModalLock(showEditModal);
|
useModalLock(showEditModal);
|
||||||
|
|
||||||
@@ -211,8 +187,7 @@ export default function TripsAdmin() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
await fetchData(false);
|
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -237,7 +212,7 @@ export default function TripsAdmin() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setDeleteConfirm({ show: false, trip: null });
|
setDeleteConfirm({ show: false, trip: null });
|
||||||
await fetchData(false);
|
queryClient.invalidateQueries({ queryKey: ["trips"] });
|
||||||
alert.success(result.message);
|
alert.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
alert.error(result.error);
|
alert.error(result.error);
|
||||||
@@ -259,7 +234,7 @@ export default function TripsAdmin() {
|
|||||||
};
|
};
|
||||||
const getSelectedUserName = () => {
|
const getSelectedUserName = () => {
|
||||||
if (!filterUserId) return null;
|
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;
|
return u?.name || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -468,7 +443,7 @@ export default function TripsAdmin() {
|
|||||||
className="admin-form-select"
|
className="admin-form-select"
|
||||||
>
|
>
|
||||||
<option value="">Všichni řidiči</option>
|
<option value="">Všichni řidiči</option>
|
||||||
{users.map((u) => (
|
{tripUsers.map((u) => (
|
||||||
<option key={u.id} value={u.id}>
|
<option key={u.id} value={u.id}>
|
||||||
{u.name}
|
{u.name}
|
||||||
</option>
|
</option>
|
||||||
@@ -565,119 +540,117 @@ export default function TripsAdmin() {
|
|||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{loading && (
|
<Skeleton
|
||||||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
name="trips-admin"
|
||||||
{[0, 1, 2, 3, 4].map((i) => (
|
loading={isPending}
|
||||||
<div key={i} className="admin-skeleton-row">
|
fixture={<TripsAdminFixture />}
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
>
|
||||||
<div className="admin-skeleton-line w-1/3" />
|
<>
|
||||||
<div className="admin-skeleton-line w-1/4" />
|
{trips.length === 0 && (
|
||||||
|
<div className="admin-empty-state">
|
||||||
|
<p>Žádné záznamy jízd pro vybrané období.</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
{trips.length > 0 && (
|
||||||
)}
|
<div className="admin-table-responsive">
|
||||||
{!loading && trips.length === 0 && (
|
<table className="admin-table">
|
||||||
<div className="admin-empty-state">
|
<thead>
|
||||||
<p>Žádné záznamy jízd pro vybrané období.</p>
|
<tr>
|
||||||
</div>
|
<th>Datum</th>
|
||||||
)}
|
<th>Řidič</th>
|
||||||
{!loading && trips.length > 0 && (
|
<th>Vozidlo</th>
|
||||||
<div className="admin-table-responsive">
|
<th>Trasa</th>
|
||||||
<table className="admin-table">
|
<th>Stav km</th>
|
||||||
<thead>
|
<th>Vzdálenost</th>
|
||||||
<tr>
|
<th>Typ</th>
|
||||||
<th>Datum</th>
|
<th>Akce</th>
|
||||||
<th>Řidič</th>
|
</tr>
|
||||||
<th>Vozidlo</th>
|
</thead>
|
||||||
<th>Trasa</th>
|
<tbody>
|
||||||
<th>Stav km</th>
|
{trips.map((trip) => (
|
||||||
<th>Vzdálenost</th>
|
<tr key={trip.id}>
|
||||||
<th>Typ</th>
|
<td className="admin-mono">
|
||||||
<th>Akce</th>
|
{formatDate(trip.trip_date)}
|
||||||
</tr>
|
</td>
|
||||||
</thead>
|
<td>{trip.driver_name}</td>
|
||||||
<tbody>
|
<td>
|
||||||
{trips.map((trip) => (
|
<span className="admin-badge">{trip.spz}</span>
|
||||||
<tr key={trip.id}>
|
</td>
|
||||||
<td className="admin-mono">
|
<td>
|
||||||
{formatDate(trip.trip_date)}
|
<span style={{ whiteSpace: "nowrap" }}>
|
||||||
</td>
|
{trip.route_from} → {trip.route_to}
|
||||||
<td>{trip.driver_name}</td>
|
</span>
|
||||||
<td>
|
</td>
|
||||||
<span className="admin-badge">{trip.spz}</span>
|
<td className="admin-mono">
|
||||||
</td>
|
<span style={{ whiteSpace: "nowrap" }}>
|
||||||
<td>
|
{formatKm(trip.start_km)} -{" "}
|
||||||
<span style={{ whiteSpace: "nowrap" }}>
|
{formatKm(trip.end_km)}
|
||||||
{trip.route_from} → {trip.route_to}
|
</span>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="admin-mono">
|
||||||
<td className="admin-mono">
|
<strong>{formatKm(trip.distance)} km</strong>
|
||||||
<span style={{ whiteSpace: "nowrap" }}>
|
</td>
|
||||||
{formatKm(trip.start_km)} - {formatKm(trip.end_km)}
|
<td>
|
||||||
</span>
|
<span
|
||||||
</td>
|
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
{trip.is_business ? "Služební" : "Soukromá"}
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
</span>
|
||||||
</svg>
|
</td>
|
||||||
</button>
|
<td>
|
||||||
<button
|
<div className="admin-table-actions">
|
||||||
onClick={() =>
|
<button
|
||||||
setDeleteConfirm({ show: true, trip })
|
onClick={() => openEditModal(trip)}
|
||||||
}
|
className="admin-btn-icon"
|
||||||
className="admin-btn-icon danger"
|
title="Upravit"
|
||||||
title="Smazat"
|
aria-label="Upravit"
|
||||||
aria-label="Smazat"
|
>
|
||||||
>
|
<svg
|
||||||
<svg
|
width="18"
|
||||||
width="18"
|
height="18"
|
||||||
height="18"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="2"
|
||||||
strokeWidth="2"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="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" />
|
||||||
<polyline points="3 6 5 6 21 6" />
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
<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>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
<button
|
||||||
</div>
|
onClick={() =>
|
||||||
</td>
|
setDeleteConfirm({ show: true, trip })
|
||||||
</tr>
|
}
|
||||||
))}
|
className="admin-btn-icon danger"
|
||||||
</tbody>
|
title="Smazat"
|
||||||
</table>
|
aria-label="Smazat"
|
||||||
</div>
|
>
|
||||||
)}
|
<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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user