diff --git a/package-lock.json b/package-lock.json index a863c99..0231da6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "nodemailer": "^8.0.2", "otpauth": "^9.5.0", "prisma": "^6.19.2", + "puppeteer": "^24.40.0", "qrcode": "^1.5.4", "react": "^18.3.1", "react-datepicker": "^9.1.0", @@ -59,6 +60,29 @@ "vitest": "^4.1.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -1108,6 +1132,27 @@ "@prisma/debug": "6.19.2" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1391,6 +1436,12 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1490,7 +1541,7 @@ "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -1567,6 +1618,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -1724,6 +1785,15 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -1781,6 +1851,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1798,6 +1874,18 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1843,6 +1931,97 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz", + "integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1863,6 +2042,15 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -1908,6 +2096,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1985,6 +2182,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2049,6 +2255,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -2062,7 +2290,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2213,6 +2440,32 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2220,6 +2473,15 @@ "dev": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -2234,7 +2496,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2272,6 +2533,20 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2316,6 +2591,12 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -2403,6 +2684,33 @@ "node": ">=14" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2505,7 +2813,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2517,6 +2824,49 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2527,6 +2877,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2551,6 +2910,15 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2567,6 +2935,26 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -2607,6 +2995,12 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-json-stringify": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", @@ -2721,6 +3115,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2918,6 +3321,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -2931,6 +3349,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -3056,6 +3488,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3076,12 +3534,37 @@ ], "license": "BSD-3-Clause" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -3091,6 +3574,12 @@ "node": ">= 10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3115,6 +3604,24 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -3481,6 +3988,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3664,6 +4177,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", @@ -3704,6 +4223,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -3785,7 +4313,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3839,12 +4366,74 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parchment": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", "license": "BSD-3-Clause" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3889,6 +4478,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -3899,7 +4494,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -4051,6 +4645,98 @@ ], "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4382,6 +5068,15 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -4670,6 +5365,44 @@ "dev": true, "license": "ISC" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -4679,6 +5412,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4721,6 +5464,17 @@ "dev": true, "license": "MIT" }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4844,6 +5598,78 @@ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/thread-stream": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", @@ -4970,6 +5796,12 @@ "fsevents": "~2.3.3" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -4988,7 +5820,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vite": { @@ -5152,6 +5984,12 @@ } } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", @@ -5179,7 +6017,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5197,14 +6034,33 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -5214,7 +6070,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -5233,12 +6088,21 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 9675de9..22c9a52 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "nodemailer": "^8.0.2", "otpauth": "^9.5.0", "prisma": "^6.19.2", + "puppeteer": "^24.40.0", "qrcode": "^1.5.4", "react": "^18.3.1", "react-datepicker": "^9.1.0", diff --git a/prisma/migrations/20260326_add_invoice_language/migration.sql b/prisma/migrations/20260326_add_invoice_language/migration.sql new file mode 100644 index 0000000..e6fbc0d --- /dev/null +++ b/prisma/migrations/20260326_add_invoice_language/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `invoices` ADD COLUMN `language` VARCHAR(5) DEFAULT 'cs' AFTER `billing_text`; diff --git a/prisma/migrations/20260326_add_received_invoice_file_path/migration.sql b/prisma/migrations/20260326_add_received_invoice_file_path/migration.sql new file mode 100644 index 0000000..4480261 --- /dev/null +++ b/prisma/migrations/20260326_add_received_invoice_file_path/migration.sql @@ -0,0 +1,2 @@ +-- Add file_path column for NAS storage of received invoice files +ALTER TABLE `received_invoices` ADD COLUMN `file_path` VARCHAR(500) NULL AFTER `file_data`; diff --git a/prisma/migrations/20260326_drop_received_invoice_blob_columns/migration.sql b/prisma/migrations/20260326_drop_received_invoice_blob_columns/migration.sql new file mode 100644 index 0000000..3fdccd2 --- /dev/null +++ b/prisma/migrations/20260326_drop_received_invoice_blob_columns/migration.sql @@ -0,0 +1,4 @@ +-- Remove file_data (BLOB) and file_path columns — files are now stored on NAS +-- Path is derived from year, month, and file_name +ALTER TABLE `received_invoices` DROP COLUMN `file_data`; +ALTER TABLE `received_invoices` DROP COLUMN `file_path`; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..9bee74d --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 37f5ee3..e5f5b32 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -167,6 +167,7 @@ model invoices { paid_date DateTime? @db.Date issued_by String? @db.VarChar(255) billing_text String? @db.VarChar(500) + language String? @default("cs") @db.VarChar(5) notes String? @db.Text internal_notes String? @db.Text created_at DateTime? @default(now()) @db.DateTime(0) @@ -410,7 +411,6 @@ model received_invoices { due_date DateTime? @db.Date paid_date DateTime? @db.Date status received_invoices_status @default(unpaid) - file_data Bytes? @db.MediumBlob file_name String? @db.VarChar(255) file_mime String? @db.VarChar(100) file_size Int? @db.UnsignedInt diff --git a/scripts/migrate-received-invoices-to-nas.ts b/scripts/migrate-received-invoices-to-nas.ts new file mode 100644 index 0000000..0d02c98 --- /dev/null +++ b/scripts/migrate-received-invoices-to-nas.ts @@ -0,0 +1,75 @@ +/** + * Migrate received invoice files from DB BLOB to NAS storage. + * + * Usage: NAS_FINANCIALS_PATH=/mnt/nas/financials npx tsx scripts/migrate-received-invoices-to-nas.ts + */ + +import "../src/config/env"; +import { PrismaClient } from "@prisma/client"; +import { nasFinancialsManager } from "../src/services/nas-financials-manager"; + +const prisma = new PrismaClient(); + +async function main() { + if (!nasFinancialsManager.isConfigured()) { + console.error( + "NAS_FINANCIALS_PATH is not configured or path does not exist.", + ); + process.exit(1); + } + + const records = await prisma.received_invoices.findMany({ + where: { file_data: { not: null }, file_path: null }, + select: { + id: true, + file_data: true, + file_name: true, + month: true, + year: true, + }, + }); + + console.log(`Found ${records.length} invoices to migrate.`); + + let migrated = 0; + let failed = 0; + + for (const rec of records) { + if (!rec.file_data) continue; + + const fileName = rec.file_name || `invoice-${rec.id}.pdf`; + const result = nasFinancialsManager.saveReceivedInvoice( + fileName, + rec.year, + rec.month, + Buffer.from(rec.file_data), + ); + + if ("error" in result) { + console.error(` FAIL id=${rec.id}: ${result.error}`); + failed++; + continue; + } + + await prisma.received_invoices.update({ + where: { id: rec.id }, + data: { file_path: result.filePath, file_data: null }, + }); + + migrated++; + if (migrated % 50 === 0) { + console.log(` Progress: ${migrated}/${records.length}...`); + } + } + + console.log( + `Done. Migrated: ${migrated}, Failed: ${failed}, Total: ${records.length}`, + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/src/admin/admin.css b/src/admin/admin.css index c8ca558..2822fee 100644 --- a/src/admin/admin.css +++ b/src/admin/admin.css @@ -17,7 +17,9 @@ html { overflow-x: hidden; } -html, body, #root { +html, +body, +#root { min-height: 100%; min-height: 100dvh; max-width: 100vw; @@ -33,14 +35,19 @@ body { overscroll-behavior-x: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - transition: background-color 0.3s ease, color 0.3s ease; + transition: + background-color 0.3s ease, + color 0.3s ease; } .admin-sidebar, .admin-header, .admin-card, .admin-modal { - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; + transition: + background-color 0.3s ease, + color 0.3s ease, + border-color 0.3s ease; } #root { @@ -48,16 +55,27 @@ body { touch-action: pan-y pinch-zoom; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-family: var(--font-heading); font-weight: 700; line-height: 1.2; color: var(--text-primary); } -h1 { font-size: clamp(2.5rem, 5vw, 4rem); } -h2 { font-size: clamp(2rem, 4vw, 3rem); } -h3 { font-size: clamp(1.25rem, 2vw, 1.5rem); } +h1 { + font-size: clamp(2.5rem, 5vw, 4rem); +} +h2 { + font-size: clamp(2rem, 4vw, 3rem); +} +h3 { + font-size: clamp(1.25rem, 2vw, 1.5rem); +} p { color: var(--text-secondary); @@ -114,15 +132,15 @@ img { --space-12: 3rem; /* Shared colors */ - --accent-color: #D63031; - --accent-hover: #B52626; + --accent-color: #d63031; + --accent-hover: #b52626; --success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #3b82f6; --error: var(--danger); --muted: #9ca3af; - --gradient: #D63031; + --gradient: #d63031; --gradient-subtle: rgba(214, 48, 49, 0.9); /* Shared layout */ @@ -131,9 +149,9 @@ img { --border-radius-lg: 16px; --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); - --font-heading: 'Urbanist', sans-serif; - --font-body: 'Plus Jakarta Sans', sans-serif; - --font-mono: 'DM Mono', 'Menlo', monospace; + --font-heading: "Urbanist", sans-serif; + --font-body: "Plus Jakarta Sans", sans-serif; + --font-mono: "DM Mono", "Menlo", monospace; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); --safe-left: env(safe-area-inset-left, 0px); @@ -192,14 +210,14 @@ img { --accent-color: #c73030; --accent-hover: #b52828; --muted: #6b7280; - --bg-primary: #F5F4F2; + --bg-primary: #f5f4f2; --bg-secondary: #ffffff; - --bg-tertiary: #EEECEA; - --text-primary: #1A1A1A; + --bg-tertiary: #eeecea; + --text-primary: #1a1a1a; --text-secondary: #555555; --text-muted: #717180; --text-tertiary: #8a8a96; - --border-color: rgba(0, 0, 0, 0.10); + --border-color: rgba(0, 0, 0, 0.1); --border-color-hover: rgba(0, 0, 0, 0.18); --glass-bg: #ffffff; --glass-bg-solid: #ffffff; @@ -210,16 +228,16 @@ img { --input-bg: #ffffff; --glow-color: rgba(222, 58, 58, 0.08); --accent-light: rgba(222, 58, 58, 0.08); - --accent-soft: #FFF0F0; + --accent-soft: #fff0f0; --accent-glow: rgba(222, 58, 58, 0.15); --success-light: rgba(34, 197, 94, 0.1); - --success-soft: #E8FBF7; + --success-soft: #e8fbf7; --warning-light: rgba(245, 158, 11, 0.1); - --warning-soft: #FEF9EC; + --warning-soft: #fef9ec; --danger-light: rgba(239, 68, 68, 0.1); - --danger-soft: #FEF2F2; + --danger-soft: #fef2f2; --info-light: rgba(59, 130, 246, 0.1); - --info-soft: #EBF3FD; + --info-soft: #ebf3fd; --muted-light: rgba(107, 114, 128, 0.12); --calendar-icon-filter: none; --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23555555' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); @@ -234,7 +252,9 @@ img { /* Light mode - jemnejsi stiny */ [data-theme="light"] .admin-toast { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.08), + 0 1px 3px rgba(0, 0, 0, 0.06); } [data-theme="light"] .react-datepicker { @@ -302,28 +322,67 @@ img { } /* Layout utilities */ -.flex-1 { flex: 1; } -.flex-row { display: flex; align-items: center; } -.flex-row-gap { display: flex; align-items: center; gap: var(--space-3); } -.flex-between { display: flex; align-items: center; justify-content: space-between; } +.flex-1 { + flex: 1; +} +.flex-row { + display: flex; + align-items: center; +} +.flex-row-gap { + display: flex; + align-items: center; + gap: var(--space-3); +} +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} /* Spacing utilities */ -.mb-2 { margin-bottom: var(--space-2); } -.mb-4 { margin-bottom: var(--space-4); } -.mb-6 { margin-bottom: var(--space-6); } -.mt-2 { margin-top: var(--space-2); } -.mt-6 { margin-top: var(--space-6); } -.gap-2 { gap: var(--space-2); } -.gap-4 { gap: var(--space-4); } -.gap-5 { gap: var(--space-5); } +.mb-2 { + margin-bottom: var(--space-2); +} +.mb-4 { + margin-bottom: var(--space-4); +} +.mb-6 { + margin-bottom: var(--space-6); +} +.mt-2 { + margin-top: var(--space-2); +} +.mt-6 { + margin-top: var(--space-6); +} +.gap-2 { + gap: var(--space-2); +} +.gap-4 { + gap: var(--space-4); +} +.gap-5 { + gap: var(--space-5); +} /* Typography utilities */ -.fw-500 { font-weight: 500; } -.text-right { text-align: right; } -.text-center { text-align: center; } +.fw-500 { + font-weight: 500; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} /* Spinner variant */ -.admin-spinner-sm { width: 16px; height: 16px; border-width: 2px; } +.admin-spinner-sm { + width: 16px; + height: 16px; + border-width: 2px; +} /* ============================================================================ Forms @@ -357,7 +416,9 @@ img { font-size: 13px; font-family: inherit; outline: none; - transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: + border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-sizing: border-box; min-height: 36px; } @@ -420,7 +481,9 @@ img { font-size: 13px; font-family: inherit; outline: none; - transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: + border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); min-height: 36px; box-sizing: border-box; cursor: pointer; @@ -452,7 +515,9 @@ img { font-size: 13px; font-family: inherit; outline: none; - transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: + border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); resize: vertical; box-sizing: border-box; min-height: 80px; @@ -481,7 +546,7 @@ img { } .admin-form-checkbox input + span::before { - content: ''; + content: ""; display: inline-flex; align-items: center; justify-content: center; @@ -492,7 +557,10 @@ img { border: 1px solid var(--border-color); border-radius: 4px; vertical-align: middle; - transition: border-color var(--transition), box-shadow var(--transition), background var(--transition); + transition: + border-color var(--transition), + box-shadow var(--transition), + background var(--transition); flex-shrink: 0; } @@ -510,7 +578,9 @@ img { box-shadow: 0 0 0 3px var(--accent-light); } -.admin-form-checkbox:hover input:not(:checked):not(:disabled):not(:indeterminate) + span::before { +.admin-form-checkbox:hover + input:not(:checked):not(:disabled):not(:indeterminate) + + span::before { border-color: var(--border-color-hover); background: var(--bg-secondary); } @@ -641,7 +711,7 @@ img { /* Required field indicator */ .admin-form-label.required::after { - content: ' *'; + content: " *"; color: var(--danger); font-weight: 600; } @@ -760,7 +830,6 @@ img { color: var(--text-primary); } - .admin-btn-icon { display: inline-flex; align-items: center; @@ -823,7 +892,7 @@ img { [data-theme="dark"] .admin-sidebar { --sb-bg: #141414; --sb-border: #2a2a2a; - --sb-text: #A0A0A0; + --sb-text: #a0a0a0; --sb-text-hover: #ddd; --sb-hover-bg: #1f1f1f; --sb-active-bg: #ffffff; @@ -835,14 +904,14 @@ img { [data-theme="light"] .admin-sidebar { --sb-bg: #ffffff; - --sb-border: #E8E6E1; - --sb-text: #7C7C84; - --sb-text-hover: #1A1A1A; - --sb-hover-bg: #F5F4F2; + --sb-border: #e8e6e1; + --sb-text: #7c7c84; + --sb-text-hover: #1a1a1a; + --sb-hover-bg: #f5f4f2; --sb-active-bg: #141414; --sb-active-text: #ffffff; - --sb-label: #A0A0A0; - --sb-muted: #A0A0A0; + --sb-label: #a0a0a0; + --sb-muted: #a0a0a0; --sb-scrollbar: #ddd; } @@ -866,7 +935,9 @@ img { padding-right: env(safe-area-inset-right, 0px); transform: translateX(-100%); visibility: hidden; - transition: transform 0.3s ease, visibility 0.3s ease; + transition: + transform 0.3s ease, + visibility 0.3s ease; overflow: hidden; overscroll-behavior: none; } @@ -889,7 +960,9 @@ img { } [data-theme="light"] .admin-sidebar { - box-shadow: 1px 0 0 0 var(--sb-border), 4px 0 16px rgba(0, 0, 0, 0.04); + box-shadow: + 1px 0 0 0 var(--sb-border), + 4px 0 16px rgba(0, 0, 0, 0.04); } /* Sidebar Overlay (mobile) */ @@ -1367,8 +1440,6 @@ img { } } -/* Stat cards, quick links, dashboard modules moved to dashboard.css */ - /* ============================================================================ Tables ============================================================================ */ @@ -1557,21 +1628,54 @@ img { } /* Status Badges - Leave Requests */ -.badge-pending { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); } -.badge-approved { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } -.badge-rejected { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } -.badge-cancelled { background: var(--muted-light); color: var(--muted); } +.badge-pending { + background: color-mix(in srgb, var(--warning) 15%, transparent); + color: var(--warning); +} +.badge-approved { + background: color-mix(in srgb, var(--success) 15%, transparent); + color: var(--success); +} +.badge-rejected { + background: color-mix(in srgb, var(--danger) 15%, transparent); + color: var(--danger); +} +.badge-cancelled { + background: var(--muted-light); + color: var(--muted); +} /* Status Badges - Orders */ -.admin-badge-order-prijata { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info); } -.admin-badge-order-realizace { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); } -.admin-badge-order-dokoncena { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } -.admin-badge-order-stornovana { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } +.admin-badge-order-prijata { + background: color-mix(in srgb, var(--info) 15%, transparent); + color: var(--info); +} +.admin-badge-order-realizace { + background: color-mix(in srgb, var(--warning) 15%, transparent); + color: var(--warning); +} +.admin-badge-order-dokoncena { + background: color-mix(in srgb, var(--success) 15%, transparent); + color: var(--success); +} +.admin-badge-order-stornovana { + background: color-mix(in srgb, var(--danger) 15%, transparent); + color: var(--danger); +} /* Status Badges - Projects */ -.admin-badge-project-aktivni { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } -.admin-badge-project-dokonceny { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info); } -.admin-badge-project-zruseny { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } +.admin-badge-project-aktivni { + background: color-mix(in srgb, var(--success) 15%, transparent); + color: var(--success); +} +.admin-badge-project-dokonceny { + background: color-mix(in srgb, var(--info) 15%, transparent); + color: var(--info); +} +.admin-badge-project-zruseny { + background: color-mix(in srgb, var(--danger) 15%, transparent); + color: var(--danger); +} /* ============================================================================ Modals @@ -1823,10 +1927,18 @@ img { color: var(--text-primary); } -.admin-toast-success .admin-toast-icon { color: var(--success); } -.admin-toast-error .admin-toast-icon { color: var(--danger); } -.admin-toast-warning .admin-toast-icon { color: var(--warning); } -.admin-toast-info .admin-toast-icon { color: var(--info); } +.admin-toast-success .admin-toast-icon { + color: var(--success); +} +.admin-toast-error .admin-toast-icon { + color: var(--danger); +} +.admin-toast-warning .admin-toast-icon { + color: var(--warning); +} +.admin-toast-info .admin-toast-icon { + color: var(--info); +} /* ============================================================================ Loading & Animations @@ -1849,22 +1961,38 @@ img { } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } @keyframes float { - 0%, 100% { transform: translate(0, 0); } - 50% { transform: translate(30px, -30px); } + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(30px, -30px); + } } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } @keyframes shimmer { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } } /* ============================================================================ @@ -1881,7 +2009,9 @@ img { } @keyframes skeleton-fade-in { - to { opacity: 1; } + to { + opacity: 1; + } } .admin-skeleton-row { @@ -1893,18 +2023,37 @@ img { .admin-skeleton-line { height: 14px; border-radius: 6px; - background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-color) 50%, var(--bg-tertiary) 75%); + background: linear-gradient( + 90deg, + var(--bg-tertiary) 25%, + var(--border-color) 50%, + var(--bg-tertiary) 75% + ); background-size: 200% 100%; animation: shimmer 1.2s ease-in-out infinite; } -.admin-skeleton-line.w-full { width: 100%; } -.admin-skeleton-line.w-3\/4 { width: 75%; } -.admin-skeleton-line.w-1\/2 { width: 50%; } -.admin-skeleton-line.w-1\/3 { width: 33%; } -.admin-skeleton-line.w-1\/4 { width: 25%; } -.admin-skeleton-line.h-8 { height: 32px; } -.admin-skeleton-line.h-10 { height: 40px; } +.admin-skeleton-line.w-full { + width: 100%; +} +.admin-skeleton-line.w-3\/4 { + width: 75%; +} +.admin-skeleton-line.w-1\/2 { + width: 50%; +} +.admin-skeleton-line.w-1\/3 { + width: 33%; +} +.admin-skeleton-line.w-1\/4 { + width: 25%; +} +.admin-skeleton-line.h-8 { + height: 32px; +} +.admin-skeleton-line.h-10 { + height: 40px; +} .admin-skeleton-line.circle { width: 40px; height: 40px; @@ -1939,7 +2088,10 @@ img { font-weight: 500; font-family: inherit; cursor: pointer; - transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; + transition: + color 0.2s ease, + background 0.2s ease, + box-shadow 0.2s ease; letter-spacing: 0.01em; white-space: nowrap; } @@ -1952,7 +2104,9 @@ img { color: var(--text-primary); font-weight: 600; background: var(--bg-secondary); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--border-color); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 0 0 1px var(--border-color); } /* ============================================================================ @@ -1986,13 +2140,6 @@ img { max-width: 320px; } -/* Back link styles moved to login.css */ -/* Attendance styles moved to attendance.css */ - -/* Sessions/devices styles moved to dashboard.css */ - -/* Settings/permissions styles moved to settings.css */ - .admin-role-locked-notice { display: flex; align-items: center; @@ -2006,13 +2153,6 @@ img { margin-bottom: 0.5rem; } -/* Offers styles moved to offers.css */ - -/* Leave badges moved to leave.css */ -/* Order badges moved to orders.css */ -/* Project badges moved to projects.css */ - - /* ============================================================================ React DatePicker Overrides ============================================================================ */ @@ -2060,7 +2200,9 @@ img { .react-datepicker__day { color: var(--text-primary) !important; border-radius: 6px !important; - transition: background 0.15s, color 0.15s !important; + transition: + background 0.15s, + color 0.15s !important; } .react-datepicker__day:hover { @@ -2118,21 +2260,35 @@ img { background-color: var(--bg-secondary) !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box { width: 100% !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item { color: var(--text-primary) !important; transition: background 0.15s !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item:hover { background-color: var(--accent-light) !important; color: var(--text-primary) !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item--selected { background-color: var(--accent-color) !important; color: #fff !important; font-weight: 600 !important; @@ -2261,7 +2417,7 @@ img { .admin-form-select, .admin-form-textarea { min-height: 44px; - font-size: 16px; /* zabrání auto-zoomu na iOS */ + font-size: 16px; /* prevent auto-zoom on iOS */ } .admin-form-checkbox { @@ -2397,7 +2553,9 @@ img { cursor: grab; border-radius: 4px; padding: 0; - transition: color 0.15s, background 0.15s; + transition: + color 0.15s, + background 0.15s; touch-action: none; } @@ -2452,7 +2610,10 @@ img { font-size: 13px; font-family: var(--font-mono); cursor: pointer; - transition: background 0.15s, color 0.15s, border-color 0.15s; + transition: + background 0.15s, + color 0.15s, + border-color 0.15s; } .admin-pagination-page:hover { @@ -2722,7 +2883,9 @@ img { } @keyframes dp-fade-in { - to { opacity: 1; } + to { + opacity: 1; + } } .react-datepicker { @@ -2758,7 +2921,9 @@ img { .react-datepicker__day { color: var(--text-primary) !important; border-radius: 6px !important; - transition: background 0.15s, color 0.15s !important; + transition: + background 0.15s, + color 0.15s !important; } .react-datepicker__day:hover { @@ -2813,21 +2978,35 @@ img { background-color: var(--bg-secondary) !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box { width: 100% !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item { color: var(--text-primary) !important; transition: background 0.15s !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item:hover { background-color: var(--accent-light) !important; color: var(--text-primary) !important; } -.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected { +.react-datepicker__time-container + .react-datepicker__time + .react-datepicker__time-box + ul.react-datepicker__time-list + li.react-datepicker__time-list-item--selected { background-color: var(--accent-color) !important; color: #fff !important; font-weight: 600 !important; @@ -3047,4 +3226,3 @@ img { color: var(--text-tertiary); cursor: help; } - diff --git a/src/admin/dashboard.css b/src/admin/dashboard.css index c92db51..02199de 100644 --- a/src/admin/dashboard.css +++ b/src/admin/dashboard.css @@ -18,7 +18,7 @@ } .admin-stat-card::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -28,10 +28,18 @@ border-radius: var(--border-radius) var(--border-radius) 0 0; } -.admin-stat-card.success::before { background: var(--success); } -.admin-stat-card.warning::before { background: var(--warning); } -.admin-stat-card.danger::before { background: var(--danger); } -.admin-stat-card.info::before { background: var(--info); } +.admin-stat-card.success::before { + background: var(--success); +} +.admin-stat-card.warning::before { + background: var(--warning); +} +.admin-stat-card.danger::before { + background: var(--danger); +} +.admin-stat-card.info::before { + background: var(--info); +} .admin-stat-icon { width: 40px; @@ -73,10 +81,22 @@ color: var(--text-secondary); } -.admin-stat-icon.danger { background: var(--danger-soft); color: var(--danger); } -.admin-stat-icon.info { background: var(--info-soft); color: var(--info); } -.admin-stat-icon.success { background: var(--success-soft); color: var(--success); } -.admin-stat-icon.warning { background: var(--warning-soft); color: var(--warning); } +.admin-stat-icon.danger { + background: var(--danger-soft); + color: var(--danger); +} +.admin-stat-icon.info { + background: var(--info-soft); + color: var(--info); +} +.admin-stat-icon.success { + background: var(--success-soft); + color: var(--success); +} +.admin-stat-icon.warning { + background: var(--warning-soft); + color: var(--warning); +} /* ============================================================================ Dashboard @@ -99,10 +119,19 @@ gap: 0.875rem; } -.dash-kpi-4 { grid-template-columns: repeat(4, 1fr); } -.dash-kpi-3 { grid-template-columns: repeat(3, 1fr); } -.dash-kpi-2 { grid-template-columns: repeat(2, 1fr); } -.dash-kpi-1 { grid-template-columns: 1fr; max-width: 320px; } +.dash-kpi-4 { + grid-template-columns: repeat(4, 1fr); +} +.dash-kpi-3 { + grid-template-columns: repeat(3, 1fr); +} +.dash-kpi-2 { + grid-template-columns: repeat(2, 1fr); +} +.dash-kpi-1 { + grid-template-columns: 1fr; + max-width: 320px; +} /* Quick actions */ .dash-quick-actions { @@ -134,20 +163,44 @@ transform: none !important; } -.dash-quick-btn-success { background: var(--success-soft); color: var(--success); } -.dash-quick-btn-info { background: var(--info-soft); color: var(--info); } -.dash-quick-btn-warning { background: var(--warning-soft); color: var(--warning); } -.dash-quick-btn-danger { background: var(--danger-soft); color: var(--danger); } +.dash-quick-btn-success { + background: var(--success-soft); + color: var(--success); +} +.dash-quick-btn-info { + background: var(--info-soft); + color: var(--info); +} +.dash-quick-btn-warning { + background: var(--warning-soft); + color: var(--warning); +} +.dash-quick-btn-danger { + background: var(--danger-soft); + color: var(--danger); +} .dash-quick-btn:hover { transform: translateY(-1px); filter: brightness(0.95); } -[data-theme="light"] .dash-quick-btn-success { background: var(--success); color: #fff; } -[data-theme="light"] .dash-quick-btn-info { background: var(--info); color: #fff; } -[data-theme="light"] .dash-quick-btn-warning { background: var(--warning); color: #fff; } -[data-theme="light"] .dash-quick-btn-danger { background: var(--danger); color: #fff; } +[data-theme="light"] .dash-quick-btn-success { + background: var(--success); + color: #fff; +} +[data-theme="light"] .dash-quick-btn-info { + background: var(--info); + color: #fff; +} +[data-theme="light"] .dash-quick-btn-warning { + background: var(--warning); + color: #fff; +} +[data-theme="light"] .dash-quick-btn-danger { + background: var(--danger); + color: #fff; +} /* Main content 3-col grid */ .dash-main-grid { @@ -197,12 +250,30 @@ flex-shrink: 0; } -.dash-activity-icon.success { background: var(--success-soft); color: var(--success); } -.dash-activity-icon.info { background: var(--info-soft); color: var(--info); } -.dash-activity-icon.warning { background: var(--warning-soft); color: var(--warning); } -.dash-activity-icon.danger { background: var(--danger-soft); color: var(--danger); } -.dash-activity-icon.accent { background: var(--accent-soft); color: var(--accent-color); } -.dash-activity-icon.muted { background: var(--bg-tertiary); color: var(--text-secondary); } +.dash-activity-icon.success { + background: var(--success-soft); + color: var(--success); +} +.dash-activity-icon.info { + background: var(--info-soft); + color: var(--info); +} +.dash-activity-icon.warning { + background: var(--warning-soft); + color: var(--warning); +} +.dash-activity-icon.danger { + background: var(--danger-soft); + color: var(--danger); +} +.dash-activity-icon.accent { + background: var(--accent-soft); + color: var(--accent-color); +} +.dash-activity-icon.muted { + background: var(--bg-tertiary); + color: var(--text-secondary); +} .dash-activity-main { flex: 1; @@ -256,10 +327,22 @@ text-transform: uppercase; } -.dash-presence-avatar.dash-status-in { background: var(--success-soft); color: var(--success); } -.dash-presence-avatar.dash-status-away { background: var(--warning-soft); color: var(--warning); } -.dash-presence-avatar.dash-status-out { background: var(--bg-tertiary); color: var(--text-muted); } -.dash-presence-avatar.dash-status-leave { background: var(--info-soft); color: var(--info); } +.dash-presence-avatar.dash-status-in { + background: var(--success-soft); + color: var(--success); +} +.dash-presence-avatar.dash-status-away { + background: var(--warning-soft); + color: var(--warning); +} +.dash-presence-avatar.dash-status-out { + background: var(--bg-tertiary); + color: var(--text-muted); +} +.dash-presence-avatar.dash-status-leave { + background: var(--info-soft); + color: var(--info); +} .dash-status-dot { width: 8px; @@ -268,15 +351,31 @@ flex-shrink: 0; } -.dash-status-dot.dash-status-in { background: var(--success); } -.dash-status-dot.dash-status-away { background: var(--warning); } -.dash-status-dot.dash-status-out { background: var(--text-muted); } -.dash-status-dot.dash-status-leave { background: var(--info); } +.dash-status-dot.dash-status-in { + background: var(--success); +} +.dash-status-dot.dash-status-away { + background: var(--warning); +} +.dash-status-dot.dash-status-out { + background: var(--text-muted); +} +.dash-status-dot.dash-status-leave { + background: var(--info); +} -.dash-presence-label.dash-status-in { color: var(--success); } -.dash-presence-label.dash-status-away { color: var(--warning); } -.dash-presence-label.dash-status-out { color: var(--text-muted); } -.dash-presence-label.dash-status-leave { color: var(--info); } +.dash-presence-label.dash-status-in { + color: var(--success); +} +.dash-presence-label.dash-status-away { + color: var(--warning); +} +.dash-presence-label.dash-status-out { + color: var(--text-muted); +} +.dash-presence-label.dash-status-leave { + color: var(--info); +} .dash-presence-name { flex: 1; @@ -414,12 +513,18 @@ grid-template-columns: 1fr 1fr; } - .dash-kpi-4 { grid-template-columns: repeat(2, 1fr); } + .dash-kpi-4 { + grid-template-columns: repeat(2, 1fr); + } } @media (max-width: 768px) { - .dash-kpi-grid { grid-template-columns: repeat(2, 1fr); } - .dash-quick-actions { grid-template-columns: repeat(2, 1fr); } + .dash-kpi-grid { + grid-template-columns: repeat(2, 1fr); + } + .dash-quick-actions { + grid-template-columns: repeat(2, 1fr); + } .dash-main-grid { grid-template-columns: 1fr; @@ -435,9 +540,15 @@ } @media (max-width: 480px) { - .dash-quick-actions { grid-template-columns: 1fr 1fr; } - .dash-kpi-grid { grid-template-columns: 1fr; } - .dash-profile-grid { grid-template-columns: 1fr; } + .dash-quick-actions { + grid-template-columns: 1fr 1fr; + } + .dash-kpi-grid { + grid-template-columns: 1fr; + } + .dash-profile-grid { + grid-template-columns: 1fr; + } } /* ============================================================================ diff --git a/src/admin/hooks/useAttendanceAdmin.ts b/src/admin/hooks/useAttendanceAdmin.ts index 92f9641..c102c3e 100644 --- a/src/admin/hooks/useAttendanceAdmin.ts +++ b/src/admin/hooks/useAttendanceAdmin.ts @@ -479,7 +479,8 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { // ---- Create modal ---- const [showCreateModal, setShowCreateModal] = useState(false); - const today = new Date().toISOString().split("T")[0]; + const now = new Date(); + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const [createForm, setCreateForm] = useState({ user_id: "", shift_date: today, @@ -589,11 +590,9 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { try { const [yearStr, monthStr] = month.split("-"); - // Build records URL let recordsUrl = `${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000`; if (filterUserId) recordsUrl += `&user_id=${filterUserId}`; - // Fetch records and balances in parallel const [recordsResponse, balancesResponse] = await Promise.all([ apiFetch(recordsUrl), apiFetch(`${API_BASE}/attendance?action=balances&year=${yearStr}`), @@ -614,7 +613,6 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { const leaveBalances: Record = balancesObj?.balances ?? balancesObj ?? {}; - // Compute user_totals client-side const userTotals = computeUserTotals(records, usersRef.current, month); setData((prev) => ({ @@ -670,7 +668,8 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { // Create modal // ========================================================================= const openCreateModal = () => { - const todayDate = new Date().toISOString().split("T")[0]; + const d = new Date(); + const todayDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; setCreateForm({ user_id: "", shift_date: todayDate, diff --git a/src/admin/invoices.css b/src/admin/invoices.css index 59f916e..9cb7462 100644 --- a/src/admin/invoices.css +++ b/src/admin/invoices.css @@ -48,7 +48,9 @@ background: transparent; color: var(--text-secondary); cursor: pointer; - transition: border-color 0.15s, color 0.15s; + transition: + border-color 0.15s, + color 0.15s; } .invoice-month-btn:hover:not(:disabled) { @@ -138,4 +140,3 @@ max-width: 200px; } } - diff --git a/src/admin/offers.css b/src/admin/offers.css index 48d076a..27071ca 100644 --- a/src/admin/offers.css +++ b/src/admin/offers.css @@ -164,7 +164,11 @@ } .offers-scope-section:hover { - border-color: color-mix(in srgb, var(--border-color) 70%, var(--accent-color)); + border-color: color-mix( + in srgb, + var(--border-color) 70%, + var(--accent-color) + ); } .offers-scope-section-header { @@ -397,7 +401,10 @@ font-weight: 500; font-family: inherit; cursor: pointer; - transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; + transition: + color 0.2s ease, + background 0.2s ease, + box-shadow 0.2s ease; letter-spacing: 0.01em; white-space: nowrap; } @@ -410,7 +417,9 @@ color: var(--text-primary); font-weight: 600; background: var(--bg-secondary); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--border-color); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 0 0 1px var(--border-color); } /* RichEditor (Quill) */ @@ -509,78 +518,189 @@ } /* Font picker */ -.rich-editor .ql-snow .ql-font .ql-picker-options { min-width: 11rem; max-height: 200px; overflow-y: auto; } -.rich-editor .ql-snow .ql-size .ql-picker-options { max-height: 200px; overflow-y: auto; } +.rich-editor .ql-snow .ql-font .ql-picker-options { + min-width: 11rem; + max-height: 200px; + overflow-y: auto; +} +.rich-editor .ql-snow .ql-size .ql-picker-options { + max-height: 200px; + overflow-y: auto; +} /* Font labels - vysoka specificita kvuli quill.snow.css */ .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="arial"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="arial"]::before { content: 'Arial' !important; font-family: Arial, sans-serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="arial"]::before { + content: "Arial" !important; + font-family: Arial, sans-serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="tahoma"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="tahoma"]::before { content: 'Tahoma' !important; font-family: Tahoma, sans-serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="tahoma"]::before { + content: "Tahoma" !important; + font-family: Tahoma, sans-serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="verdana"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="verdana"]::before { content: 'Verdana' !important; font-family: Verdana, sans-serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="verdana"]::before { + content: "Verdana" !important; + font-family: Verdana, sans-serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="georgia"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="georgia"]::before { content: 'Georgia' !important; font-family: Georgia, serif; } -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="times-new-roman"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="times-new-roman"]::before { content: 'Times New Roman' !important; font-family: 'Times New Roman', serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="georgia"]::before { + content: "Georgia" !important; + font-family: Georgia, serif; +} +.ql-snow + .ql-picker.ql-font + .ql-picker-label[data-value="times-new-roman"]::before, +.ql-snow + .ql-picker.ql-font + .ql-picker-item[data-value="times-new-roman"]::before { + content: "Times New Roman" !important; + font-family: "Times New Roman", serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="courier-new"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="courier-new"]::before { content: 'Courier New' !important; font-family: 'Courier New', monospace; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="courier-new"]::before { + content: "Courier New" !important; + font-family: "Courier New", monospace; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="trebuchet-ms"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="trebuchet-ms"]::before { content: 'Trebuchet MS' !important; font-family: 'Trebuchet MS', sans-serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="trebuchet-ms"]::before { + content: "Trebuchet MS" !important; + font-family: "Trebuchet MS", sans-serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="impact"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="impact"]::before { content: 'Impact' !important; font-family: Impact, sans-serif; } -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="comic-sans-ms"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="comic-sans-ms"]::before { content: 'Comic Sans MS' !important; font-family: 'Comic Sans MS', cursive; } -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="lucida-console"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="lucida-console"]::before { content: 'Lucida Console' !important; font-family: 'Lucida Console', monospace; } -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="palatino-linotype"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="palatino-linotype"]::before { content: 'Palatino Linotype' !important; font-family: 'Palatino Linotype', serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="impact"]::before { + content: "Impact" !important; + font-family: Impact, sans-serif; +} +.ql-snow + .ql-picker.ql-font + .ql-picker-label[data-value="comic-sans-ms"]::before, +.ql-snow + .ql-picker.ql-font + .ql-picker-item[data-value="comic-sans-ms"]::before { + content: "Comic Sans MS" !important; + font-family: "Comic Sans MS", cursive; +} +.ql-snow + .ql-picker.ql-font + .ql-picker-label[data-value="lucida-console"]::before, +.ql-snow + .ql-picker.ql-font + .ql-picker-item[data-value="lucida-console"]::before { + content: "Lucida Console" !important; + font-family: "Lucida Console", monospace; +} +.ql-snow + .ql-picker.ql-font + .ql-picker-label[data-value="palatino-linotype"]::before, +.ql-snow + .ql-picker.ql-font + .ql-picker-item[data-value="palatino-linotype"]::before { + content: "Palatino Linotype" !important; + font-family: "Palatino Linotype", serif; +} .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="garamond"]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="garamond"]::before { content: 'Garamond' !important; font-family: Garamond, serif; } +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="garamond"]::before { + content: "Garamond" !important; + font-family: Garamond, serif; +} /* Font classes */ -.ql-font-arial { font-family: Arial, sans-serif; } -.ql-font-tahoma { font-family: Tahoma, sans-serif; } -.ql-font-verdana { font-family: Verdana, sans-serif; } -.ql-font-georgia { font-family: Georgia, serif; } -.ql-font-times-new-roman { font-family: 'Times New Roman', serif; } -.ql-font-courier-new { font-family: 'Courier New', monospace; } -.ql-font-trebuchet-ms { font-family: 'Trebuchet MS', sans-serif; } -.ql-font-impact { font-family: Impact, sans-serif; } -.ql-font-comic-sans-ms { font-family: 'Comic Sans MS', cursive; } -.ql-font-lucida-console { font-family: 'Lucida Console', monospace; } -.ql-font-palatino-linotype { font-family: 'Palatino Linotype', serif; } -.ql-font-garamond { font-family: Garamond, serif; } +.ql-font-arial { + font-family: Arial, sans-serif; +} +.ql-font-tahoma { + font-family: Tahoma, sans-serif; +} +.ql-font-verdana { + font-family: Verdana, sans-serif; +} +.ql-font-georgia { + font-family: Georgia, serif; +} +.ql-font-times-new-roman { + font-family: "Times New Roman", serif; +} +.ql-font-courier-new { + font-family: "Courier New", monospace; +} +.ql-font-trebuchet-ms { + font-family: "Trebuchet MS", sans-serif; +} +.ql-font-impact { + font-family: Impact, sans-serif; +} +.ql-font-comic-sans-ms { + font-family: "Comic Sans MS", cursive; +} +.ql-font-lucida-console { + font-family: "Lucida Console", monospace; +} +.ql-font-palatino-linotype { + font-family: "Palatino Linotype", serif; +} +.ql-font-garamond { + font-family: Garamond, serif; +} /* Size picker */ .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="8px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="8px"]::before { content: '8px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="8px"]::before { + content: "8px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="9px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="9px"]::before { content: '9px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="9px"]::before { + content: "9px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before { content: '10px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before { + content: "10px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="11px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="11px"]::before { content: '11px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="11px"]::before { + content: "11px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before { content: '12px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before { + content: "12px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before { content: '14px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before { + content: "14px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before { content: '16px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before { + content: "16px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before { content: '18px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before { + content: "18px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before { content: '20px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before { + content: "20px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before { content: '24px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before { + content: "24px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before { content: '28px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before { + content: "28px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="32px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before { content: '32px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before { + content: "32px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before { content: '36px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before { + content: "36px" !important; +} .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="48px"]::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="48px"]::before { content: '48px' !important; } +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="48px"]::before { + content: "48px" !important; +} /* Editor area */ .rich-editor .ql-container.ql-snow { @@ -627,7 +747,6 @@ flex-shrink: 0; } - /* Tooltip (link editor) */ .rich-editor .ql-snow .ql-tooltip { background: var(--bg-primary); diff --git a/src/admin/pages/Attendance.tsx b/src/admin/pages/Attendance.tsx index 7871417..81d6b28 100644 --- a/src/admin/pages/Attendance.tsx +++ b/src/admin/pages/Attendance.tsx @@ -755,10 +755,7 @@ export default function Attendance() { {formatTime(shift.departure_time)} - {formatMinutes( - calculateWorkMinutes(shift as any), - true, - )} + {formatMinutes(calculateWorkMinutes(shift), true)} {projects.length > 0 && ( diff --git a/src/admin/pages/AttendanceHistory.tsx b/src/admin/pages/AttendanceHistory.tsx index 2ed454d..dc263ca 100644 --- a/src/admin/pages/AttendanceHistory.tsx +++ b/src/admin/pages/AttendanceHistory.tsx @@ -156,7 +156,6 @@ export default function AttendanceHistory() { fetchData(); }, [fetchData]); - // Compute totals client-side from raw records const computed = useMemo(() => { const [yearStr, monthStr] = month.split("-"); const monthIndex = parseInt(monthStr, 10) - 1; @@ -181,11 +180,9 @@ export default function AttendanceHistory() { } } - // Compute monthly fund (working days * 8h) // Exclude holidays from business days (matching PHP CzechHolidays logic) const yr = parseInt(yearStr, 10); const mo = parseInt(monthStr, 10) - 1; - // Count holiday records to subtract from business days const holidayDays = records.filter( (r) => (r.leave_type || "work") === "holiday", ).length; diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index 65e91c3..5b76652 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -12,7 +12,7 @@ import Forbidden from "../components/Forbidden"; import FormField from "../components/FormField"; import AdminDatePicker from "../components/AdminDatePicker"; import ConfirmModal from "../components/ConfirmModal"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import { DndContext, closestCenter, @@ -104,6 +104,7 @@ interface InvoiceForm { issued_by: string; billing_text: string; notes: string; + language: string; bank_account_id: number | string; bank_name: string; bank_swift: string; @@ -132,6 +133,7 @@ interface Invoice { issued_by: string | null; paid_date?: string; notes: string; + language: string; apply_vat: number | string; items: Omit[]; valid_transitions?: string[]; @@ -521,6 +523,7 @@ export default function InvoiceDetail() { issued_by: user?.fullName || "", billing_text: "", notes: "", + language: "cs", bank_account_id: "", bank_name: "", bank_swift: "", @@ -536,12 +539,10 @@ export default function InvoiceDetail() { const [loading, setLoading] = useState(true); const [invoiceNumber, setInvoiceNumber] = useState(""); - // Customer selector (create mode) const [customers, setCustomers] = useState([]); const [customerSearch, setCustomerSearch] = useState(""); const [showCustomerDropdown, setShowCustomerDropdown] = useState(false); - // Draft const DRAFT_KEY = "boha_invoice_draft"; const clearDraft = useCallback(() => { @@ -561,18 +562,15 @@ export default function InvoiceDetail() { status: string | null; }>({ show: false, status: null }); const [pdfLoading, setPdfLoading] = useState(false); - const [langModal, setLangModal] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); const [deleting, setDeleting] = useState(false); - // Edit items (edit mode) const [editingItems, setEditingItems] = useState(false); const [editItems, setEditItems] = useState([]); const editKeyCounter = useRef(0); // ─── Data loading ─── - // Create mode: load next number, customers, bank accounts, order data useEffect(() => { if (isEdit) return; const load = async () => { @@ -843,6 +841,9 @@ export default function InvoiceDetail() { const result = await response.json(); if (result.success) { clearDraft(); + await apiFetch( + `${API_BASE}/invoices-pdf/${result.data.invoice_id}?lang=${form.language}&save=1`, + ).catch(() => {}); alert.success(result.message || "Faktura byla vytvořena"); navigate(`/invoices/${result.data.invoice_id}`); } else { @@ -918,6 +919,9 @@ export default function InvoiceDetail() { }); const result = await response.json(); if (result.success) { + await apiFetch( + `${API_BASE}/invoices-pdf/${id}?lang=${invoice?.language || "cs"}&save=1`, + ).catch(() => {}); alert.success("Poznámky byly uloženy"); } else { alert.error(result.error || "Nepodařilo se uložit poznámky"); @@ -930,28 +934,19 @@ export default function InvoiceDetail() { }; // ─── Edit mode: PDF export ─── - const handleViewPdf = async (lang = "cs") => { - setLangModal(false); - const newWindow = window.open("", "_blank"); + const handleViewPdf = async (_lang = "cs") => { setPdfLoading(true); try { - const response = await apiFetch( - `${API_BASE}/invoices-pdf/${id}?lang=${encodeURIComponent(lang)}`, - ); + const response = await apiFetch(`${API_BASE}/invoices/${id}/file`); if (!response.ok) { - newWindow?.close(); - alert.error("Nepodařilo se vygenerovat PDF"); + alert.error("PDF soubor nenalezen — uložte fakturu pro vygenerování"); return; } - const html = await response.text(); - if (newWindow) { - newWindow.document.open(); - newWindow.document.write(html); - newWindow.document.close(); - newWindow.onload = () => newWindow.print(); - } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { - newWindow?.close(); alert.error("Chyba připojení"); } finally { setPdfLoading(false); @@ -1028,6 +1023,10 @@ export default function InvoiceDetail() { }); const result = await response.json(); if (result.success) { + // Regenerate PDF on NAS (fire-and-forget) + await apiFetch( + `${API_BASE}/invoices-pdf/${id}?lang=${invoice?.language || "cs"}&save=1`, + ).catch(() => {}); alert.success("Položky byly uloženy"); setEditingItems(false); fetchDetail(); @@ -1379,6 +1378,21 @@ export default function InvoiceDetail() { + + +
- {hasPermission("invoices.export") && ( + {hasPermission("invoices.export") && invoice && ( )} {hasPermission("invoices.edit") && @@ -2026,70 +2040,6 @@ export default function InvoiceDetail() { type="danger" loading={deleting} /> - - {/* Language selection for PDF */} - - {langModal && ( - -
setLangModal(false)} - /> - -
-
- - - - - -
-

Jazyk faktury

-

- V jakém jazyce chcete vygenerovat fakturu? -

-
-
- - -
-
- - )} -
); } diff --git a/src/admin/pages/Invoices.tsx b/src/admin/pages/Invoices.tsx index f604c0c..2daefd3 100644 --- a/src/admin/pages/Invoices.tsx +++ b/src/admin/pages/Invoices.tsx @@ -223,7 +223,11 @@ export default function Invoices() { sort, order, page, - extraParams: statusFilter ? { status: statusFilter } : {}, + extraParams: { + month: String(statsMonth), + year: String(statsYear), + ...(statusFilter ? { status: statusFilter } : {}), + }, errorMsg: "Nepodařilo se načíst faktury", }); diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx index f9f177e..34cdb84 100644 --- a/src/admin/pages/OfferDetail.tsx +++ b/src/admin/pages/OfferDetail.tsx @@ -345,7 +345,6 @@ export default function OfferDetail() { form.valid_until && new Date(form.valid_until) < new Date(new Date().toDateString()); - // Load data const fetchDetail = useCallback(async () => { if (!id) return; try { @@ -473,7 +472,6 @@ export default function OfferDetail() { } }, [showCustomerDropdown]); - // Fetch next quotation number for new offers useEffect(() => { if (isEdit) return; const fetchNextNumber = async () => { @@ -584,7 +582,6 @@ export default function OfferDetail() { setItems((prev) => prev.filter((_, i) => i !== index)); }; - // Totals const subtotal = items.reduce((sum, item) => { if (item.is_included_in_total) { return ( @@ -624,6 +621,12 @@ export default function OfferDetail() { }); const result = await response.json(); if (result.success) { + const offerId = isEdit ? id : result.data?.id; + if (offerId) { + await apiFetch(`${API_BASE}/offers-pdf/${offerId}?save=1`).catch( + () => {}, + ); + } alert.success( result.message || (isEdit ? "Nabídka byla aktualizována" : "Nabídka byla vytvořena"), @@ -736,22 +739,16 @@ export default function OfferDetail() { if (!isEdit || pdfLoading) return; setPdfLoading(true); try { - const response = await apiFetch(`${API_BASE}/offers-pdf/${id}`); + const response = await apiFetch(`${API_BASE}/offers/${id}/file`); if (response.status === 401) return; if (!response.ok) { - alert.error("Nepodařilo se vygenerovat PDF"); + alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování"); return; } - const html = await response.text(); - const w = window.open("", "_blank"); - if (w) { - w.document.open(); - w.document.write(html); - w.document.close(); - w.onload = () => w.print(); - } else { - alert.error("Prohlížeč zablokoval vyskakovací okno"); - } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { alert.error("Chyba při generování PDF"); } finally { @@ -858,29 +855,29 @@ export default function OfferDetail() { )} {isEdit && diff --git a/src/admin/pages/ReceivedInvoices.tsx b/src/admin/pages/ReceivedInvoices.tsx index a4db274..2bc84ed 100644 --- a/src/admin/pages/ReceivedInvoices.tsx +++ b/src/admin/pages/ReceivedInvoices.tsx @@ -148,7 +148,6 @@ export default function ReceivedInvoices({ const { sort, order, handleSort, activeSort } = useTableSort("created_at"); const [search, setSearch] = useState(""); - // Data const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); const [stats, setStats] = useState(null); @@ -159,7 +158,6 @@ export default function ReceivedInvoices({ const prevMonth = useRef(statsMonth); const prevYear = useRef(statsYear); - // Modals const [editOpen, setEditOpen] = useState(false); const [editInvoice, setEditInvoice] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ @@ -169,10 +167,8 @@ export default function ReceivedInvoices({ const [deleting, setDeleting] = useState(false); const [saving, setSaving] = useState(false); - // Supplier autocomplete const [supplierNames, setSupplierNames] = useState([]); - // Upload state const [uploadFiles, setUploadFiles] = useState([]); const [uploadMeta, setUploadMeta] = useState([]); const [uploadErrors, setUploadErrors] = useState({}); @@ -180,7 +176,6 @@ export default function ReceivedInvoices({ useModalLock(uploadOpen || editOpen); - // Slide direction detection useEffect(() => { const prev = prevYear.current * 12 + prevMonth.current; const curr = statsYear * 12 + statsMonth; @@ -194,7 +189,6 @@ export default function ReceivedInvoices({ prevYear.current = statsYear; }, [statsMonth, statsYear]); - // Fetch list const fetchList = useCallback(async () => { if (!hasLoadedOnce.current) setLoading(true); try { @@ -228,7 +222,6 @@ export default function ReceivedInvoices({ fetchList(); }, [fetchList]); - // Fetch supplier names for autocomplete useEffect(() => { apiFetch(`${API_BASE}/received-invoices/suppliers`) .then((r) => r.json()) @@ -277,7 +270,6 @@ export default function ReceivedInvoices({ load(); }, [statsMonth, statsYear]); - // Upload handlers const handleFileSelect = (e: React.ChangeEvent) => { const selected = Array.from(e.target.files || []); if (selected.length === 0) { @@ -384,7 +376,6 @@ export default function ReceivedInvoices({ } }; - // Edit handlers const toDateInput = (d: string | null | undefined): string => { if (!d) return ""; const date = new Date(d); @@ -455,7 +446,6 @@ export default function ReceivedInvoices({ } }; - // Delete const handleDelete = async () => { if (!deleteConfirm.invoice) { return; @@ -484,26 +474,20 @@ export default function ReceivedInvoices({ } }; - // View file const openFile = async (inv: ReceivedInvoice) => { - const newWindow = window.open("", "_blank"); try { const response = await apiFetch( `${API_BASE}/received-invoices/${inv.id}/file`, ); if (!response.ok) { - newWindow?.close(); alert.error("Nepodařilo se načíst soubor"); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); - if (newWindow) { - newWindow.location.href = url; - } + window.open(url, "_blank"); setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { - newWindow?.close(); alert.error("Chyba připojení"); } }; @@ -531,7 +515,6 @@ export default function ReceivedInvoices({ const monthLabel = `${MONTH_NAMES[statsMonth - 1]}`; - // KPI const renderKpi = () => { if (!hasLoadedOnce.current && statsLoading) { return ( diff --git a/src/admin/pages/Settings.tsx b/src/admin/pages/Settings.tsx index e8b3fa6..661e19b 100644 --- a/src/admin/pages/Settings.tsx +++ b/src/admin/pages/Settings.tsx @@ -55,7 +55,6 @@ export default function Settings() { Record >({}); - // 2FA requirement const [require2FA, setRequire2FA] = useState(false); const [require2FALoading, setRequire2FALoading] = useState(true); const [require2FASaving, setRequire2FASaving] = useState(false); diff --git a/src/config/env.ts b/src/config/env.ts index 5bfe8cc..49b35e8 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -4,10 +4,11 @@ dotenv.config(); // Set timezone for Date operations — all attendance/time records are in Czech local time process.env.TZ = process.env.TZ || "Europe/Prague"; -// Override Date.toJSON to serialize as local time instead of UTC -// MySQL DATETIME stores local time, Prisma creates Date objects, -// JSON.stringify calls toJSON() which defaults to toISOString() (UTC with Z suffix). -// This causes times to shift by timezone offset on the frontend. +// Override Date.toJSON so JSON.stringify outputs local time (Europe/Prague). +// Prisma stores UTC in MySQL DATETIME columns. When reading, it creates +// JS Date objects with correct UTC internals. The default toJSON() calls +// toISOString() which returns UTC — this override uses local getters instead, +// so the frontend always receives times in the user's timezone. Date.prototype.toJSON = function () { const y = this.getFullYear(); const m = String(this.getMonth() + 1).padStart(2, "0"); @@ -53,6 +54,8 @@ export const config = { nas: { path: process.env.NAS_PATH || "Z:/02_PROJEKTY", + financialsPath: process.env.NAS_FINANCIALS_PATH || "", + offersPath: process.env.NAS_OFFERS_PATH || "", maxUploadSize: parseInt(process.env.MAX_UPLOAD_SIZE || "52428800", 10), }, diff --git a/src/routes/admin/attendance.ts b/src/routes/admin/attendance.ts index 6104a76..5f0a577 100644 --- a/src/routes/admin/attendance.ts +++ b/src/routes/admin/attendance.ts @@ -16,6 +16,7 @@ import { UpdateAttendanceSchema, } from "../../schemas/attendance.schema"; import * as attendanceService from "../../services/attendance.service"; +import { localMonthStr } from "../../utils/date"; export default async function attendanceRoutes( fastify: FastifyInstance, @@ -125,7 +126,7 @@ export default async function attendanceRoutes( const monthStr = query.month ? String(query.month) - : `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; + : localMonthStr(new Date()); const filterUserId = query.user_id ? Number(query.user_id) : null; const data = await attendanceService.getPrintData(monthStr, filterUserId); return reply.send({ success: true, data }); diff --git a/src/routes/admin/auth.ts b/src/routes/admin/auth.ts index c385f8b..7a65f08 100644 --- a/src/routes/admin/auth.ts +++ b/src/routes/admin/auth.ts @@ -128,7 +128,6 @@ export default async function authRoutes( return error(reply, "Neplatný TOTP kód", 401); } - // Delete used login token await prisma.totp_login_tokens.delete({ where: { id: storedToken.id } }); // Reset failed attempts and update last login (TOTP verified = successful login) @@ -149,7 +148,6 @@ export default async function authRoutes( return error(reply, "Chyba načítání uživatele", 500); } - // Create tokens manually since password was already verified const jwt = await import("jsonwebtoken"); const accessToken = jwt.default.sign( { diff --git a/src/routes/admin/bank-accounts.ts b/src/routes/admin/bank-accounts.ts index 3c7b147..6797dbc 100644 --- a/src/routes/admin/bank-accounts.ts +++ b/src/routes/admin/bank-accounts.ts @@ -40,8 +40,7 @@ export default async function bankAccountsRoutes( iban: body.iban ? String(body.iban) : null, bic: body.bic ? String(body.bic) : null, currency: body.currency ? String(body.currency) : "CZK", - is_default: - !!body.is_default, + is_default: !!body.is_default, position: body.position ? Number(body.position) : 0, }, }); @@ -107,9 +106,7 @@ export default async function bankAccountsRoutes( currency: body.currency !== undefined ? String(body.currency) : undefined, is_default: - body.is_default !== undefined - ? !!body.is_default - : undefined, + body.is_default !== undefined ? !!body.is_default : undefined, position: body.position !== undefined ? Number(body.position) : undefined, modified_at: new Date(), diff --git a/src/routes/admin/dashboard.ts b/src/routes/admin/dashboard.ts index fdd169b..5ec5c2b 100644 --- a/src/routes/admin/dashboard.ts +++ b/src/routes/admin/dashboard.ts @@ -2,6 +2,7 @@ import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requireAuth } from "../../middleware/auth"; import { success } from "../../utils/response"; +import { localTimeStr } from "../../utils/date"; export default async function dashboardRoutes( fastify: FastifyInstance, @@ -106,9 +107,7 @@ export default async function dashboardRoutes( name: `${user.first_name} ${user.last_name}`, initials: `${firstInitial}${lastInitial}`.toUpperCase(), status, - arrived_at: a.arrival_time - ? `${String(a.arrival_time.getHours()).padStart(2, "0")}:${String(a.arrival_time.getMinutes()).padStart(2, "0")}` - : null, + arrived_at: a.arrival_time ? localTimeStr(a.arrival_time) : null, }); } @@ -252,7 +251,7 @@ export default async function dashboardRoutes( entity_type: log.entity_type ?? "", description: log.description ?? "", username: log.username ?? null, - created_at: log.created_at ? log.created_at.toISOString() : "", + created_at: log.created_at ?? "", })); } diff --git a/src/routes/admin/invoices-pdf.ts b/src/routes/admin/invoices-pdf.ts index c58f1cf..ee2fe52 100644 --- a/src/routes/admin/invoices-pdf.ts +++ b/src/routes/admin/invoices-pdf.ts @@ -2,6 +2,9 @@ import { FastifyInstance } from "fastify"; import QRCode from "qrcode"; import prisma from "../../config/database"; import { requirePermission } from "../../middleware/auth"; +import { localDateCzStr } from "../../utils/date"; +import { nasFinancialsManager } from "../../services/nas-financials-manager"; +import { htmlToPdf } from "../../utils/html-to-pdf"; /* ── Helpers ─────────────────────────────────────────────────────── */ @@ -9,7 +12,7 @@ function formatDate(date: Date | string | null | undefined): string { if (!date) return ""; const d = new Date(date); if (isNaN(d.getTime())) return String(date); - return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`; + return localDateCzStr(d); } function formatNum(n: number, decimals = 2): string { @@ -278,7 +281,6 @@ export default async function invoicesPdfRoutes( unknown > | null; - // Order number lookup let orderNumber = ""; if (invoice.order_id) { const orderRow = await prisma.orders.findUnique({ @@ -298,7 +300,6 @@ export default async function invoicesPdfRoutes( } } - // Logo let logoImg = ""; if (settings?.logo_data) { const buf = Buffer.from(settings.logo_data as Buffer); @@ -313,7 +314,6 @@ export default async function invoicesPdfRoutes( const currency = invoice.currency || "CZK"; const applyVat = !!invoice.apply_vat; - // Calculations const vatSummary: Record = {}; let subtotal = 0; @@ -380,7 +380,6 @@ export default async function invoicesPdfRoutes( }); } - // Address lines const supp = buildAddressLines(settings, true, t); const cust = buildAddressLines(customer, false, t); @@ -410,7 +409,6 @@ export default async function invoicesPdfRoutes( const invoiceNumber = escapeHtml(invoice.invoice_number); - // Items HTML const itemsHtml = items .map((item, i) => { const qty = Number(item.quantity); @@ -434,7 +432,6 @@ export default async function invoicesPdfRoutes( }) .join(""); - // VAT recap rows const vatRecapHtml = vatRecap .map( (vr) => ` @@ -446,7 +443,6 @@ export default async function invoicesPdfRoutes( ) .join(""); - // VAT detail rows for totals section let vatDetailHtml = ""; if (applyVat) { for (const [rate, data] of Object.entries(vatSummary)) { @@ -460,7 +456,6 @@ export default async function invoicesPdfRoutes( } } - // Notes section const notesRaw = invoice.notes ?? ""; const notesStripped = notesRaw.replace(/<[^>]*>/g, "").trim(); const notesHtml = notesStripped @@ -1027,6 +1022,31 @@ ${indentCSS} `; + // Save PDF to NAS + if (nasFinancialsManager.isConfigured() && invoice.invoice_number) { + const issueDate = invoice.issue_date + ? new Date(invoice.issue_date) + : new Date(); + const saveMode = query.save === "1"; + const pdfPromise = htmlToPdf(html) + .then((pdfBuffer) => { + nasFinancialsManager.saveIssuedInvoicePdf( + invoice.invoice_number!, + issueDate.getFullYear(), + issueDate.getMonth() + 1, + pdfBuffer, + ); + }) + .catch((err) => { + request.log.error(err, "Failed to save invoice PDF to NAS"); + }); + + if (saveMode) { + await pdfPromise; + return reply.send({ success: true, message: "PDF uloženo" }); + } + } + return reply.type("text/html").send(html); }, ); diff --git a/src/routes/admin/invoices.ts b/src/routes/admin/invoices.ts index e21b238..1936e59 100644 --- a/src/routes/admin/invoices.ts +++ b/src/routes/admin/invoices.ts @@ -1,4 +1,5 @@ import { FastifyInstance } from "fastify"; +import prisma from "../../config/database"; import { requirePermission } from "../../middleware/auth"; import { logAudit } from "../../services/audit"; import { success, error, parseId } from "../../utils/response"; @@ -19,6 +20,7 @@ import { updateInvoice, deleteInvoice, } from "../../services/invoices.service"; +import { nasFinancialsManager } from "../../services/nas-financials-manager"; export default async function invoicesRoutes( fastify: FastifyInstance, @@ -46,6 +48,8 @@ export default async function invoicesRoutes( search, status: query.status ? String(query.status) : undefined, customer_id: query.customer_id ? Number(query.customer_id) : undefined, + month: query.month ? Number(query.month) : undefined, + year: query.year ? Number(query.year) : undefined, }); return reply.send({ @@ -185,6 +189,13 @@ export default async function invoicesRoutes( const existing = await deleteInvoice(id); if (!existing) return error(reply, "Faktura nenalezena", 404); + // Delete PDF from NAS + if (existing.invoice_number && existing.issue_date) { + const d = new Date(existing.issue_date); + const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${existing.invoice_number}.pdf`; + nasFinancialsManager.deleteIssuedInvoice(relPath); + } + await logAudit({ request, authData: request.authData, @@ -196,4 +207,33 @@ export default async function invoicesRoutes( return success(reply, null, 200, "Faktura smazána"); }, ); + + // GET /api/admin/invoices/:id/file — serve PDF from NAS + fastify.get<{ Params: { id: string } }>( + "/:id/file", + { preHandler: requirePermission("invoices.view") }, + async (request, reply) => { + const id = parseId(request.params.id, reply); + if (id === null) return; + const invoice = await prisma.invoices.findUnique({ + where: { id }, + select: { invoice_number: true, issue_date: true }, + }); + if (!invoice?.invoice_number || !invoice.issue_date) + return error(reply, "Faktura nenalezena", 404); + + const d = new Date(invoice.issue_date); + const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${invoice.invoice_number}.pdf`; + const file = nasFinancialsManager.readIssuedInvoice(relPath); + if (!file) return error(reply, "PDF soubor nenalezen", 404); + + return reply + .type("application/pdf") + .header( + "Content-Disposition", + `inline; filename="${invoice.invoice_number}.pdf"`, + ) + .send(file.data); + }, + ); } diff --git a/src/routes/admin/leave-requests.ts b/src/routes/admin/leave-requests.ts index 01fc814..02b45a4 100644 --- a/src/routes/admin/leave-requests.ts +++ b/src/routes/admin/leave-requests.ts @@ -205,7 +205,6 @@ export default async function leaveRequestsRoutes( } } - // Count business days and create attendance records let totalBusinessDays = 0; const current = new Date(dateFrom); const attendanceCreates: Array<{ @@ -242,7 +241,6 @@ export default async function leaveRequestsRoutes( const totalHours = totalBusinessDays * 8; - // Run everything in a transaction await prisma.$transaction(async (tx) => { // 1. Create attendance records for each business day if (attendanceCreates.length > 0) { diff --git a/src/routes/admin/offers-pdf.ts b/src/routes/admin/offers-pdf.ts index 183b3d6..37a4dd6 100644 --- a/src/routes/admin/offers-pdf.ts +++ b/src/routes/admin/offers-pdf.ts @@ -1,12 +1,15 @@ import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requirePermission } from "../../middleware/auth"; +import { localDateCzStr } from "../../utils/date"; +import { nasOffersManager } from "../../services/nas-offers-manager"; +import { htmlToPdf } from "../../utils/html-to-pdf"; function formatDate(date: Date | string | null | undefined): string { if (!date) return ""; const d = new Date(date); if (isNaN(d.getTime())) return String(date); - return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`; + return localDateCzStr(d); } /** Format number with comma decimal separator and non-breaking space thousands separator */ @@ -53,7 +56,6 @@ function cleanQuillHtml(html: string | null | undefined): string { if (!html) return ""; const allowedTags = "