commit 2cf748fa36540a08474b214047c29787364d14b8 Author: karim gabriele varano Date: Sat May 9 01:53:15 2026 +0200 Initial commit — RAPPORT App (Stand Mai 2026) diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..245445d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Claude Code +.claude/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100755 index 0000000..a36934d --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/eslint.config.js b/eslint.config.js new file mode 100755 index 0000000..ea36dd3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100755 index 0000000..12ab3c7 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + rapportv01 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..8a5f1c1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2703 @@ +{ + "name": "rapportv01", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rapportv01", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/cli": "^2.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "vite": "^8.0.10" + } + }, + "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==", + "dev": true, + "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/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "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" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.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==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "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": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..673246b --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "rapportv01", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/cli": "^2.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "vite": "^8.0.10" + } +} diff --git a/public/erste-schritte.html b/public/erste-schritte.html new file mode 100755 index 0000000..2271495 --- /dev/null +++ b/public/erste-schritte.html @@ -0,0 +1,179 @@ + + + + + + Erste Schritte — Rapport + + + + + + + + + +

Erste Schritte

+

Rapport ist eine lokale Studio-Administrationssoftware für Architekturbüros. Alle Daten bleiben auf deinem Gerät — kein Server, kein Login.

+ +
+ +
+
1
+
+
Einstellungen konfigurieren
+
Bevor du beginnst, trage die Stammdaten deines Studios ein. Diese erscheinen auf allen Dokumenten und Rechnungen.
+
+
Studio-NameErscheint in der Sidebar und auf Druckdokumenten
+
IBANFür den QR-Einzahlungsschein auf Rechnungen (QR-IBAN empfohlen)
+
MwSt-SatzStandardmässig 8.1 %
+
StundensätzePro Rolle, werden für Honorarberechnungen verwendet
+
+
+
+ +
+
2
+
+
Ersten Kunden anlegen
+
Unter Personen kannst du Kunden und Kontakte verwalten. Kunden werden mit Projekten und Rechnungen verknüpft. Name und Adresse sind für Dokumente erforderlich.
+
+
+ +
+
3
+
+
Mitarbeiter erfassen
+
Unter Mitarbeiter legst du dein Team an. Wichtig sind Pensum, Wochenstunden und Eintrittsdatum — diese fliessen in die Zeiterfassung und Lohnabrechnung ein.
+
+
+ +
+
4
+
+
Erstes Projekt erstellen
+
Unter Projekte erfasst du dein Bauvorhaben. Vergib eine Projektnummer, wähle den Auftraggeber und setze den Status. Optional kannst du eine Offerte verknüpfen, um das Stundenbudget abzuleiten.
+
+
SIA-PhasenAktiviere nur die für das Projekt relevanten Phasen
+
AbrechnungsartStundenbasis oder Pauschal
+
+
+
+ +
+
5
+
+
Zeit erfassen
+
Unter Zeiterfassung buchst du Stunden auf Projekte und Phasen. Die Wochenansicht erlaubt das visuelle Planen per Drag & Drop. Der Monatssaldo zeigt jederzeit Stand bis heute.
+
+
+ +
+
6
+
+
Erste Rechnung stellen
+
Unter Rechnungen erstellst du Rechnungen mit Positionen, MwSt und QR-Einzahlungsschein. Rechnungen lassen sich als PDF drucken und mit einem Status versehen (Entwurf → Gesendet → Bezahlt).
+
+
+ +
+ +
+
Datenspeicherung
+

Alle Daten werden ausschliesslich lokal im Browser gespeichert (localStorage). Es gibt keinen Server, keine Cloud, keine Synchronisation. Erstelle regelmässig ein Backup über Einstellungen → Datenbank exportieren.

+
+ +
+ Lokal gespeichert + Kein Login erforderlich + SIA 102 Honorare + QR-Rechnung + Lohnabrechnung + PDF-Export + Zeiterfassung + AGPL-3.0 +
+ + + + + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100755 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100755 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100755 index 0000000..502406b --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100755 index 0000000..bd50df6 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5295 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "app" +version = "0.2.0" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-log", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.14.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100755 index 0000000..980baf0 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "app" +version = "0.2.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.77.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.6", features = [] } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4" +tauri = { version = "2.10.3", features = [] } +tauri-plugin-log = "2" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100755 index 0000000..795b9b7 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100755 index 0000000..5203780 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "core:webview:allow-print" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100755 index 0000000..77e7d23 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100755 index 0000000..0f7976f Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100755 index 0000000..98fda06 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100755 index 0000000..f35d84f Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100755 index 0000000..1823bb2 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100755 index 0000000..dc2b22c Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100755 index 0000000..0ed3984 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100755 index 0000000..60bf0ea Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100755 index 0000000..c8ca0ad Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100755 index 0000000..8756459 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100755 index 0000000..2c8023c Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100755 index 0000000..2c5e603 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100755 index 0000000..17d142c Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100755 index 0000000..1bf69b8 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100755 index 0000000..06c23c8 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100755 index 0000000..d1756ce Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100755 index 0000000..9c3118c --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,16 @@ +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100755 index 0000000..ad5fe83 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + app_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100755 index 0000000..1c2a3b1 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "RAPPORT PRE-RELEASE", + "version": "0.5.0", + "identifier": "com.karimgabrielevarano.rapport", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:3000", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "windows": [ + { + "title": "RAPPORT PRE-RELEASE", + "width": 1400, + "height": 900, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.css b/src/App.css new file mode 100755 index 0000000..f90339d --- /dev/null +++ b/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100755 index 0000000..71f927f --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,684 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { STORAGE_KEY, NAV_ITEMS, defaultData } from "./constants.js"; +import { migrateDashboardLayout } from "./utils.js"; +import Dashboard from "./views/Dashboard.jsx"; +import { Projects, ProjectDetail } from "./views/Projects.jsx"; +import Time from "./views/Time.jsx"; +import { Spesen, InternalExpenses } from "./views/Spesen.jsx"; +import Protokolle from "./views/Protokolle.jsx"; +import Lieferscheine from "./views/Lieferscheine.jsx"; +import Buchhaltung from "./views/Buchhaltung.jsx"; +import Invoices from "./views/Invoices.jsx"; +import Quotes from "./views/Quotes.jsx"; +import Personen from "./views/Personen.jsx"; +import Letters from "./views/Letters.jsx"; +import Settings from "./views/Settings.jsx"; +import StudioBudget from "./views/StudioBudget.jsx"; +import Loehne from "./views/Loehne.jsx"; +import Mitarbeiter from "./views/Mitarbeiter.jsx"; +import Pinnwand from "./views/Pinnwand.jsx"; +import Dokumente from "./views/Dokumente.jsx"; +import { PrintView } from "./print/PrintComponents.jsx"; +import Login from "./views/Login.jsx"; +import Setup from "./views/Setup.jsx"; +import MigrationScreen from "./views/MigrationScreen.jsx"; + +export default function App() { + const [data, setData] = useState(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + let merged = { ...defaultData, ...parsed, settings: { ...defaultData.settings, ...parsed.settings } }; + + // Migrate: clients[] + contacts[] → persons[] + if (!merged.persons && (merged.clients?.length || merged.contacts?.length)) { + const idMap = {}; + const persons = []; + const usedContactIds = new Set(); + for (const c of merged.clients || []) { + const linked = (merged.contacts || []).find(ct => ct.id === c.linkedContactId); + persons.push({ + ...c, + isAuftraggeber: true, + isPartner: !!linked, + type: c.type || linked?.type || "", + note: c.note || linked?.note || "", + honorarOffers: c.honorarOffers || linked?.honorarOffers || [], + contacts: c.contacts?.length ? c.contacts : (linked?.contacts || []), + linkedContactId: undefined, + linkedClientId: undefined, + }); + if (linked) { usedContactIds.add(linked.id); idMap[linked.id] = c.id; } + } + for (const ct of merged.contacts || []) { + if (usedContactIds.has(ct.id)) continue; + persons.push({ ...ct, isAuftraggeber: false, isPartner: true, linkedClientId: undefined }); + } + const remapProjects = (merged.projects || []).map(p => ({ + ...p, + projectContacts: (p.projectContacts || []).map(pc => ({ ...pc, contactId: idMap[pc.contactId] || pc.contactId })), + })); + const remapProtocols = (merged.protocols || []).map(p => ({ + ...p, + entries: (p.entries || []).map(e => ({ ...e, assignee: e.assignee ? (idMap[e.assignee] || e.assignee) : e.assignee })), + })); + merged = { ...merged, persons, projects: remapProjects, protocols: remapProtocols, clients: undefined, contacts: undefined }; + } + + // Migrate: projects linked to SIA/manual quotes should be pauschal (not stundensatz) + const allQuotes = merged.quotes || []; + const projects = (merged.projects || []).map(p => { + if ((p.billingType || p.type || "stundensatz") === "stundensatz" && (p.linkedQuotes || []).length > 0) { + const linkedQs = (p.linkedQuotes || []).map(lq => allQuotes.find(q => q.id === lq.quoteId)).filter(Boolean); + if (linkedQs.some(q => q.mode === "sia" || q.mode === "manual")) { + return { ...p, billingType: "pauschal", budget: p.budget || p.budgetAmount || 0 }; + } + } + return p; + }); + // Migrate: add r-projektleiter if missing, seed dashboardTemplateId from defaultData + const roleDefMap = (defaultData.appRoles || []).reduce((acc, r) => { acc[r.id] = r; return acc; }, {}); + const roles = (merged.appRoles || defaultData.appRoles).map(r => ({ + ...r, + dashboardTemplateId: r.dashboardTemplateId || roleDefMap[r.id]?.dashboardTemplateId || null, + permissions: (() => { + let perms = r.permissions; + if (perms && r.id === "r-projektleiter" && !perms.includes("mitarbeiter")) perms = [...perms, "mitarbeiter"]; + if (perms && !perms.includes("settings")) perms = [...perms, "settings"]; + return perms; + })(), + })); + if (!roles.find(r => r.id === "r-projektleiter") && roleDefMap["r-projektleiter"]) { + const adminIdx = roles.findIndex(r => r.id === "r-admin"); + roles.splice(adminIdx + 1, 0, roleDefMap["r-projektleiter"]); + } + // Migrate user-level dashboardWidgets to Row[] format + const users = (merged.users || []).map(u => ({ + ...u, + dashboardWidgets: u.dashboardWidgets ? migrateDashboardLayout(u.dashboardWidgets) : undefined, + })); + // Ensure dashboardTemplates exist (old data won't have them) + const dashboardTemplates = merged.dashboardTemplates?.length ? merged.dashboardTemplates : defaultData.dashboardTemplates; + return { ...merged, projects, appRoles: roles, users, dashboardTemplates }; + } + } catch {} + return defaultData; + }); + const [isNewInstall] = useState(() => !localStorage.getItem(STORAGE_KEY)); + const [currentUser, setCurrentUser] = useState(() => { + try { return JSON.parse(sessionStorage.getItem("rapport_user")) || null; } catch { return null; } + }); + const handleLogin = (user) => { + sessionStorage.setItem("rapport_user", JSON.stringify(user)); + setCurrentUser(user); + }; + const handleLogout = () => { + sessionStorage.removeItem("rapport_user"); + setCurrentUser(null); + }; + const handleSetupComplete = (newData) => { + localStorage.setItem("rapport_v0_5_migrated", "1"); + save(newData); + const adminUser = (newData.users || []).find(u => u.role === "admin"); + if (adminUser) handleLogin(adminUser); + }; + const userPermissions = (() => { + if (!currentUser || currentUser.role === "admin") return null; + const role = (data.appRoles || []).find(r => r.id === currentUser.appRoleId); + if (role) return role.permissions; // null = alle + return currentUser.permissions || null; // Fallback für alte Einträge ohne Rolle + })(); + const currentUserRecord = (data.users || []).find(u => u.id === currentUser?.id); + const userInitials = (() => { + const parts = ((currentUser?.displayName || currentUser?.username) || "").trim().split(/\s+/).filter(Boolean); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return (parts[0] || "?")[0].toUpperCase(); + })(); + const visibleNavItems = userPermissions === null ? NAV_ITEMS : NAV_ITEMS.map(item => { + if (item.children) { + const ch = item.children.filter(c => userPermissions.includes(c.id)); + return ch.length > 0 ? { ...item, children: ch } : null; + } + return userPermissions.includes(item.id) ? item : null; + }).filter(Boolean); + const allAccessibleViews = visibleNavItems.flatMap(item => item.children ? item.children.map(c => c.id) : [item.id]); + const [view, setView] = useState(() => { + if (!userPermissions) return "dashboard"; + return userPermissions.includes("dashboard") ? "dashboard" : (userPermissions[0] || "dashboard"); + }); + const navHistRef = useRef([view]); + const navPosRef = useRef(0); + const [navCanBack, setNavCanBack] = useState(false); + const [navCanForward, setNavCanForward] = useState(false); + + const navigate = (newView) => { + const pos = navPosRef.current; + const hist = navHistRef.current; + if (hist[pos] === newView) return; + const trimmed = [...hist.slice(0, pos + 1), newView]; + navHistRef.current = trimmed; + navPosRef.current = trimmed.length - 1; + setView(newView); + setNavCanBack(true); + setNavCanForward(false); + }; + const goBack = () => { + const pos = navPosRef.current; + if (pos <= 0) return; + navPosRef.current = pos - 1; + setView(navHistRef.current[pos - 1]); + setNavCanBack(pos - 1 > 0); + setNavCanForward(true); + }; + const goForward = () => { + const pos = navPosRef.current; + const hist = navHistRef.current; + if (pos >= hist.length - 1) return; + navPosRef.current = pos + 1; + setView(hist[pos + 1]); + setNavCanBack(true); + setNavCanForward(pos + 1 < hist.length - 1); + }; + + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [modal, setModal] = useState(null); + const [printContent, setPrintContent] = useState(null); + const [darkMode, setDarkMode] = useState(() => localStorage.getItem("rapport_dark") === "1"); + const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.5"); + const [changelogVersion, setChangelogVersion] = useState("0.5"); + const [navOpen, setNavOpen] = useState(false); + const [expandedNav, setExpandedNav] = useState(new Set(["buchhaltung"])); + const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("rapport_sidebar_collapsed") === "1"); + const [isMobile, setIsMobile] = useState(() => window.matchMedia("(max-width: 768px)").matches); + useEffect(() => { + const mq = window.matchMedia("(max-width: 768px)"); + const handler = (e) => setIsMobile(e.matches); + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, []); + const collapsed = sidebarCollapsed && !isMobile; + const [uiZoom, setUiZoom] = useState(() => parseFloat(localStorage.getItem("rapport_zoom") || "1")); + + // Persist dark mode + useEffect(() => { localStorage.setItem("rapport_dark", darkMode ? "1" : "0"); }, [darkMode]); + useEffect(() => { localStorage.setItem("rapport_sidebar_collapsed", sidebarCollapsed ? "1" : "0"); }, [sidebarCollapsed]); + + // UI-Zoom: nur main-content, Sidebar bleibt unberührt + useEffect(() => { + localStorage.setItem("rapport_zoom", String(uiZoom)); + // Tauri: native WebView-Zoom entfernen falls gesetzt (Sidebar-Problem) + if (window.__TAURI_INTERNALS__) { + import("@tauri-apps/api/webviewWindow") + .then(({ getCurrentWebviewWindow }) => getCurrentWebviewWindow().setZoom(1)) + .catch(() => {}); + } + }, [uiZoom]); + + const zoomStep = 0.05; + const zoomIn = () => setUiZoom(z => Math.min(1.5, Math.round((z + zoomStep) * 100) / 100)); + const zoomOut = () => setUiZoom(z => Math.max(0.5, Math.round((z - zoomStep) * 100) / 100)); + + + // Navigation zu Protokoll von Projekt aus + useEffect(() => { + const handler = (e) => { + navigate("protokolle"); + window.__openProtokoll = e.detail?.id || null; + }; + window.addEventListener("openProtokoll", handler); + return () => window.removeEventListener("openProtokoll", handler); + }, []); + + // Auto-expand parent when navigating to a child + useEffect(() => { + NAV_ITEMS.forEach(item => { + if (item.children?.some(c => c.id === view)) { + setExpandedNav(prev => { const next = new Set(prev); next.add(item.id); return next; }); + } + }); + }, [view]); + + const save = useCallback((newData) => { + setData(newData); + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(newData)); } catch {} + }, []); + + const update = useCallback((key, value) => { + save({ ...data, [key]: value }); + }, [data, save]); + + // Auto-überfällig + useEffect(() => { + const today = new Date().toISOString().slice(0, 10); + const updated = data.invoices.map(inv => + inv.status === "gesendet" && inv.dueDate && inv.dueDate < today + ? { ...inv, status: "überfällig" } : inv + ); + if (updated.some((inv, i) => inv.status !== data.invoices[i].status)) + save({ ...data, invoices: updated }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (isNewInstall && !data.settings.setupCompleted) { + return ; + } + + if (!localStorage.getItem("rapport_v0_5_migrated")) { + return ; + } + + if (!currentUser) { + return ; + } + + if (printContent) { + return setPrintContent(null)} settings={data.settings} />; + } + + return ( +
+ + + {/* Mobile Header */} +
+ +
RAPPORT
+
+ + {/* Sidebar */} +
+
+ {collapsed ? ( + + ) : ( + <> + + + + )} +
+ + {/* ── Vor / Zurück ── */} +
+ {[["goBack", "‹", goBack, navCanBack, "Zurück"], ["goForward", "›", goForward, navCanForward, "Vorwärts"]].map(([key, ch, fn, enabled, title]) => ( + + ))} +
+ + {!collapsed &&
+
+ + AGPL-3.0 ↗ + WEBSITE ↗ +
+
} + + {/* Benutzer + Logout + Theme */} +
+ {!collapsed && ( +
+
+ {currentUserRecord?.avatar + ? + : userInitials} +
+
+
+ {currentUser.displayName || currentUser.username} +
+ {currentUser.role === "admin" &&
ADMIN
} +
+
+ )} +
+ + +
+
+
+ + {/* Main */} +
+ {view === "dashboard" && } + {view === "pinnwand" && } + {view === "projects" && !selectedProjectId && } + {view === "projects" && selectedProjectId && setSelectedProjectId(null)} setPrintContent={setPrintContent} modal={modal} setModal={setModal} currentUser={currentUser} />} + {view === "time" &&
+ + {showChangelog && (() => { + const CHANGELOGS = { + "0.5": { + items: [ + ["Anmeldesystem", "Benutzerverwaltung mit Rollen und Passwörtern. Jeder Mitarbeiter erhält einen eigenen Login. Berechtigungen steuern, welche Module sichtbar sind."], + ["Migration bestehender Daten", "Beim ersten Start mit einer bestehenden Datenbank erscheint ein Migrations-Assistent: Daten sichern, Admin-Konto einrichten — alle bisherigen Inhalte bleiben erhalten."], + ["Rechnungstypen: Akonto / Teilrechnung / Schluss", "Klare Trennung der Rechnungsarten mit unterschiedlicher steuerlicher Behandlung. Akonto ist erst bei der Schlussrechnung steuerrelevant; Teilrechnungen sind sofort wirksam."], + ["Neuer Rechnungsdialog", "Zweistufige Auswahl: zuerst Art der Rechnung (Akonto / Teilrechnung / Schlussrechnung), dann Berechnungsmethode (Stunden / % vom Budget / Fixer Betrag / SIA-Phase)."], + ["Akonto & Teilrechnung nach SIA-Phase", "Für Pauschal-Projekte können einzelne SIA-Phasen direkt verrechnet werden — bereits verrechnete Phasen werden als solche markiert."], + ["Aufwandsrechnungen erweitert", "Stundenprojekte unterstützen jetzt Akonto (Stunden bis heute, %, Fixbetrag) und Teilrechnung (Stunden auswählen, %, Fixbetrag, SIA-Phase)."], + ["Buchhaltung: Akonto-MwSt getrennt", "Akonto-Rechnungen werden in der Buchhaltung separat ausgewiesen — die MwSt wird erst bei der Schlussrechnung als steuerrelevant gezählt."], + ["Mitarbeiter: Intern & Absenzen", "Umbenennung zu «Intern / Absenzen». Neue Jahresübersicht mit Monatsvergleich und Vorjahr, Absenzkategorien-Matrix, sowie Auswertung interner Stunden ohne Projektbezug."], + ], + }, + "0.4": { + items: [ + ["Material Design 3", "Sidebar mit einklappbarem Icon-Modus und Material Symbols Rounded Icons. Buttons als Pill, Inputs und Cards mit mehr Radius, Tags als Chips, Modals mit Backdrop-Blur."], + ["Interne Projektbeteiligung", "Mitarbeitende können direkt im Projekt zugewiesen werden. In Protokollen erscheinen unter «Intern» nur noch die zugewiesenen Personen."], + ["Offerten im Budget", "Neben Rechnungen können jetzt auch Offerten in die Einnahmen-Planung des Bürobudgets einbezogen werden."], + ["Kategorien direkt auf der Seite", "Spesenarten und Ausgaben-Kategorien werden neu direkt auf der jeweiligen Seite verwaltet, nicht mehr in den Einstellungen."], + ], + }, + "0.3": { + items: [ + ["Visuelles Redesign", "Sidebar schwebt als eigenständiges Panel mit Radius und Schatten. Cards haben mehr Tiefe. Header kompakter, Abstände überarbeitet, Menüpunkte in Kapitälchen. Studio-Name in der Sidebar."], + ["Zoom", "Der UI-Zoom betrifft nur den Hauptinhalt — die Sidebar bleibt immer gleich gross und unverzerrt."], + ], + }, + "0.2": { + items: [ + ["Neue Module", "Lohnabrechnung, Bürobudget, Protokolle, Lieferscheine und Spesen (Mitarbeiter & intern) hinzugefügt."], + ["Zeiterfassung Wochenansicht", "Visuelles Zeitraster mit Drag & Drop — Einträge verschieben und skalieren, Wechsel zwischen Tag-, Wochen- und Monatsansicht."], + ["Ferienplanung", "Ferienanträge stellen, genehmigen und zurückziehen. Absenzverwaltung ausgebaut."], + ["Teilzeit", "Lohn wird proportional zum Pensum berechnet, pro Monat überschreibbar."], + ], + }, + "0.1": { + items: [ + ["Erster Release", "Projekte, Kunden, Mitarbeiterverwaltung, Zeiterfassung."], + ["Rechnungen & Offerten", "Rechnungen mit QR-Einzahlungsschein, Offerten nach SIA 102, manuell oder frei — konvertierbar zur Rechnung."], + ["Einstellungen", "Studio-Stammdaten, Stundensätze, Rollen, MwSt und Projektnummern-Format."], + ], + }, + }; + const versions = Object.keys(CHANGELOGS); + const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.5"]; + return ( +
+
+
+
CHANGELOG
+
+
Alpha {changelogVersion}
+
+
+ {versions.map(v => ( + + ))} +
+
+
+ {current.items.map(([title, desc]) => ( +
+
+
+
{title}
+
{desc}
+
+
+ ))} +
+
+ +
+
+
+ ); + })()} +
+ ); +} diff --git a/src/App.jsx.bak b/src/App.jsx.bak new file mode 100755 index 0000000..5b15ac2 --- /dev/null +++ b/src/App.jsx.bak @@ -0,0 +1,451 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { STORAGE_KEY, NAV_ITEMS, defaultData } from "./constants.js"; +import Dashboard from "./views/Dashboard.jsx"; +import { Projects, ProjectDetail } from "./views/Projects.jsx"; +import Time from "./views/Time.jsx"; +import { Spesen, InternalExpenses } from "./views/Spesen.jsx"; +import Protokolle from "./views/Protokolle.jsx"; +import Lieferscheine from "./views/Lieferscheine.jsx"; +import Buchhaltung from "./views/Buchhaltung.jsx"; +import Invoices from "./views/Invoices.jsx"; +import Quotes, { ConvertQuoteModal } from "./views/Quotes.jsx"; +import Personen from "./views/Personen.jsx"; +import Letters from "./views/Letters.jsx"; +import Settings from "./views/Settings.jsx"; +import StudioBudget from "./views/StudioBudget.jsx"; +import Loehne from "./views/Loehne.jsx"; +import Mitarbeiter from "./views/Mitarbeiter.jsx"; +import Dokumente from "./views/Dokumente.jsx"; +import { PrintView } from "./print/PrintComponents.jsx"; + +export default function App() { + const [data, setData] = useState(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + let merged = { ...defaultData, ...parsed, settings: { ...defaultData.settings, ...parsed.settings } }; + + // Migrate: clients[] + contacts[] → persons[] + if (!merged.persons && (merged.clients?.length || merged.contacts?.length)) { + const idMap = {}; + const persons = []; + const usedContactIds = new Set(); + for (const c of merged.clients || []) { + const linked = (merged.contacts || []).find(ct => ct.id === c.linkedContactId); + persons.push({ + ...c, + isAuftraggeber: true, + isPartner: !!linked, + type: c.type || linked?.type || "", + note: c.note || linked?.note || "", + honorarOffers: c.honorarOffers || linked?.honorarOffers || [], + contacts: c.contacts?.length ? c.contacts : (linked?.contacts || []), + linkedContactId: undefined, + linkedClientId: undefined, + }); + if (linked) { usedContactIds.add(linked.id); idMap[linked.id] = c.id; } + } + for (const ct of merged.contacts || []) { + if (usedContactIds.has(ct.id)) continue; + persons.push({ ...ct, isAuftraggeber: false, isPartner: true, linkedClientId: undefined }); + } + const remapProjects = (merged.projects || []).map(p => ({ + ...p, + projectContacts: (p.projectContacts || []).map(pc => ({ ...pc, contactId: idMap[pc.contactId] || pc.contactId })), + })); + const remapProtocols = (merged.protocols || []).map(p => ({ + ...p, + entries: (p.entries || []).map(e => ({ ...e, assignee: e.assignee ? (idMap[e.assignee] || e.assignee) : e.assignee })), + })); + merged = { ...merged, persons, projects: remapProjects, protocols: remapProtocols, clients: undefined, contacts: undefined }; + } + + // Migrate: projects linked to SIA/manual quotes should be pauschal (not stundensatz) + const allQuotes = merged.quotes || []; + const projects = (merged.projects || []).map(p => { + if ((p.billingType || p.type || "stundensatz") === "stundensatz" && (p.linkedQuotes || []).length > 0) { + const linkedQs = (p.linkedQuotes || []).map(lq => allQuotes.find(q => q.id === lq.quoteId)).filter(Boolean); + if (linkedQs.some(q => q.mode === "sia" || q.mode === "manual")) { + return { ...p, billingType: "pauschal", budget: p.budget || p.budgetAmount || 0 }; + } + } + return p; + }); + return { ...merged, projects }; + } + } catch {} + return defaultData; + }); + const [view, setView] = useState("dashboard"); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [modal, setModal] = useState(null); + const [printContent, setPrintContent] = useState(null); + const [darkMode, setDarkMode] = useState(() => localStorage.getItem("rapport_dark") === "1"); + const [whatsNewDismissed, setWhatsNewDismissed] = useState(() => localStorage.getItem("rapport_whats_new_v0_2_0") === "1"); + const [navOpen, setNavOpen] = useState(false); + const [expandedNav, setExpandedNav] = useState(new Set(["buchhaltung"])); + const [uiZoom, setUiZoom] = useState(() => parseFloat(localStorage.getItem("rapport_zoom") || "1")); + + // Persist dark mode + useEffect(() => { localStorage.setItem("rapport_dark", darkMode ? "1" : "0"); }, [darkMode]); + + // UI-Zoom via Tauri native API (kein CSS-Balken-Problem), Fallback auf document.body.style.zoom + useEffect(() => { + localStorage.setItem("rapport_zoom", String(uiZoom)); + const apply = async () => { + try { + if (window.__TAURI_INTERNALS__) { + const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + await getCurrentWebviewWindow().setZoom(uiZoom); + } else { + document.body.style.zoom = uiZoom === 1 ? "" : String(uiZoom); + } + } catch { + document.body.style.zoom = uiZoom === 1 ? "" : String(uiZoom); + } + }; + apply(); + }, [uiZoom]); + + const zoomStep = 0.05; + const zoomIn = () => setUiZoom(z => Math.min(1.5, Math.round((z + zoomStep) * 100) / 100)); + const zoomOut = () => setUiZoom(z => Math.max(0.5, Math.round((z - zoomStep) * 100) / 100)); + + + // Navigation zu Protokoll von Projekt aus + useEffect(() => { + const handler = (e) => { + setView("protokolle"); + window.__openProtokoll = e.detail?.id || null; + }; + window.addEventListener("openProtokoll", handler); + return () => window.removeEventListener("openProtokoll", handler); + }, []); + + // Auto-expand parent when navigating to a child + useEffect(() => { + NAV_ITEMS.forEach(item => { + if (item.children?.some(c => c.id === view)) { + setExpandedNav(prev => { const next = new Set(prev); next.add(item.id); return next; }); + } + }); + }, [view]); + + const save = useCallback((newData) => { + setData(newData); + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(newData)); } catch {} + }, []); + + const update = useCallback((key, value) => { + save({ ...data, [key]: value }); + }, [data, save]); + + // Auto-überfällig + useEffect(() => { + const today = new Date().toISOString().slice(0, 10); + const updated = data.invoices.map(inv => + inv.status === "gesendet" && inv.dueDate && inv.dueDate < today + ? { ...inv, status: "überfällig" } : inv + ); + if (updated.some((inv, i) => inv.status !== data.invoices[i].status)) + save({ ...data, invoices: updated }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (printContent) { + return setPrintContent(null)} settings={data.settings} />; + } + + return ( +
+ + + {/* Mobile Header */} +
+ +
RAPPORT
+
{NAV_ITEMS.find(n => n.id === view || (n.children||[]).some(c=>c.id===view))?.label || ""}
+
+ + {/* Sidebar */} +
+
+
+
RAPPORT
+
STUDIO ADMINISTRATION
+
+ +
+ + {/* Zoom-Steuerung */} +
+ + + +
+ +
+
ALPHA 0.2.1
+
+
+
+
AGPL-3.0
+
+ +
+
+
+
+ + {/* Main */} +
+ {view === "dashboard" && } + {view === "projects" && !selectedProjectId && } + {view === "projects" && selectedProjectId && setSelectedProjectId(null)} setPrintContent={setPrintContent} modal={modal} setModal={setModal} />} + {view === "time" &&
+ + {!whatsNewDismissed && ( +
+
+
+
NEUERUNGEN
+
Version 0.2.0
+
+
+ {[ + ["Wochenansicht", "Zeiterfassung mit Kalender-Ansicht, Einträge per Drag & Drop verschieben und skalieren"], + ["Tagessaldo", "Wochenkopf zeigt Soll/Ist-Differenz pro Tag — wie im Monatskalender"], + ["Eintrag kopieren", "Rechtsklick auf einen Eintrag → auf morgen duplizieren"], + ["Teilzeit & Pensum", "Lohn wird automatisch gemäss Pensum berechnet, pro Monat überschreibbar"], + ["Ferien zurückziehen", "Beantragte und genehmigte Ferien können jederzeit zurückgezogen werden"], + ].map(([title, desc]) => ( +
+
+
+
{title}
+
{desc}
+
+
+ ))} +
+
+
+
Datenbank-Neustart empfohlen
+
Aufgrund umfangreicher Neuerungen und fehlerhafter Teile im letzten Build wird empfohlen, unter Einstellungen → Datenbank löschen neu zu starten.
+
+
+
+
+ +
+
+
+ )} +
+ ); +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100755 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100755 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100755 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/UI.jsx b/src/components/UI.jsx new file mode 100755 index 0000000..4a7dcf9 --- /dev/null +++ b/src/components/UI.jsx @@ -0,0 +1,469 @@ +import React, { useState, useEffect, useRef } from "react"; +import { STATUS_COLORS, STATUS_BG } from "../constants.js"; + +export function StatusBadge({ status }) { + const color = STATUS_COLORS[status] || "#888"; + const bg = STATUS_BG[status] || "#f5f5f5"; + return ( + {status} + ); +} + +export function StatusSelect({ value, options, onChange }) { + const color = STATUS_COLORS[value] || "#888"; + const bg = STATUS_BG[value] || "#f5f5f5"; + return ( + + ); +} + +export function Header({ title, action }) { + return ( +
+
+

{title}

+ {action} +
+
+ ); +} + +export function FormField({ label, children }) { + return ( +
+ + {children} +
+ ); +} + +export function Modal({ title, onClose, onSave, saveLabel, hideSave, children, wide, overflow }) { + return ( +
e.target === e.currentTarget && onClose()}> +
+

{title}

+ {children} +
+ + {!hideSave && } +
+
+
+ ); +} + +export function StudioLogo({ settings, size = 24 }) { + if (settings.logo) { + const h = settings.logoSize || 60; + return {settings.name}; + } + return
{settings.name}
; +} + +export function useConfirm() { + const [pending, setPending] = useState(null); + const askConfirm = (msg, confirmLabel = "Löschen") => + new Promise(resolve => setPending({ msg, confirmLabel, resolve })); + const ConfirmModalEl = pending ? ( +
+
+
{pending.msg}
+
+ + +
+
+
+ ) : null; + return { askConfirm, ConfirmModalEl }; +} + +export function DateInput({ value, onChange, style, ...props }) { + const toDE = (iso) => { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; + return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; + }; + const toISO = (de) => { + const m = de.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); + if (!m) return null; + const iso = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; + return isNaN(new Date(iso).getTime()) ? null : iso; + }; + const [text, setText] = React.useState(() => toDE(value)); + React.useEffect(() => { const de = toDE(value); if (de !== text) setText(de); }, [value]); + const handleChange = (e) => { + const digits = e.target.value.replace(/\D/g, "").slice(0, 8); + let fmt = ""; + if (digits.length <= 2) fmt = digits; + else if (digits.length <= 4) fmt = `${digits.slice(0, 2)}.${digits.slice(2)}`; + else fmt = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; + setText(fmt); + const iso = toISO(fmt); + if (iso) onChange({ target: { value: iso } }); + }; + return ( + { if (!toISO(text) && value) setText(toDE(value)); }} + placeholder="TT.MM.JJJJ" + style={style} + {...props} + /> + ); +} + +export function RichEditor({ value, onChange, minHeight = 420, compact = false }) { + const editorRef = React.useRef(null); + const isInternalChange = React.useRef(false); + const lastValue = React.useRef(value); + const savedRange = React.useRef(null); + const colorInputRef = React.useRef(null); + const [colorValue, setColorValue] = React.useState("#000000"); + + // Only push content into DOM when value changes from outside (template load) + useEffect(() => { + if (!editorRef.current) return; + if (value !== lastValue.current && !isInternalChange.current) { + editorRef.current.innerHTML = value; + lastValue.current = value; + } + isInternalChange.current = false; + }, [value]); + + const saveSelection = () => { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0 && editorRef.current?.contains(sel.anchorNode)) { + savedRange.current = sel.getRangeAt(0).cloneRange(); + } + }; + + const restoreSelection = () => { + editorRef.current?.focus(); + if (!savedRange.current) { + // Place cursor at end if no saved range + const range = document.createRange(); + range.selectNodeContents(editorRef.current); + range.collapse(false); + const sel = window.getSelection(); + if (sel) { sel.removeAllRanges(); sel.addRange(range); } + return; + } + const sel = window.getSelection(); + if (sel) { sel.removeAllRanges(); sel.addRange(savedRange.current); } + }; + + const exec = (cmd, val = null) => { + restoreSelection(); + document.execCommand(cmd, false, val); + saveSelection(); + // Sync content after command + isInternalChange.current = true; + const html = editorRef.current?.innerHTML || ""; + lastValue.current = html; + onChange(html); + }; + + + + const TB = ({ cmd, val, title, style: s, children }) => ( + + ); + const Sep = () =>
; + + return ( +
+
+ B + I + U + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Font size */} + + + + + {/* Color */} + + { setColorValue(e.target.value); exec("foreColor", e.target.value); }} + style={{ position: "absolute", width: 0, height: 0, opacity: 0, pointerEvents: "none" }} + /> +
+ + {/* Editor */} +
{ + isInternalChange.current = true; + const html = editorRef.current?.innerHTML || ""; + lastValue.current = html; + onChange(html); + }} + onKeyUp={saveSelection} + onMouseUp={saveSelection} + onBlur={saveSelection} + onPaste={e => { + e.preventDefault(); + const text = e.clipboardData.getData("text/plain"); + restoreSelection(); + document.execCommand("insertText", false, text); + saveSelection(); + }} + style={{ + minHeight, padding: compact ? "8px 10px" : "20px 24px", outline: "none", + fontSize: compact ? 12 : 13, lineHeight: 1.8, color: "var(--text)", + background: "var(--surface)", + fontFamily: "'DM Mono', 'Courier New', monospace", + }} + /> +
+ ); +} + +export function CalendarPopup({ value, onChange, onClose, align = "left", showClear = true }) { + const [viewYM, setViewYM] = useState(() => { + const d = value ? new Date(value + "T00:00:00") : new Date(); + return { year: d.getFullYear(), month: d.getMonth() }; + }); + const todayStr = new Date().toISOString().slice(0, 10); + const { year, month } = viewYM; + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const startWeekday = (firstDay.getDay() + 6) % 7; + const cells = []; + for (let i = 0; i < startWeekday; i++) cells.push(null); + for (let d = 1; d <= lastDay.getDate(); d++) { + cells.push(`${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`); + } + const prevMonth = () => { const d = new Date(year, month - 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; + const nextMonth = () => { const d = new Date(year, month + 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; + const select = (ds) => { onChange(ds); onClose(); }; + + return ( +
e.stopPropagation()} + > +
+ +
+ {firstDay.toLocaleDateString("de-CH", { month: "long", year: "numeric" })} +
+ +
+
+ {["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].map(d => ( +
{d}
+ ))} +
+
+ {cells.map((ds, i) => { + if (!ds) return
; + const dow = new Date(ds + "T00:00:00").getDay(); + const isWeekend = dow === 0 || dow === 6; + const isSelected = ds === value; + const isToday = ds === todayStr; + return ( + + ); + })} +
+ {showClear && value && ( +
+ +
+ )} +
+ ); +} + +export function DatePicker({ value, onChange, style, placeholder, align = "left", ...props }) { + const [open, setOpen] = useState(false); + const wrapRef = useRef(null); + const toDE = (iso) => { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; + return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; + }; + useEffect(() => { + if (!open) return; + const close = (e) => { if (!wrapRef.current?.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, [open]); + return ( +
+ setOpen(o => !o)} + placeholder={placeholder || "TT.MM.JJJJ"} + style={{ cursor: "pointer", ...style }} + {...props} + /> + {open && ( + onChange({ target: { value: ds } })} + onClose={() => setOpen(false)} + align={align} + /> + )} +
+ ); +} + +export function NavArrows({ onPrev, onNext, disabledNext = false }) { + const btn = (onClick, disabled, children) => ( + + ); + return ( +
+ {btn(onPrev, false, "‹")} +
+ {btn(onNext, disabledNext, "›")} +
+ ); +} + +export function useCalendarNav() { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useEffect(() => { + if (!open) return; + const close = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, [open]); + return { open, setOpen, ref }; +} diff --git a/src/components/UI.jsx.bak b/src/components/UI.jsx.bak new file mode 100755 index 0000000..71786f0 --- /dev/null +++ b/src/components/UI.jsx.bak @@ -0,0 +1,444 @@ +import React, { useState, useEffect, useRef } from "react"; +import { STATUS_COLORS, STATUS_BG } from "../constants.js"; + +export function StatusBadge({ status }) { + const color = STATUS_COLORS[status] || "#888"; + const bg = STATUS_BG[status] || "#f5f5f5"; + return ( + {status} + ); +} + +export function StatusSelect({ value, options, onChange }) { + const color = STATUS_COLORS[value] || "#888"; + const bg = STATUS_BG[value] || "#f5f5f5"; + return ( + + ); +} + +export function Header({ title, action }) { + return ( +
+

{title}

+ {action} +
+ ); +} + +export function FormField({ label, children }) { + return ( +
+ + {children} +
+ ); +} + +export function Modal({ title, onClose, onSave, saveLabel, hideSave, children, wide, overflow }) { + return ( +
e.target === e.currentTarget && onClose()}> +
+

{title}

+ {children} +
+ + {!hideSave && } +
+
+
+ ); +} + +export function StudioLogo({ settings, size = 24 }) { + if (settings.logo) { + const h = settings.logoSize || 60; + return {settings.name}; + } + return
{settings.name}
; +} + +export function useConfirm() { + const [pending, setPending] = useState(null); + const askConfirm = (msg, confirmLabel = "Löschen") => + new Promise(resolve => setPending({ msg, confirmLabel, resolve })); + const ConfirmModalEl = pending ? ( +
+
+
{pending.msg}
+
+ + +
+
+
+ ) : null; + return { askConfirm, ConfirmModalEl }; +} + +export function DateInput({ value, onChange, style, ...props }) { + const toDE = (iso) => { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; + return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; + }; + const toISO = (de) => { + const m = de.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); + if (!m) return null; + const iso = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; + return isNaN(new Date(iso).getTime()) ? null : iso; + }; + const [text, setText] = React.useState(() => toDE(value)); + React.useEffect(() => { const de = toDE(value); if (de !== text) setText(de); }, [value]); + const handleChange = (e) => { + const digits = e.target.value.replace(/\D/g, "").slice(0, 8); + let fmt = ""; + if (digits.length <= 2) fmt = digits; + else if (digits.length <= 4) fmt = `${digits.slice(0, 2)}.${digits.slice(2)}`; + else fmt = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; + setText(fmt); + const iso = toISO(fmt); + if (iso) onChange({ target: { value: iso } }); + }; + return ( + { if (!toISO(text) && value) setText(toDE(value)); }} + placeholder="TT.MM.JJJJ" + style={style} + {...props} + /> + ); +} + +export function RichEditor({ value, onChange, minHeight = 420, compact = false }) { + const editorRef = React.useRef(null); + const isInternalChange = React.useRef(false); + const lastValue = React.useRef(value); + const savedRange = React.useRef(null); + const colorInputRef = React.useRef(null); + const [colorValue, setColorValue] = React.useState("#000000"); + + // Only push content into DOM when value changes from outside (template load) + useEffect(() => { + if (!editorRef.current) return; + if (value !== lastValue.current && !isInternalChange.current) { + editorRef.current.innerHTML = value; + lastValue.current = value; + } + isInternalChange.current = false; + }, [value]); + + const saveSelection = () => { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0 && editorRef.current?.contains(sel.anchorNode)) { + savedRange.current = sel.getRangeAt(0).cloneRange(); + } + }; + + const restoreSelection = () => { + editorRef.current?.focus(); + if (!savedRange.current) { + // Place cursor at end if no saved range + const range = document.createRange(); + range.selectNodeContents(editorRef.current); + range.collapse(false); + const sel = window.getSelection(); + if (sel) { sel.removeAllRanges(); sel.addRange(range); } + return; + } + const sel = window.getSelection(); + if (sel) { sel.removeAllRanges(); sel.addRange(savedRange.current); } + }; + + const exec = (cmd, val = null) => { + restoreSelection(); + document.execCommand(cmd, false, val); + saveSelection(); + // Sync content after command + isInternalChange.current = true; + const html = editorRef.current?.innerHTML || ""; + lastValue.current = html; + onChange(html); + }; + + + + const TB = ({ cmd, val, title, style: s, children }) => ( + + ); + const Sep = () =>
; + + return ( +
+
+ B + I + U + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Font size */} + + + + + {/* Color */} + + { setColorValue(e.target.value); exec("foreColor", e.target.value); }} + style={{ position: "absolute", width: 0, height: 0, opacity: 0, pointerEvents: "none" }} + /> +
+ + {/* Editor */} +
{ + isInternalChange.current = true; + const html = editorRef.current?.innerHTML || ""; + lastValue.current = html; + onChange(html); + }} + onKeyUp={saveSelection} + onMouseUp={saveSelection} + onBlur={saveSelection} + onPaste={e => { + e.preventDefault(); + const text = e.clipboardData.getData("text/plain"); + restoreSelection(); + document.execCommand("insertText", false, text); + saveSelection(); + }} + style={{ + minHeight, padding: compact ? "8px 10px" : "20px 24px", outline: "none", + fontSize: compact ? 12 : 13, lineHeight: 1.8, color: "var(--text)", + background: "var(--surface)", + fontFamily: "'DM Mono', 'Courier New', monospace", + }} + /> +
+ ); +} + +export function CalendarPopup({ value, onChange, onClose, align = "left", showClear = true }) { + const [viewYM, setViewYM] = useState(() => { + const d = value ? new Date(value + "T00:00:00") : new Date(); + return { year: d.getFullYear(), month: d.getMonth() }; + }); + const todayStr = new Date().toISOString().slice(0, 10); + const { year, month } = viewYM; + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const startWeekday = (firstDay.getDay() + 6) % 7; + const cells = []; + for (let i = 0; i < startWeekday; i++) cells.push(null); + for (let d = 1; d <= lastDay.getDate(); d++) { + cells.push(`${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`); + } + const prevMonth = () => { const d = new Date(year, month - 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; + const nextMonth = () => { const d = new Date(year, month + 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; + const select = (ds) => { onChange(ds); onClose(); }; + + return ( +
e.stopPropagation()} + > +
+ +
+ {firstDay.toLocaleDateString("de-CH", { month: "long", year: "numeric" })} +
+ +
+
+ {["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].map(d => ( +
{d}
+ ))} +
+
+ {cells.map((ds, i) => { + if (!ds) return
; + const dow = new Date(ds + "T00:00:00").getDay(); + const isWeekend = dow === 0 || dow === 6; + const isSelected = ds === value; + const isToday = ds === todayStr; + return ( + + ); + })} +
+ {showClear && value && ( +
+ +
+ )} +
+ ); +} + +export function DatePicker({ value, onChange, style, placeholder, align = "left", ...props }) { + const [open, setOpen] = useState(false); + const wrapRef = useRef(null); + const toDE = (iso) => { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; + return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; + }; + useEffect(() => { + if (!open) return; + const close = (e) => { if (!wrapRef.current?.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, [open]); + return ( +
+ setOpen(o => !o)} + placeholder={placeholder || "TT.MM.JJJJ"} + style={{ cursor: "pointer", ...style }} + {...props} + /> + {open && ( + onChange({ target: { value: ds } })} + onClose={() => setOpen(false)} + align={align} + /> + )} +
+ ); +} + +export function useCalendarNav() { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useEffect(() => { + if (!open) return; + const close = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, [open]); + return { open, setOpen, ref }; +} diff --git a/src/constants.js b/src/constants.js new file mode 100755 index 0000000..e7a2325 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,252 @@ +// ─── CONSTANTS ────────────────────────────────────────────────── + +export const STORAGE_KEY = "studio_data_v1"; + +// SIA-Phasen nach SIA 102/103 (Architektur) +export const SIA_PHASES = [ + { id: "11", label: "11 Strategische Planung" }, + { id: "21", label: "21 Vorstudien" }, + { id: "22", label: "22 Machbarkeitsstudie" }, + { id: "31", label: "31 Vorprojekt" }, + { id: "32", label: "32 Bauprojekt" }, + { id: "33", label: "33 Bewilligungsverfahren" }, + { id: "41", label: "41 Ausschreibung" }, + { id: "51", label: "51 Ausführungsprojekt" }, + { id: "52", label: "52 Ausführung" }, + { id: "53", label: "53 Inbetriebnahme / Abschluss" }, +]; + +// SIA 102 Teilleistungen mit Standard-Prozentanteilen +export const SIA_PHASE_WEIGHTS = [ + { id: "31", label: "Vorprojekt", items: [ + { label: "Lösungsstudien / Grobschätzung", pct: 3 }, + { label: "Vorprojekt / Kostenschätzung", pct: 6 }, + ]}, + { id: "32", label: "Bauprojekt", items: [ + { label: "Bauprojekt", pct: 13 }, + { label: "Detailstudien", pct: 4 }, + { label: "Kostenvoranschlag", pct: 4 }, + ]}, + { id: "33", label: "Bewilligungsverfahren", items: [ + { label: "Bewilligungsverfahren", pct: 2.5 }, + ]}, + { id: "41", label: "Ausschreibung", items: [ + { label: "Ausschreibungspläne", pct: 10 }, + { label: "Ausschreibung / Offertvergleich", pct: 8 }, + ]}, + { id: "51", label: "Ausführungsplanung", items: [ + { label: "Ausführungspläne", pct: 15 }, + { label: "Werkverträge", pct: 1 }, + ]}, + { id: "52", label: "Ausführung", items: [ + { label: "Gestalterische Leitung", pct: 6 }, + { label: "Bauleitung / Kostenkontrolle", pct: 23 }, + ]}, + { id: "53", label: "Inbetriebnahme / Abschluss", items: [ + { label: "Inbetriebnahme", pct: 1 }, + { label: "Dokumentation", pct: 1 }, + { label: "Garantiearbeiten", pct: 1.5 }, + { label: "Schlussabrechnung", pct: 1 }, + ]}, +]; + +// Projekt-Typen +export const PROJECT_TYPES = [ + "Wettbewerb", + "Studienauftrag", + "Direktauftrag", + "Machbarkeitsstudie", + "Gutachten", + "Grafik", + "Sonstiges", +]; + +export const EXPENSE_CATEGORIES = [ + "Reise / Fahrt", "Drucken / Reprografie", "Modellbau / Material", + "Büromaterial", "Weiterbildung", + "Unterauftrag / Freelancer", "Verpflegung / Geschäftsessen", + "Sonstiges", +]; + +export const INTERNAL_EXPENSE_CATEGORIES = [ + "Miete / Raumkosten", "Software / Lizenzen", "Hardware / IT", + "Telefon / Internet", "Versicherung", "Steuern / Abgaben", + "Büromaterial", "Marketing / Werbung", "Weiterbildung", + "Unterauftrag / Freelancer", "Bankgebühren", "Sonstiges", +]; + +export const NAV_ITEMS = [ + { id: "dashboard", label: "Übersicht", icon: "grid_view" }, + { id: "pinnwand", label: "Pinnwand", icon: "campaign" }, + { id: "projects", label: "Projekte", icon: "work" }, + { id: "time", label: "Zeiterfassung", icon: "schedule" }, + { id: "quotes", label: "Offerten", icon: "request_quote" }, + { id: "buchhaltung", label: "Buchhaltung", icon: "account_balance", children: [ + { id: "invoices", label: "Rechnungen" }, + { id: "internal-expenses", label: "Ausgaben" }, + { id: "expenses", label: "Spesen" }, + { id: "loehne", label: "Löhne" }, + { id: "studio-budget", label: "Budget" }, + ]}, + { id: "dokumente", label: "Dokumente", icon: "folder", children: [ + { id: "protokolle", label: "Protokolle" }, + { id: "lieferscheine", label: "Lieferscheine" }, + { id: "letters", label: "Briefe" }, + ]}, + { id: "personen", label: "Personen", icon: "group" }, + { id: "mitarbeiter", label: "Mitarbeiter", icon: "badge" }, + { id: "settings", label: "Einstellungen", icon: "settings" }, +]; + +export const STATUS_COLORS = { + aktiv: "#2d6a4f", abgeschlossen: "#555", pausiert: "#b5621e", + entwurf: "#7a6a00", gesendet: "#1a4e8a", bezahlt: "#2d6a4f", überfällig: "#8a1a1a", + offen: "#7a6a00", angenommen: "#2d6a4f", abgelehnt: "#8a1a1a", abgelaufen: "#888", + genehmigt: "#1a4e8a", "auf nächsten Lohn": "#b5621e", ausbezahlt: "#2d6a4f", +}; + +export const STATUS_BG = { + aktiv: "#e8f5ee", abgeschlossen: "#f0f0ee", pausiert: "#fdf0e8", + entwurf: "#fffbe6", gesendet: "#e8f0fa", bezahlt: "#e8f5ee", überfällig: "#fdf2f2", + offen: "#fffbe6", angenommen: "#e8f5ee", abgelehnt: "#fdf2f2", abgelaufen: "#f2f2f2", + genehmigt: "#e8f0fa", "auf nächsten Lohn": "#fdf0e8", ausbezahlt: "#e8f5ee", +}; + +export const defaultData = { + settings: { + setupCompleted: false, + name: "Mein Studio", + address: "Musterstrasse 1\n8001 Zürich", + street: "", + zip: "", + city: "", + country: "CH", + email: "mail@studio.ch", + phone: "+41 79 000 00 00", + iban: "CH00 0000 0000 0000 0000 0", + ibanType: "qr", // "qr" | "normal" + mwst: "CHE-000.000.000 MWST", + mwstRate: 8.1, + defaultHourlyRate: 120, + autoPrint: false, + logoSize: 60, + expenseCategories: [...EXPENSE_CATEGORIES], + internalExpenseCategories: [...INTERNAL_EXPENSE_CATEGORIES], + projectNumberFormat: "YYYY/NN", + invoiceNumberFormat: "YYYY-NNN", + protokollNumberFormat: "YYYY-TT-NN", + protokollTypeAbbreviations: { + "Bausitzung": "BS", + "Planungssitzung": "PS", + "Baubesprechung": "BB", + "Jour fixe": "JF", + "Interne Sitzung": "IS", + "Kundensitzung": "KS", + "Abnahme": "AB", + "Sonstiges": "SO", + }, + pdfNameFormat: "{studio}_{typ}_{nummer}", + qrNewPage: true, + pageMarginTop: 20, + pageMarginBottom: 20, + pageMarginLeft: 20, + pageMarginRight: 20, + defaultWochenstunden: 35, + defaultFerienWochen: 5, + closedMonths: [], + blockMaiTag: true, + roles: [ + { id: "PL", label: "Projektleiter/in", rate: 140 }, + { id: "TS", label: "Technischer Support", rate: 120 }, + { id: "BL", label: "Bauleiter/in", rate: 135 }, + { id: "AS", label: "Administrativer Support", rate: 120 }, + ], + }, + persons: [], + projects: [], + timeEntries: [], + invoices: [], + quotes: [], + expenses: [], + internalExpenses: [], + deliveryNotes: [], + protocols: [], + employees: [], + feiertage: [], + absences: [], + ferienEntries: [], + absenzTypes: [], + lohnEntries: [], + uberstundenAbschluss: [], + dashboardTemplates: [ + { id: "tpl-admin", name: "Administrator", isPublic: true, layout: [ + { id: "dw-a1", cols: 4, minH: 0, widgets: ["kpi-projekte","kpi-stunden","kpi-ausstehend","kpi-umsatz"] }, + { id: "dw-a2", cols: 1, minH: 0, widgets: ["warnungen"] }, + { id: "dw-a3", cols: 2, minH: 0, widgets: ["aktive-projekte","unverrechnete-stunden"] }, + { id: "dw-a4", cols: 2, minH: 0, widgets: ["umsatz-sparkline","offene-offerten"] }, + { id: "dw-a5", cols: 1, minH: 0, widgets: ["letzte-zeiteintraege"] }, + ]}, + { id: "tpl-projektleiter", name: "Projektleiter", isPublic: true, layout: [ + { id: "dw-p1", cols: 2, minH: 0, widgets: ["kpi-projekte","kpi-stunden"] }, + { id: "dw-p2", cols: 1, minH: 0, widgets: ["warnungen"] }, + { id: "dw-p3", cols: 3, minH: 0, widgets: ["meine-projekte","team-auslastung","offene-offerten"] }, + { id: "dw-p4", cols: 1, minH: 0, widgets: ["letzte-zeiteintraege"] }, + ]}, + { id: "tpl-mitarbeiter", name: "Mitarbeiter", isPublic: true, layout: [ + { id: "dw-m1", cols: 3, minH: 0, widgets: ["kpi-stunden","ueberstunden","meine-ferien"] }, + { id: "dw-m2", cols: 2, minH: 0, widgets: ["meine-projekte","stunden-woche"] }, + { id: "dw-m3", cols: 1, minH: 0, widgets: ["meine-zeiteintraege"] }, + ]}, + ], + appRoles: [ + { id: "r-admin", name: "Administrator", permissions: null, dashboardTemplateId: "tpl-admin" }, + { id: "r-projektleiter", name: "Projektleiter", permissions: ["dashboard","projects","time","quotes","personen","mitarbeiter","settings"], dashboardTemplateId: "tpl-projektleiter" }, + { id: "r-mitarbeiter", name: "Mitarbeiter", permissions: ["dashboard","projects","time","personen","settings"], dashboardTemplateId: "tpl-mitarbeiter" }, + ], + users: [ + { id: "admin", username: "admin", password: "admin", role: "admin", displayName: "Administrator", appRoleId: "r-admin" }, + ], + blogPosts: [], + letterTemplates: [ + { id: "offer", name: "Offerte", body: "Sehr geehrte/r {{client}}\n\nGerne unterbreiten wir Ihnen die Offerte für das Projekt «{{project}}».\n\n[Leistungsumfang]\n\nHonorar: CHF [Betrag]\n\nWir freuen uns auf die Zusammenarbeit.\n\nFreundliche Grüsse" }, + { id: "reminder", name: "Zahlungserinnerung", body: "Sehr geehrte/r {{client}}\n\nBei einer Überprüfung unserer Buchhaltung stellen wir fest, dass die Rechnung [Nr.] vom [Datum] über CHF [Betrag] noch nicht beglichen ist.\n\nWir bitten Sie höflich, den offenen Betrag innert 10 Tagen zu überweisen.\n\nFreundliche Grüsse" }, + ], +}; + +export const PROTOKOLL_TYPES = ["Bausitzung", "Planungssitzung", "Baubesprechung", "Jour fixe", "Interne Sitzung", "Kundensitzung", "Abnahme", "Sonstiges"]; + +export const PROTOKOLL_ENTRY_TYPES = [ + { id: "beschluss", label: "Beschluss", color: "#1a4e8a", bg: "#e8f0fa", icon: "⬡" }, + { id: "info", label: "Info", color: "#2d6a4f", bg: "#e8f5ee", icon: "ℹ" }, + { id: "aufgabe", label: "Aufgabe", color: "#b5621e", bg: "#fdf0e8", icon: "◎" }, +]; + +export const DASHBOARD_WIDGETS = [ + { id: "kpi-projekte", label: "Aktive Projekte (KPI)", span: 3 }, + { id: "kpi-stunden", label: "Stunden diesen Monat (KPI)", span: 3 }, + { id: "kpi-ausstehend", label: "Ausstehend (KPI)", span: 3 }, + { id: "kpi-umsatz", label: "Jahresumsatz (KPI)", span: 3 }, + { id: "warnungen", label: "Warnungen", span: 12 }, + { id: "aktive-projekte", label: "Projektliste mit Budget", span: 4 }, + { id: "unverrechnete-stunden", label: "Unverrechnete Stunden", span: 4 }, + { id: "umsatz-sparkline", label: "Umsatz Sparkline", span: 4 }, + { id: "offene-offerten", label: "Offene Offerten", span: 4 }, + { id: "letzte-zeiteintraege", label: "Letzte Zeiteinträge", span: 12 }, + { id: "meine-zeiteintraege", label: "Meine Zeiteinträge", span: 12 }, + { id: "meine-projekte", label: "Meine Projekte", span: 6 }, + { id: "meine-ferien", label: "Ferienstand", span: 4 }, + { id: "ueberstunden", label: "Stundensaldo", span: 4 }, + { id: "stunden-woche", label: "Stunden pro Woche", span: 6 }, + { id: "team-auslastung", label: "Team-Auslastung", span: 6 }, + { id: "interner-blog", label: "Pinnwand", span: 6 }, +]; + +export const DEFAULT_ABSENZ_TYPES = [ + { id: "krankheit", label: "Krankheit", color: "#8a1a1a" }, + { id: "unfall", label: "Unfall", color: "#b5621e" }, + { id: "intern", label: "Intern", color: "#1a4e8a" }, + { id: "informatik", label: "Informatik", color: "#555" }, + { id: "rechnungswesen", label: "Rechnungswesen", color: "#7a6a00" }, + { id: "weiterbildung", label: "Weiterbildung", color: "#2d6a4f" }, + { id: "militaer", label: "Militär / Zivildienst", color: "#3d3d38" }, +]; diff --git a/src/index.css b/src/index.css new file mode 100755 index 0000000..3cb100e --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +*, *::before, *::after { box-sizing: border-box; } +html, body { margin: 0; padding: 0; height: 100%; } +#root { height: 100%; } diff --git a/src/main.jsx b/src/main.jsx new file mode 100755 index 0000000..b9a1a6d --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/src/print/PrintComponents.jsx b/src/print/PrintComponents.jsx new file mode 100755 index 0000000..8976d9e --- /dev/null +++ b/src/print/PrintComponents.jsx @@ -0,0 +1,1686 @@ +import React, { useState, useEffect } from "react"; +import { SIA_PHASES, PROJECT_TYPES, STATUS_COLORS } from "../constants.js"; +import { calcSIAHours, calcManualHours, formatCHF, formatDate, formatHours, formatSenderAddress, isQRIban, generateQRReference, formatIban, buildPdfName } from "../utils.js"; +import { StudioLogo } from "../components/UI.jsx"; + +export +function PrintView({ content, onClose, settings }) { + const triggerPrint = async () => { + const pdfName = buildPdfName(settings?.pdfNameFormat, content, settings); + const prevTitle = document.title; + document.title = pdfName; + try { + if (window.__TAURI_INTERNALS__) { + const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + await getCurrentWebviewWindow().print(); + } else { + window.print(); + } + } catch { + window.print(); + } finally { + setTimeout(() => { document.title = prevTitle; }, 2000); + } + }; + + useEffect(() => { + if (!settings.autoPrint) return; + const timer = setTimeout(() => triggerPrint(), 400); + return () => clearTimeout(timer); + }, [settings.autoPrint]); + + const mTop = settings?.pageMarginTop ?? 20; + const mBottom = settings?.pageMarginBottom ?? 20; + const mLeft = settings?.pageMarginLeft ?? 20; + const mRight = settings?.pageMarginRight ?? 20; + const isQrOnly = content.type === "qrbill"; + + return ( +
+ +
+ + +
+ +
+ {content.type === "invoice" && } + {content.type === "invoice+qr" && ( + <> + +
+ +
+ + )} + {content.type === "letter" && } + {content.type === "projectDetail" && } + {content.type === "projectsOverview" && } + {content.type === "qrbill" && } + {content.type === "quote" && } + {content.type === "buchhaltung" && } + {content.type === "lohn" && } + {content.type === "lieferschein" && } + {content.type === "studioBudget" && } + {content.type === "protokoll" && } + {content.type === "mitarbeiterOverview" && } + {content.type === "timeReport" && } +
+
+ ); +} + +export +function InvoicePrint({ inv, client, settings }) { + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
{settings.email} · {settings.phone}
+
+
+
RECHNUNG
+
Nr. {inv.number}
+
+
+ +
+
+
RECHNUNG AN
+
{client?.name || "—"}
+ {(() => { + const contact = inv.contactId ? (client?.contacts || []).find(ct => ct.id === inv.contactId) : null; + if (contact) return
z.H. {contact.name}{contact.position ? `, ${contact.position}` : ""}
; + return null; + })()} + {client?.street &&
{client.street}
} + {(client?.zip || client?.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} + {!client?.street && client?.address &&
{client.address}
} +
+
+
Datum: {formatDate(inv.date)}
+ {inv.dueDate &&
Fällig: {formatDate(inv.dueDate)}
} +
+
+ + + + + + + + {(inv.items || []).some(it => (it.discount || 0) > 0) && ( + + )} + + + + + {(inv.items || []).map((item, i) => { + const lineTotal = item.qty * item.price; + const lineDisc = (item.discount || 0) > 0 ? lineTotal * (item.discount / 100) : 0; + const hasAnyDiscount = (inv.items || []).some(it => (it.discount || 0) > 0); + return ( + + + + + {hasAnyDiscount && ( + + )} + + + ); + })} + +
BESCHREIBUNGMENGEPREISRABATTTOTAL
{item.desc}{item.qty}{formatCHF(item.price)} + {(item.discount || 0) > 0 ? `−${item.discount}%` : "—"} + {formatCHF(lineTotal - lineDisc)}
+ +
+
+ {(inv.globalDisc || 0) > 0 && <> +
Zwischentotal{formatCHF((inv.items || []).reduce((s, it) => { const l = it.qty * it.price; return s + l - ((it.discount||0) > 0 ? l*(it.discount/100) : 0); }, 0))}
+
{inv.discountLabel || "Rabatt"}−{formatCHF(inv.globalDisc)}
+ } + {inv.mwst && <> +
Netto{formatCHF(inv.sub)}
+
MWST {inv.mwstRate || settings.mwstRate}%{formatCHF(inv.tax)}
+ } +
+ Total{formatCHF(inv.total)} +
+
+
+ + {inv.notes &&
{inv.notes}
} + +
+
+
ZAHLUNG AUF
+
{settings.iban}
+
Referenz: {inv.number}
+
+
+
{settings.mwst}
+
+
+ + ); +} + +export +function LieferscheinPrint({ note, client, data, settings }) { + const project = note.projectId ? (data?.projects || []).find(p => p.id === note.projectId) : null; + const projectLabel = project?.name || note.projectManual || null; + const addr = note.deliveryAddress || client?.address || ""; + + return ( + <> + {/* Kopf */} +
+
+ +
{formatSenderAddress(settings)}
+
{settings.email} · {settings.phone}
+
+
+
LIEFERSCHEIN
+
{note.number || "—"}
+
Datum: {formatDate(note.date)}
+
+
+ + {/* Empfänger & Projektinfo */} +
+
+
LIEFERUNG AN
+
{client?.name || note.clientManual || "—"}
+ {client?.company &&
{client.company}
} + {addr &&
{addr}
} +
+ {projectLabel && ( +
+
PROJEKT
+
{projectLabel}
+ {project?.number &&
{project.number}
} +
+ )} +
+ + {/* Positionen */} + + + + + + + + + + + + + {(note.items || []).map((it, i) => ( + + + + + + + + + ))} + +
POS.BESCHREIBUNGMENGEEINHEITBEMERKUNGEMPFANGEN ✓
{i + 1}{it.desc || "—"}{it.qty}{it.unit}{it.note || ""} +
+
+ + {/* Notizen */} + {note.notes && ( +
+ {note.notes} +
+ )} + + {/* Unterschriften */} +
+
+
+ ÜBERGABE – {settings.name} +
+
Datum / Unterschrift
+
+
+
+ EMPFANG – {client?.name || note.clientManual || "Empfänger"} +
+
Datum / Unterschrift
+
+
+ + {/* Footer */} +
+
{settings.name} · {formatSenderAddress(settings).split("\n")[0]}
+
{note.number}
+
+ + ); +} + +export +function StudioBudgetPrint({ snapshot, settings }) { + const r = snapshot.results || {}; + const rateOk = (r.currentRate || 0) >= (r.zielHonorar || 0); + const b = snapshot.b || {}; + + const PRow = ({ label, value, bold, indent, color }) => ( +
+ {label} + {value} +
+ ); + + return ( + <> + {/* Kopf */} +
+
+ +
{formatSenderAddress(settings)}
+
+
+
BÜROBUDGET
+
{snapshot.name}
+
+ Erstellt: {snapshot.savedAt ? new Date(snapshot.savedAt).toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" }) : "—"} +
+
+
+ +
+ + {/* Kostenstruktur */} +
+
KOSTENSTRUKTUR / JAHR
+ + + + +
+ Gesamtkosten + {formatCHF(r.gesamtKosten || 0)} +
+
+ + {/* Jahresstunden & Kernresultat */} +
+
STUNDENANALYSE
+ + +
+
+
+
+
+
SELBSTKOSTEN/H
+
{formatCHF(Math.round(r.selbstkosten || 0))}
+
+
+
ZIEL-HONORAR/H
+
{formatCHF(Math.round(r.zielHonorar || 0))}
+
inkl. {b.zielMarge || 25}% Marge
+
+
+
+
+ {rateOk ? "✓ Aktueller Ansatz ausreichend" : "⚠ Aktueller Ansatz zu tief"} +
+
+ Aktuell: {formatCHF(r.currentRate || 0)}/h · Ziel: {formatCHF(Math.round(r.zielHonorar || 0))}/h · Differenz: {rateOk ? "+" : ""}{formatCHF(Math.round((r.currentRate || 0) - (r.zielHonorar || 0)))} +
+
+
+
+ + {/* Personal-Detail */} + {(snapshot.empSnapshot || []).length > 0 && ( +
+
PERSONAL
+ + + + + + + + + + {snapshot.empSnapshot.filter(r => r.aktiv).map((r, i) => ( + + + + + + ))} + +
MitarbeiterJahreskostenJahresstunden
{r.name}{formatCHF(r.kosten)}{r.stunden}h
+
+ )} + + {/* Fixkosten-Detail */} + {(snapshot.fixSnapshot || []).length > 0 && ( +
+
FIXKOSTEN
+ + + + + + + + + {snapshot.fixSnapshot.map((r, i) => ( + + + + + ))} + +
PostenCHF/Jahr
{r.label}{formatCHF(r.amount)}
+
+ )} + + {/* Footer */} +
+
{settings.name}
+
Bürobudget · {snapshot.name}
+
+ + ); +} + +export +function ProtokollPrint({ protokoll, data, settings }) { + const p = protokoll; + const proj = data?.projects?.find(x => x.id === p.projectId); + const projLabel = proj?.name || p.projectManual || null; + const today = new Date().toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" }); + + const statusLabels = { anwesend: "A", entschuldigt: "E", abwesend: "Ab", eingeladen: "Eingeladen" }; + const statusColors = { anwesend: "#2d6a4f", entschuldigt: "#b5621e", abwesend: "#8a1a1a", eingeladen: "#1a4e8a" }; + + const allTasks = (p.traktanden || []).flatMap(t => + (t.items || []).filter(it => it.type === "aufgabe") + .map(it => ({ ...it, tNr: t.nr, tTitle: t.title })) + ); + const allBeschluesse = (p.traktanden || []).flatMap(t => + (t.items || []).filter(it => it.type === "beschluss") + .map(it => ({ ...it, tNr: t.nr, tTitle: t.title })) + ); + + return ( + <> + {/* Kopf */} +
+
+ +
{formatSenderAddress(settings)}
+
+
+
PROTOKOLL
+
{p.nummer}
+
+
+ + {/* Titel & Meta */} +
+
{p.title || "Protokoll"}
+
+ {[ + { label: "TYP", value: p.type }, + { label: "DATUM", value: p.date ? `${formatDate(p.date)}${p.time ? `, ${p.time}${p.endTime ? `–${p.endTime}` : ""} Uhr` : ""}` : "—" }, + { label: "ORT", value: p.location || "—" }, + { label: "PROJEKT", value: projLabel || "—" }, + ].map(m => ( +
+
{m.label}
+
{m.value}
+
+ ))} +
+
+ + {/* Teilnehmer */} + {(p.participants || []).length > 0 && ( +
+
TEILNEHMER
+ + + + + + + + + + {(p.participants || []).map((tn, i) => ( + + + + + + ))} + +
NameFunktionStatus
{tn.name}{tn.role || "—"} + {statusLabels[tn.status] || tn.status} +
+
+ )} + + {/* Traktanden */} + {(p.traktanden || []).map(t => { + const hasContent = t.title || (t.items || []).length > 0; + if (!hasContent) return null; + return ( +
+
+ {t.nr} + {t.title || "—"} +
+ {(t.items || []).map((item, ii) => { + const icons = { info: "ℹ", beschluss: "✅", aufgabe: "📌" }; + const colors = { info: "#1a4e8a", beschluss: "#2d6a4f", aufgabe: "#b5621e" }; + return ( +
+
{icons[item.type]}
+
+
{item.text || "—"}
+ {item.type === "beschluss" && item.date && ( +
Beschluss vom {formatDate(item.date)}
+ )} + {item.type === "aufgabe" && ( +
+ {item.responsible && → {item.responsible}} + {item.dueDateType === "kw" ? (item.dueKW ? `KW ${item.dueKW}/${item.dueYear || ""}` : "—") : item.dueDate ? formatDate(item.dueDate) : "—"} + {item.status} +
+ )} +
+
+ ); + })} +
+ ); + })} + + {/* Aufgabenliste */} + {allTasks.length > 0 && ( +
+
AUFGABEN-ÜBERSICHT
+ + + + + + + + + + + {allTasks.map((t, i) => ( + + + + + + + ))} + +
AufgabeVerantwortlichFälligkeitStatus
+ {t.tNr}.{t.text || "—"} + {t.responsible || "—"} + {t.dueDateType === "kw" ? (t.dueKW ? `KW ${t.dueKW}/${t.dueYear || ""}` : "—") : t.dueDate ? formatDate(t.dueDate) : "—"} + {t.status}
+
+ )} + + {/* Nächste Sitzung */} + {(p.nextDate || p.verteiler) && ( +
+ {p.nextDate &&
Nächste Sitzung:{formatDate(p.nextDate)}
} + {p.verteiler &&
Verteiler:{p.verteiler}
} +
+ )} + + {/* Unterschriften */} +
+
+
PROTOKOLLFÜHRUNG
+
Datum / Unterschrift
+
+
+
GENEHMIGUNG
+
Datum / Unterschrift
+
+
+ + {/* Footer */} +
+
{settings.name} · {p.nummer}
+
Erstellt {today}{p.verteiler ? ` · Verteiler: ${p.verteiler}` : ""}
+
+ + ); +} + +export +function LetterPrint({ client, subject, body, isHtml, settings }) { + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
+
+
{settings.email}
+
{settings.phone}
+
+
+ +
+ {client ? (<> +
{client.name}
+ {client.company &&
{client.company}
} + {client.street &&
{client.street}
} + {(client.zip || client.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} + {!client.street && !client.city && client.address &&
{client.address}
} + ) :
[Empfänger]
} +
+ +
+ {settings.city || (settings.address || "").split("\n").pop().replace(/^\d{4,5}\s*/, "").trim() || "Zürich"}, {new Date().toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric" })} +
+ + {subject &&
{subject}
} + + + {isHtml + ?
+ :
{body}
+ } + +
{settings.name}
+ + ); +} + +export +function ProjectDetailPrint({ content, settings }) { + const { project, client, entries, phaseStats, unassignedMins, totalMinutes, totalAmount, billingType, invoices, data } = content; + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
+
+
PROJEKTREPORT
+
{new Date().toLocaleDateString("de-CH")}
+
+
+ +
+
{(project.category || "PROJEKT").toUpperCase()}
+
{project.name}
+ {client &&
{client.name}{client.company ? ` · ${client.company}` : ""}
} + {project.description &&
{project.description}
} +
+ +
+
STUNDEN TOTAL
{formatHours(totalMinutes)}
+
{billingType === "stundensatz" ? "AUFWAND" : "PAUSCHALE"}
{formatCHF(totalAmount)}
+
EINTRÄGE
{entries.length}
+
+ + {phaseStats.length > 0 && ( +
+
+ AUFWAND PRO SIA-PHASE · HAUPTAUFTRAG +
+ {phaseStats.map(ps => ( +
+
+ {ps.label} + {formatHours(ps.minutes)} · {ps.percent.toFixed(1)}% +
+
+
+
+
+ ))} + {unassignedMins > 0 &&
Ohne Phasen-Zuordnung: {formatHours(unassignedMins)}
} +
+ )} + + {(project.positions || []).filter(pos => { + const posEntries = entries.filter(e => e.positionId === pos.code); + return posEntries.length > 0 || (pos.enabledPhases || []).length > 0; + }).map(pos => { + const posEntries = entries.filter(e => e.positionId === pos.code); + const posMins = posEntries.reduce((s, e) => s + (e.minutes || 0), 0); + const allPhaseIds = [...new Set([...(pos.enabledPhases || []), ...posEntries.map(e => e.phaseId).filter(Boolean)])]; + const linkedQ = pos.quoteId ? (data?.quotes || []).find(q => q.id === pos.quoteId) : null; + return ( +
+
+ ↪ {pos.code}{pos.label ? ` · ${pos.label}` : ""}{linkedQ ? ` · ${linkedQ.number}` : ""} + {formatHours(posMins)} +
+ {allPhaseIds.length > 0 ? allPhaseIds.map(phId => { + const ph = SIA_PHASES.find(p => p.id === phId); + if (!ph) return null; + const phMins = posEntries.filter(e => e.phaseId === phId).reduce((s, e) => s + (e.minutes || 0), 0); + const pct = posMins > 0 ? (phMins / posMins) * 100 : 0; + return ( +
+
+ {ph.label} + {formatHours(phMins)} · {pct.toFixed(1)}% +
+
+
+
+
+ ); + }) : ( + posMins === 0 ? null :
Keine Phasen-Unterteilung · {formatHours(posMins)}
+ )} +
+ ); + })} + + {entries.length > 0 && ( +
+
ZEITEINTRÄGE
+ + + + + + + + + + + {entries.map(e => { + const phase = SIA_PHASES.find(p => p.id === e.phaseId); + return ( + + + + + + + ); + })} + +
DATUMPHASETÄTIGKEITDAUER
{formatDate(e.date)}{phase?.id || "—"}{e.description || "—"}{formatHours(e.minutes)}
+
+ )} + + {invoices && invoices.length > 0 && ( +
+
RECHNUNGEN
+ + + {invoices.map(inv => ( + + + + + + + ))} + +
{inv.number}{formatDate(inv.date)}{inv.status}{formatCHF(inv.total)}
+
+ )} + + ); +} + +export +function ProjectsOverviewPrint({ projects, data, settings }) { + const projectMinutes = (id) => data.timeEntries.filter(e => e.projectId === id).reduce((s, e) => s + (e.minutes || 0), 0); + const projectAmount = (p) => { + const bType = p.billingType || p.type; + if (bType === "stundensatz") return (projectMinutes(p.id) / 60) * p.hourlyRate; + return p.budget || 0; + }; + + const grouped = PROJECT_TYPES.map(cat => ({ + category: cat, + projects: projects.filter(p => (p.category || "Sonstiges") === cat), + })).filter(g => g.projects.length > 0); + + const uncategorized = projects.filter(p => !p.category); + if (uncategorized.length > 0) grouped.push({ category: "Ohne Kategorie", projects: uncategorized }); + + const totalMins = projects.reduce((s, p) => s + projectMinutes(p.id), 0); + const totalAmount = projects.reduce((s, p) => s + projectAmount(p), 0); + + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
+
+
PROJEKTÜBERSICHT
+
{new Date().toLocaleDateString("de-CH")}
+
+
+ +
+
Alle Projekte
+
{projects.length} Projekte · {formatHours(totalMins)} · {formatCHF(totalAmount)}
+
+ + {grouped.map(g => ( +
+
{g.category.toUpperCase()} — {g.projects.length}
+ + + + + + + + + + + + + + + + + + + {g.projects.map(p => { + const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === p.clientId); + return ( + + + + + + + + ); + })} + +
PROJEKTKUNDESTATUSSTUNDENBETRAG
{p.name}{client?.name || "—"}{p.status} + {formatHours(projectMinutes(p.id))} + {p.budgetHours > 0 && / {p.budgetHours}h} + {formatCHF(projectAmount(p))}
+
+ ))} + +
+ Total + {formatHours(totalMins)} · {formatCHF(totalAmount)} +
+ + ); +} + +export +function QRBillPrint({ inv, client, settings }) { + const [qrSvg, setQrSvg] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + const loadSwissQRBill = () => new Promise((resolve, reject) => { + if (window.SwissQRBill) return resolve(window.SwissQRBill); + const existing = document.querySelector('script[data-swissqrbill]'); + if (existing) { + existing.addEventListener("load", () => resolve(window.SwissQRBill)); + existing.addEventListener("error", reject); + return; + } + const script = document.createElement("script"); + script.src = "https://cdn.jsdelivr.net/npm/swissqrbill@4/lib/browser/bundle/svg.min.js"; + script.dataset.swissqrbill = "true"; + script.onload = () => resolve(window.SwissQRBill); + script.onerror = () => { + // Fallback URL + const fallback = document.createElement("script"); + fallback.src = "https://cdn.jsdelivr.net/npm/swissqrbill/lib/browser/bundle/index.js"; + fallback.onload = () => resolve(window.SwissQRBill); + fallback.onerror = reject; + document.head.appendChild(fallback); + }; + document.head.appendChild(script); + }); + + (async () => { + try { + const lib = await loadSwissQRBill(); + if (cancelled) return; + if (!lib) throw new Error("swissqrbill-Bibliothek konnte nicht geladen werden"); + + // Daten vorbereiten + const ibanClean = (settings.iban || "").replace(/\s/g, "").toUpperCase(); + const hasQRIban = isQRIban(ibanClean); + const data = { + currency: "CHF", + amount: inv.total, + creditor: { + name: settings.name || "—", + address: settings.street || "—", + zip: settings.zip || "", + city: settings.city || "", + account: ibanClean, + country: settings.country || "CH", + }, + }; + if (client) { + data.debtor = { + name: client.company || client.name || "—", + address: client.street || "—", + zip: client.zip || "", + city: client.city || "", + country: client.country || "CH", + }; + } + if (hasQRIban) { + data.reference = generateQRReference(inv.number); + } + if (inv.notes || inv.number) { + data.message = `Rechnung ${inv.number}`; + } + + // SVG erstellen — je nach Exportstruktur unterschiedlich + const SVG = lib.SVG || lib.SwissQRBill || (lib.svg && lib.svg.SVG); + if (!SVG) throw new Error("SVG-Klasse nicht gefunden in swissqrbill"); + const qr = new SVG(data); + const svgElement = qr.element || qr.outerHTML || qr; + const svgString = typeof svgElement === "string" ? svgElement : svgElement.outerHTML; + + if (!cancelled) setQrSvg(svgString); + } catch (err) { + console.error("QR-Bill Fehler:", err); + if (!cancelled) setError(err.message || "Fehler beim Generieren der QR-Rechnung"); + } + })(); + + return () => { cancelled = true; }; + }, [inv, client, settings]); + + if (error) { + return ( +
+
Fehler beim Generieren
+
{error}
+
+ Mögliche Ursachen: fehlende Empfänger-Adresse (Strasse, PLZ, Ort), ungültige IBAN, oder fehlende Internetverbindung zum CDN. +
+
+ ); + } + + if (!qrSvg) { + return ( +
QR-Rechnung wird generiert…
+ ); + } + + return ( +
+ ); +} + +export +function QuotePrint({ quote, client, settings }) { + const taxRate = settings.mwstRate || 8.1; + const roles = quote.quoteRoles || settings.roles || []; + const siaCalc = quote.mode === "sia" && quote.sia ? calcSIAHours(quote.sia.baukosten, quote.sia.schwierigkeit, quote.sia.phases) : null; + const manCalc = quote.mode === "manual" ? calcManualHours(quote.manualPhases || [], roles) : null; + const stundenansatz = quote.sia?.stundenansatz || 0; + + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
{settings.email} · {settings.phone}
+
+
+
HONORAROFFERTE
+
Nr. {quote.number}
+
+
+ +
+
+
OFFERTE AN
+ {client ? ( + <> + {client.company &&
{client.company}
} +
{client.name || "—"}
+ {client.street &&
{client.street}
} + {(client.zip || client.city) &&
{[client.zip, client.city].filter(Boolean).join(" ")}
} + {!client.street && !client.city && client.address &&
{client.address}
} + + ) :
} +
+
+
Datum: {formatDate(quote.date)}
+ {quote.validUntil &&
Gültig bis: {formatDate(quote.validUntil)}
} +
+
+ + {quote.notes &&
{quote.notes}
} + + {/* SIA-Modus */} + {quote.mode === "sia" && siaCalc && ( + <> +
+ HONORARBERECHNUNG NACH SIA 102 +
+
+
+
Aufwandbestimmende Baukosten
+
{formatCHF(quote.sia.baukosten)}
+
+
+
Schwierigkeitsgrad n
+
{quote.sia.schwierigkeit}
+
+
+
Stundenansatz
+
CHF {stundenansatz}.–/h
+
+
+ + + + + + + + + + + + {siaCalc.phases.map(ph => ( + + + + + {ph.items.filter(it => it.enabled !== false && it.hours > 0).map((it, idx) => ( + + + + + + + ))} + + + + + + + ))} + +
TEILLEISTUNG%STUNDENHONORAR
+ Phase {ph.id} · {ph.label} +
{it.label}{it.pct}%{formatHours(Math.round(it.hours * 60))}{formatCHF(it.hours * stundenansatz)}
Total Phase {ph.id}{formatHours(Math.round(ph.hours * 60))}{formatCHF(ph.hours * stundenansatz)}
+ + )} + + {/* Manueller Modus */} + {quote.mode === "manual" && manCalc && ( + <> +
+ HONORARSCHÄTZUNG STUNDENAUFWAND +
+
+ {roles.map(r => ( +
{r.id} {r.label} — CHF {r.rate}.–/h
+ ))} +
+ + + + + {roles.map(r => ( + + ))} + + + + + + {manCalc.phases.filter(p => p.totalHours > 0).map(ph => ( + + + {roles.map(r => { + const h = ph.roleDetails.find(rd => rd.id === r.id)?.hours || 0; + return ; + })} + + + + ))} + +
PHASE{r.id}StdHonorar
{ph.label}{h || "—"}{ph.totalHours}{formatCHF(ph.totalAmount)}
+ + )} + + {/* Freier Modus */} + {quote.mode === "free" && (quote.freeItems || []).length > 0 && ( + <> +
+ LEISTUNGEN / POSITIONEN +
+ + + + + + + + + + + {(quote.freeItems || []).map((it, idx) => ( + + + + + + + ))} + +
BESCHREIBUNGMENGEPREISTOTAL
{it.desc || "—"}{it.qty}{formatCHF(it.price)}{formatCHF(it.qty * it.price)}
+ + )} + + {/* Total */} +
+
+
+ Netto + {formatCHF(quote.sub)} +
+ {quote.mwst && ( +
+ MWST {taxRate}% + {formatCHF(quote.tax)} +
+ )} +
+ Offertsumme + {formatCHF(quote.total)} +
+
+
+ +
+ Diese Offerte ist unverbindlich und {quote.validUntil ? `gültig bis ${formatDate(quote.validUntil)}` : "zeitlich unbegrenzt gültig"}. + {quote.mode !== "free" && " Honorar gemäss SIA-Ordnung 102."} Änderungen am Auftragsumfang können zu einer Anpassung führen. +
+ + ); +} + +export +function LohnPrint({ entry, emp, data, monatLabel, settings }) { + const spesen = (data.expenses || []).filter(e => e.lohnEntryId === entry.id); + // Immer den gespeicherten Snapshot verwenden — nie live emp-Werte + const s = entry.saetzeSnapshot || emp; + + const LRow = ({ label, betrag, satz, bold, sub, color, topBorder }) => ( + + {label} + {satz || ""} + {formatCHF(betrag)} + + ); + + return ( +
+ {/* Header: Arbeitgeber links, Arbeitnehmer rechts */} +
+
+ +
+ {formatSenderAddress(settings).split("\n").map((l,i) =>
{l}
)} + {settings.email &&
{settings.email}
} +
+
+
+
LOHNABRECHNUNG
+
{entry.empSnapshot?.name || emp.name}
+ {emp.role &&
{emp.role}
} + {emp.personalNr &&
Personal-Nr. {emp.personalNr}
} + {emp.ahvNr &&
AHV-Nr. {emp.ahvNr}
} + {emp.adresse &&
{emp.adresse}
} + {emp.ort &&
{emp.ort}
} +
+
+ + {/* Periode */} +
+
+
PERIODE
+
{monatLabel}
+
+
+
ABRECHNUNGSDATUM
+
{new Date(entry.createdAt).toLocaleDateString("de-CH")}
+
+ {emp.eintrittsdatum && ( +
+
EINTRITTSDATUM
+
{formatDate(emp.eintrittsdatum)}
+
+ )} +
+
PENSUM
+
{s.pensum || 100}%
+
+
+ + {/* Lohn-Tabelle */} + + + + + + + + + + + {(s.pensum || 100) < 100 && entry.bruttoBase != null && entry.bruttoBase !== entry.brutto && ( + + )} + {entry.dreizehnter > 0 && } + {entry.bonusBetrag > 0 && } + + + {/* Spacer */} + + + + + + + + {entry.qst > 0 && } + + + + {spesen.length > 0 && <> + + {spesen.map(s => )} + } + + + +
POSITIONSATZBETRAG CHF
ABZÜGE ARBEITNEHMER
SPESENERSTATTUNG
+ + {/* Überweisungsdetails */} + {emp.lohnIban && ( +
+
ÜBERWEISUNG AUF
+
{formatIban(emp.lohnIban)}
+ {emp.name &&
{emp.name}{emp.ort ? ` · ${emp.ort}` : ""}
} +
+ )} + + {/* Arbeitgeber-Anteile (informativ, klein) */} +
+
ARBEITGEBERANTEILE (informativ)
+
+ {[ + { label: "AHV/IV/EO", val: Math.round(entry.bruttoTotal * (s.ahvSatz ?? 5.3) / 100 * 100)/100 }, + { label: "ALV", val: Math.round(entry.bruttoTotal * (s.alvSatz ?? 1.1) / 100 * 100)/100 }, + { label: "BVG/PK (AG)", val: Math.round(entry.bruttoTotal * (s.bvgSatz ?? 8.0) / 100 * 100)/100 }, + { label: "UVG BU", val: Math.round(entry.bruttoTotal * 0.5 / 100 * 100)/100 }, + ].map(r => ( +
+
{r.label}
+
{formatCHF(r.val)}
+
+ ))} +
+
+ +
+ Lohnabrechnung gemäss OR Art. 323b · {settings.name}{settings.mwst ? ` · ${settings.mwst}` : ""} +
+
+ ); +} + +export +function BuchhaltungPrint({ data, filterYear, settings }) { + const mwstRate = settings.mwstRate || 8.1; + const invoices = [...data.invoices] + .filter(i => !filterYear || (i.date || "").startsWith(filterYear)) + .sort((a, b) => (a.date || "").localeCompare(b.date || "")); + const expenses = [...(data.expenses || [])] + .filter(e => !filterYear || (e.date || "").startsWith(filterYear)) + .sort((a, b) => (a.date || "").localeCompare(b.date || "")); + const loehne = [...(data.lohnEntries || [])] + .filter(l => !filterYear || l.monat.startsWith(filterYear)) + .sort((a, b) => a.monat.localeCompare(b.monat)); + + const totalInvoicedNet = invoices.reduce((s, i) => s + (i.sub || 0), 0); + const totalInvoicedTax = invoices.reduce((s, i) => s + (i.tax || 0), 0); + const totalExpBrutto = expenses.reduce((s, e) => s + (e.amount || 0), 0); + const totalExpNet = expenses.reduce((s, e) => { const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; return s + net; }, 0); + const totalExpTax = totalExpBrutto - totalExpNet; + const totalLoehne = loehne.reduce((s, l) => s + (l.auszahlung || 0), 0); + const totalAusgaben = totalExpNet + totalLoehne; + + const thStyle = { textAlign: "left", padding: "6px 0", fontSize: 8, letterSpacing: "0.1em", color: "#888", fontWeight: 500, borderBottom: "1px solid #1a1a18" }; + const thR = { ...thStyle, textAlign: "right" }; + const tdStyle = { padding: "5px 0", fontSize: 9, borderBottom: "1px solid #f0f0f0" }; + const tdR = { ...tdStyle, textAlign: "right" }; + + return ( + <> + {/* Header */} +
+
+ +
{formatSenderAddress(settings)}
+
+
+
BUCHHALTUNGSÜBERSICHT
+
{filterYear || "Alle Jahre"}
+
{new Date().toLocaleDateString("de-CH")}
+
+
+ + {/* Zusammenfassung */} +
+
+
EINNAHMEN
+ {[ + { label: "Umsatz (Netto)", value: totalInvoicedNet }, + { label: `MWST ${mwstRate}%`, value: totalInvoicedTax, small: true }, + { label: "Umsatz (Brutto)", value: totalInvoicedNet + totalInvoicedTax, bold: true }, + ].map((r, i) => ( +
+ {r.label} + {formatCHF(r.value)} +
+ ))} +
+
+
AUSGABEN & ERGEBNIS
+ {[ + { label: "Spesen / Ausgaben (Netto)", value: totalExpNet }, + { label: "Vorsteuer", value: totalExpTax, small: true }, + { label: `Personalaufwand / Löhne (${loehne.length})`, value: totalLoehne }, + { label: "Ergebnis (Netto)", value: totalInvoicedNet - totalAusgaben, bold: true }, + { label: "MWST-Schuld", value: totalInvoicedTax - totalExpTax, bold: true, small: true }, + ].map((r, i) => ( +
+ {r.label} + {formatCHF(r.value)} +
+ ))} +
+
+ + {/* Rechnungen */} +
+
+ RECHNUNGEN ({invoices.length}) +
+ + + + + + + + + + + + + + + {invoices.map(inv => { + const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); + const desc = (inv.items || []).map(it => it.desc).filter(Boolean).join(", "); + return ( + + + + + + + + + + + ); + })} + + + + + + + + + + +
NR.DATUMKUNDEBESCHREIBUNGNETTOMWSTTOTALSTATUS
{inv.number}{formatDate(inv.date)}{client?.name || "—"}{desc || "—"}{formatCHF(inv.sub)}{inv.mwst ? formatCHF(inv.tax) : "—"}{formatCHF(inv.total)}{inv.status}
Total{formatCHF(totalInvoicedNet)}{formatCHF(totalInvoicedTax)}{formatCHF(totalInvoicedNet + totalInvoicedTax)}
+
+ + {/* Spesen */} +
+
+ SPESEN / AUSGABEN ({expenses.length}) +
+ + + + + + + + + + + + + + {expenses.map(e => { + const proj = data.projects.find(p => p.id === e.projectId); + const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; + const tax = e.amount - net; + return ( + + + + + + + + + + ); + })} + + + + + + + + + +
DATUMKATEGORIEPROJEKTBESCHREIBUNGNETTOVORSTEUERBRUTTO
{formatDate(e.date)}{e.category}{proj?.name || "—"}{e.description || "—"}{formatCHF(net)}{e.mwstRate > 0 ? formatCHF(tax) : "—"}{formatCHF(e.amount)}
Total{formatCHF(totalExpNet)}{formatCHF(totalExpTax)}{formatCHF(totalExpBrutto)}
+
+ + {/* Personalaufwand / Löhne */} + {loehne.length > 0 && ( +
+
+ PERSONALAUFWAND / LÖHNE ({loehne.length}) +
+ + + + + + + + + + + + + + {loehne.map(l => ( + + + + + + + + + + ))} + + + + + + + + + +
MONATMITARBEITERBRUTTOABZÜGE ANNETTOSPESENAUSZAHLUNG
{l.monat}{l.empSnapshot?.name || "—"}{formatCHF(l.bruttoTotal)}{formatCHF(l.totalAbzuege)}{formatCHF(l.netto)}{l.spesenTotal > 0 ? formatCHF(l.spesenTotal) : "—"}{formatCHF(l.auszahlung)}
Total Auszahlungen{formatCHF(totalLoehne)}
+
+ )} + +
+ {settings.name} · {settings.mwst} · Erstellt am {new Date().toLocaleDateString("de-CH")} +
+ + ); +} + +function MitarbeiterOverviewPrint({ employees, settings }) { + const active = employees.filter(e => e.status !== "inaktiv"); + const inactive = employees.filter(e => e.status === "inaktiv"); + + const Row = ({ label, value }) => value ? ( +
+ {label} + {value} +
+ ) : null; + + const EmpCard = ({ emp }) => ( +
+
+
+
{emp.name}
+ {emp.role &&
{emp.role}
} +
+ {emp.personalNr &&
#{emp.personalNr}
} +
+ + + + + +
+ ); + + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
+
+
MITARBEITERÜBERSICHT
+
{new Date().toLocaleDateString("de-CH")}
+
+
+ +
+
Mitarbeiter
+
{employees.length} Mitarbeitende · {active.length} aktiv
+
+ + {active.length > 0 && ( +
+
AKTIV — {active.length}
+
+ {active.map(e => )} +
+
+ )} + + {inactive.length > 0 && ( +
+
INAKTIV — {inactive.length}
+
+ {inactive.map(e => )} +
+
+ )} + + ); +} + +function TimeReportPrint({ employee, entries, month, data, settings }) { + const monthEntries = entries.filter(e => e.date.startsWith(month)) + .sort((a, b) => a.date.localeCompare(b.date) || (a.startTime || "").localeCompare(b.startTime || "")); + + const projects = data.projects || []; + const getProj = (id) => projects.find(p => p.id === id); + + const totalMins = monthEntries.reduce((s, e) => s + (e.minutes || 0), 0); + + const byProject = {}; + monthEntries.forEach(e => { + const k = e.projectId || "__none__"; + if (!byProject[k]) byProject[k] = { proj: getProj(e.projectId), mins: 0 }; + byProject[k].mins += e.minutes || 0; + }); + + const monthLabel = new Date(month + "-01").toLocaleDateString("de-CH", { month: "long", year: "numeric" }); + + return ( + <> +
+
+ +
{formatSenderAddress(settings)}
+
+
+
STUNDENRAPPORT
+
{new Date().toLocaleDateString("de-CH")}
+
+
+ +
+
{employee?.name}
+
{monthLabel} · {formatHours(totalMins)}
+
+ + {/* Zusammenfassung */} +
+
ZUSAMMENFASSUNG PRO PROJEKT
+ {Object.values(byProject).sort((a, b) => b.mins - a.mins).map(({ proj, mins }) => ( +
+ {proj ? `${proj.number ? proj.number + " " : ""}${proj.name}` : "Kein Projekt"} + {formatHours(mins)} +
+ ))} +
+ Total{formatHours(totalMins)} +
+
+ + {/* Detailliste */} +
EINZELEINTRÄGE
+ + + + {["Datum", "Projekt", "Zeit", "Stunden", "Notiz"].map(h => ( + + ))} + + + + {monthEntries.map(e => { + const proj = getProj(e.projectId); + return ( + + + + + + + + ); + })} + +
{h}
{formatDate(e.date)}{proj ? `${proj.number ? proj.number + " " : ""}${proj.name}` : "—"}{e.startTime && e.endTime ? `${e.startTime}–${e.endTime}` : "—"}{formatHours(e.minutes || 0)}{e.note || ""}
+ + {monthEntries.length === 0 && ( +
Keine Einträge für diesen Monat
+ )} + + ); +} diff --git a/src/utils.js b/src/utils.js new file mode 100755 index 0000000..7e2aba1 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,540 @@ +// ─── UTILS ────────────────────────────────────────────────── + +import { DEFAULT_ABSENZ_TYPES } from "./constants.js"; + +// SIA-Honorar-Formel: p = Z1 + Z2 / ∛B +export function calcSIAHours(baukosten, schwierigkeit, phasen) { + if (!baukosten || baukosten <= 0) return { p: 0, total: 0, phases: [] }; + const cbrtB = Math.cbrt(baukosten); + const p = 0.062 + 10.58 / cbrtB; + const results = phasen.map(ph => { + const items = ph.items.map(it => { + if (it.enabled === false) return { ...it, hours: 0 }; + const r = it.r ?? 1; + const hours = baukosten * (p / 100) * schwierigkeit * (it.pct / 100) * r; + return { ...it, hours: Math.round(hours * 10) / 10 }; + }); + const phaseHours = items.reduce((s, it) => s + it.hours, 0); + return { ...ph, items, hours: phaseHours }; + }); + const total = results.reduce((s, ph) => s + ph.hours, 0); + return { p: Math.round(p * 1000) / 1000, cbrtB, total, phases: results }; +} + +// Manuelle Aufwandschätzung: Stunden pro Rolle × Stundensatz +export function calcManualHours(phases, roles) { + const results = phases.filter(ph => ph.enabled).map(ph => { + const roleDetails = (roles || []).map(r => ({ + ...r, hours: ph.hoursByRole?.[r.id] || 0, + })); + const totalHours = roleDetails.reduce((s, r) => s + r.hours, 0); + const totalAmount = roleDetails.reduce((s, r) => s + r.hours * r.rate, 0); + return { ...ph, roleDetails, totalHours, totalAmount }; + }); + return { + phases: results, + totalHours: results.reduce((s, p) => s + p.totalHours, 0), + totalAmount: results.reduce((s, p) => s + p.totalAmount, 0), + }; +} + +// Berechnet Budget aus linkedQuotes-Array (Migration von sourceQuoteId) +export function migrateLinkedQuotes(project) { + if (project.linkedQuotes) return project.linkedQuotes; + if (project.sourceQuoteId) return [{ quoteId: project.sourceQuoteId, role: "Hauptofferte" }]; + return []; +} + +export function deriveQuoteBudget(linkedQuotes, allQuotes, settingsRoles) { + const quotes = (linkedQuotes || []).map(lq => allQuotes.find(q => q.id === lq.quoteId)).filter(Boolean); + let totalHours = 0; + let totalAmount = 0; + const phaseMap = {}; // phaseId -> hours (summed) + const enabledSet = new Set(); // phases enabled in quotes even with 0 hours + + quotes.forEach(q => { + const roles = q.quoteRoles || settingsRoles || []; + if (q.mode === "sia") { + const calc = calcSIAHours(q.sia?.baukosten, q.sia?.schwierigkeit, q.sia?.phases || []); + totalHours += calc.total || 0; + totalAmount += (calc.total || 0) * (q.sia?.stundenansatz || 0); + (calc.phases || []).forEach(ph => { + if (ph.hours > 0) phaseMap[ph.id] = (phaseMap[ph.id] || 0) + ph.hours; + }); + } else if (q.mode === "manual") { + const calc = calcManualHours(q.manualPhases || [], roles); + totalHours += calc.totalHours || 0; + totalAmount += calc.totalAmount || 0; + (q.manualPhases || []).filter(ph => ph.enabled).forEach(ph => { + enabledSet.add(ph.id); + const h = roles.reduce((s, r) => s + (ph.hoursByRole?.[r.id] || 0), 0); + if (h > 0) phaseMap[ph.id] = (phaseMap[ph.id] || 0) + h; + }); + } else if (q.mode === "free") { + totalAmount += (q.freeItems || []).reduce((s, it) => s + (it.qty * it.price), 0); + } + }); + + const phasesBudget = Object.entries(phaseMap).map(([id, hours]) => ({ id, hours: Math.round(hours * 10) / 10 })); + const enabledPhases = [...new Set([...phasesBudget.map(p => p.id), ...enabledSet])]; + return { + budgetHours: Math.round(totalHours * 10) / 10, + budgetAmount: Math.round(totalAmount), + phasesBudget, + enabledPhases, + hasFreeQuotes: quotes.some(q => q.mode === "free"), + hasHourQuotes: quotes.some(q => q.mode === "sia" || q.mode === "manual"), + }; +} + +export function generateId() { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 6); +} + +export function formatCHF(amount) { + return new Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" }).format(amount || 0); +} + +export function formatDate(dateStr) { + if (!dateStr) return "—"; + return new Date(dateStr).toLocaleDateString("de-CH"); +} + +// Schweizer 5-Rappen-Rundung +export function roundCHF(amount) { + return Math.round((amount || 0) * 20) / 20; +} + +// Absender-Adresse aus strukturierten Feldern bauen; Fallback auf altes Freitext-Feld +export function formatSenderAddress(settings) { + const parts = []; + if (settings.street) parts.push(settings.street); + const line2 = [settings.zip, settings.city].filter(Boolean).join(" "); + if (line2) parts.push(line2); + if (parts.length > 0) return parts.join("\n"); + return settings.address || ""; +} + +// ─── QR-BILL HELPERS ────────────────────────────────────────── + +// Prüft ob IBAN eine QR-IBAN ist (IID im Bereich 30000-31999) +export function isQRIban(iban) { + const clean = (iban || "").replace(/\s/g, "").toUpperCase(); + if (!clean.startsWith("CH") && !clean.startsWith("LI")) return false; + const iid = parseInt(clean.slice(4, 9)); + return iid >= 30000 && iid <= 31999; +} + +// IBAN-Formatierung mit Leerzeichen alle 4 Stellen +export function formatIban(iban) { + const clean = (iban || "").replace(/\s/g, "").toUpperCase(); + return clean.match(/.{1,4}/g)?.join(" ") || ""; +} + +// Modulo-10-Rekursiv Prüfziffer (für QR-Referenz) +export function mod10(input) { + const table = [[0,9,4,6,8,2,7,1,3,5],[9,4,6,8,2,7,1,3,5,0],[4,6,8,2,7,1,3,5,0,9],[6,8,2,7,1,3,5,0,9,4],[8,2,7,1,3,5,0,9,4,6],[2,7,1,3,5,0,9,4,6,8],[7,1,3,5,0,9,4,6,8,2],[1,3,5,0,9,4,6,8,2,7],[3,5,0,9,4,6,8,2,7,1],[5,0,9,4,6,8,2,7,1,3]]; + let carry = 0; + for (const ch of input) { + const digit = parseInt(ch); + if (isNaN(digit)) continue; + carry = table[carry][digit]; + } + return ((10 - carry) % 10).toString(); +} + +// Generiert 27-stellige QR-Referenz aus Rechnungsnummer +export function generateQRReference(invoiceNumber) { + // Nur Ziffern extrahieren, links mit Nullen auf 26 Stellen auffüllen, dann Prüfziffer + const digits = (invoiceNumber || "").replace(/\D/g, "").padStart(26, "0").slice(-26); + return digits + mod10(digits); +} + +// Formatiert Referenz in 5er-Blöcken von rechts +export function formatReference(ref) { + const clean = (ref || "").replace(/\s/g, ""); + const reversed = clean.split("").reverse().join(""); + const blocks = reversed.match(/.{1,5}/g) || []; + return blocks.map(b => b.split("").reverse().join("")).reverse().join(" "); +} + +export function formatHours(minutes) { + const h = Math.floor((minutes || 0) / 60); + const m = (minutes || 0) % 60; + return `${h}h${m > 0 ? " " + m + "m" : ""}`; +} + +// Formatiert Projektnummer nach konfigurierbarem Format +// Platzhalter: YYYY=2025, YY=25, NN=01..99 +export function applyProjectNumberFormat(fmt, seq) { + const now = new Date(); + const yyyy = String(now.getFullYear()); + const yy = yyyy.slice(2); + const nn = String(seq).padStart(2, "0"); + return (fmt || "YYYY/NN").replace(/YYYY/g, yyyy).replace(/YY/g, yy).replace(/NN/g, nn); +} + +// Parst die laufende Nummer aus einer gespeicherten Projektnummer anhand des Formats +export function parseSeqFromNumber(num, fmt) { + if (!num || !fmt) return null; + const now = new Date(); + const yyyy = String(now.getFullYear()); + const yy = yyyy.slice(2); + // Escape regex special chars, then replace placeholders with capture groups + const pattern = (fmt || "YYYY/NN") + .replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") + .replace(/YYYY/g, yyyy) + .replace(/YY/g, yy) + .replace(/NN/g, "(\\d+)"); + const match = num.match(new RegExp("^" + pattern + "$")); + return match ? parseInt(match[1]) : null; +} + +export function exportBuchhaltungCSV(data, year = "") { + const sep = ";"; + const q = (s) => `"${String(s || "").replace(/"/g, '""')}"`; + const mwstRate = data.settings.mwstRate || 8.1; + const rows = []; + + // Einnahmen + rows.push(["EINNAHMEN", "", "", "", "", "", "", ""].map(q).join(sep)); + rows.push(["Datum", "Rechnungs-Nr.", "Kunde", "Beschreibung", "Netto CHF", "MWST-Satz %", "MWST CHF", "Brutto CHF"].map(q).join(sep)); + const paidInvoices = [...data.invoices].filter(i => !year || (i.date || "").startsWith(year)).sort((a, b) => (a.date || "").localeCompare(b.date || "")); + paidInvoices.forEach(inv => { + const client = (data.persons||[]).find(c => c.id === inv.clientId); + const desc = (inv.items || []).map(it => it.desc).filter(Boolean).join(", "); + const taxRate = inv.mwst ? mwstRate : 0; + rows.push([inv.date, inv.number, client?.name || "", desc, (inv.sub || 0).toFixed(2), taxRate, (inv.tax || 0).toFixed(2), (inv.total || 0).toFixed(2)].map(q).join(sep)); + }); + const totalNet = paidInvoices.reduce((s, i) => s + (i.sub || 0), 0); + const totalTax = paidInvoices.reduce((s, i) => s + (i.tax || 0), 0); + const totalGross = paidInvoices.reduce((s, i) => s + (i.total || 0), 0); + rows.push(["", "", "", "TOTAL", totalNet.toFixed(2), "", totalTax.toFixed(2), totalGross.toFixed(2)].map(q).join(sep)); + rows.push([""].join(sep)); + + // Ausgaben + rows.push(["SPESEN (MITARBEITERBEZOGEN)", "", "", "", "", "", "", ""].map(q).join(sep)); + rows.push(["Datum", "Kategorie", "Projekt", "Beschreibung", "Netto CHF", "MWST-Satz %", "MWST CHF", "Brutto CHF"].map(q).join(sep)); + const sortedExp = [...(data.expenses || [])].filter(e => !year || (e.date || "").startsWith(year)).sort((a, b) => (a.date || "").localeCompare(b.date || "")); + sortedExp.forEach(exp => { + const proj = data.projects.find(p => p.id === exp.projectId); + const net = exp.inclMwst ? (exp.amount / (1 + (exp.mwstRate || 0) / 100)) : exp.amount; + const taxAmt = exp.amount - net; + rows.push([exp.date, exp.category, proj?.name || "", exp.description, net.toFixed(2), exp.mwstRate || 0, taxAmt.toFixed(2), exp.amount.toFixed(2)].map(q).join(sep)); + }); + const expTotal = sortedExp.reduce((s, e) => s + (e.amount || 0), 0); + const expTax = sortedExp.reduce((s, e) => { const net = e.inclMwst ? (e.amount / (1 + (e.mwstRate || 0) / 100)) : e.amount; return s + (e.amount - net); }, 0); + rows.push(["", "", "", "TOTAL", (expTotal - expTax).toFixed(2), "", expTax.toFixed(2), expTotal.toFixed(2)].map(q).join(sep)); + rows.push([""].join(sep)); + + // Interne Ausgaben + rows.push(["INTERNE AUSGABEN", "", "", "", "", "", "", ""].map(q).join(sep)); + rows.push(["Datum", "Kategorie", "Beschreibung", "Wiederkehrend", "Netto CHF", "MWST-Satz %", "MWST CHF", "Brutto CHF"].map(q).join(sep)); + const sortedIntExp = [...(data.internalExpenses || [])].filter(e => !year || (e.date || "").startsWith(year)).sort((a, b) => (a.date || "").localeCompare(b.date || "")); + sortedIntExp.forEach(exp => { + const net = exp.inclMwst ? (exp.amount / (1 + (exp.mwstRate || 0) / 100)) : exp.amount; + const taxAmt = exp.amount - net; + rows.push([exp.date, exp.category, exp.description, exp.recurring ? (exp.recurringInterval || "monatlich") : "", net.toFixed(2), exp.mwstRate || 0, taxAmt.toFixed(2), exp.amount.toFixed(2)].map(q).join(sep)); + }); + const intExpTotal = sortedIntExp.reduce((s, e) => s + (e.amount || 0), 0); + const intExpTax = sortedIntExp.reduce((s, e) => { const net = e.inclMwst ? (e.amount / (1 + (e.mwstRate || 0) / 100)) : e.amount; return s + (e.amount - net); }, 0); + rows.push(["", "", "", "TOTAL", (intExpTotal - intExpTax).toFixed(2), "", intExpTax.toFixed(2), intExpTotal.toFixed(2)].map(q).join(sep)); + rows.push([""].join(sep)); + + // Personalaufwand (Löhne) + const sortedLoehne = [...(data.lohnEntries || [])].filter(l => !year || l.monat.startsWith(year)).sort((a, b) => a.monat.localeCompare(b.monat)); + if (sortedLoehne.length > 0) { + rows.push(["PERSONALAUFWAND / LÖHNE", "", "", "", "", "", "", "", ""].map(q).join(sep)); + rows.push(["Monat", "Mitarbeiter", "Brutto", "Abzüge AN", "Netto", "Spesen", "Auszahlung AN", "PK AG-Anteil", "Gesamtlohnkosten"].map(q).join(sep)); + sortedLoehne.forEach(l => { + const name = l.empSnapshot?.name || ""; + const bvgAG = l.bvgAG || 0; + const gesamt = (l.auszahlung || 0) + bvgAG; + rows.push([l.monat, name, (l.bruttoTotal || 0).toFixed(2), (l.totalAbzuege || 0).toFixed(2), (l.netto || 0).toFixed(2), (l.spesenTotal || 0).toFixed(2), (l.auszahlung || 0).toFixed(2), bvgAG.toFixed(2), gesamt.toFixed(2)].map(q).join(sep)); + }); + const lohnTotal = sortedLoehne.reduce((s, l) => s + (l.auszahlung || 0) + (l.bvgAG || 0), 0); + rows.push(["", "", "", "", "", "", "", "TOTAL", lohnTotal.toFixed(2)].map(q).join(sep)); + rows.push([""].join(sep)); + } + + // Zusammenfassung + const lohnTotalAll = sortedLoehne.reduce((s, l) => s + (l.auszahlung || 0) + (l.bvgAG || 0), 0); + rows.push(["ZUSAMMENFASSUNG", "", "", "", "", "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "Einnahmen (Netto)", totalNet.toFixed(2), "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "Spesen (Netto)", (expTotal - expTax).toFixed(2), "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "Interne Ausgaben (Netto)", (intExpTotal - intExpTax).toFixed(2), "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "Personalaufwand (Löhne)", lohnTotalAll.toFixed(2), "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "Ergebnis (Netto)", (totalNet - (expTotal - expTax) - (intExpTotal - intExpTax) - lohnTotalAll).toFixed(2), "", "", ""].map(q).join(sep)); + rows.push(["", "", "", "MWST auf Einnahmen", "", "", totalTax.toFixed(2), ""].map(q).join(sep)); + rows.push(["", "", "", "Vorsteuer (Spesen + Ausgaben)", "", "", (expTax + intExpTax).toFixed(2), ""].map(q).join(sep)); + rows.push(["", "", "", "MWST-Schuld", "", "", (totalTax - expTax - intExpTax).toFixed(2), ""].map(q).join(sep)); + + const bom = ""; + const blob = new Blob([bom + rows.join("\n")], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `buchhaltung-${year || "gesamt"}.csv`; + a.click(); + URL.revokeObjectURL(url); +} + +export function buildReminderLetter(inv, nr, sentDate, clients, settings) { + const client = clients.find(c => c.id === inv.clientId); + const existingReminders = inv.reminders || []; + const daysPast = Math.floor((new Date() - new Date(inv.dueDate)) / 86400000); + const intro = nr === 1 + ? `Bei einer Überprüfung unserer Buchhaltung stellen wir fest, dass die Rechnung Nr. ${inv.number} vom ${formatDate(inv.date)} über ${formatCHF(inv.total)} seit ${daysPast} Tagen (Fälligkeit: ${formatDate(inv.dueDate)}) noch nicht beglichen ist.\n\nWir bitten Sie höflich, den offenen Betrag innert 10 Tagen zu überweisen.` + : nr === 2 + ? `Leider mussten wir feststellen, dass unsere Zahlungserinnerung vom ${formatDate(existingReminders[0]?.sentDate || existingReminders[0]?.date)} bezüglich Rechnung Nr. ${inv.number} über ${formatCHF(inv.total)} bisher ohne Reaktion geblieben ist.\n\nWir fordern Sie hiermit auf, den ausstehenden Betrag innert 7 Tagen zu begleichen.` + : `Trotz unserer Zahlungserinnerungen vom ${existingReminders.map(r => formatDate(r.sentDate || r.date)).join(" und ")} ist die Rechnung Nr. ${inv.number} über ${formatCHF(inv.total)} nach wie vor unbeglichen.\n\nWir sehen uns gezwungen, bei weiterem Ausbleiben der Zahlung innerhalb von 5 Tagen rechtliche Schritte einzuleiten.`; + const body = `Sehr geehrte/r ${client?.name || "[Kunde]"}\n\n${intro}\n\nIBAN: ${settings.iban}\nReferenz: ${inv.number}\n\nFreundliche Grüsse\n${settings.name}`; + const subject = nr === 1 ? `Zahlungserinnerung – Rechnung Nr. ${inv.number}` : `${nr}. Mahnung – Rechnung Nr. ${inv.number}`; + return { client, subject, body }; +} + +export function getKW(dateStr) { + if (!dateStr) return "—"; + const d = new Date(dateStr); + const jan4 = new Date(d.getFullYear(), 0, 4); + const startOfWeek = new Date(jan4); + startOfWeek.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7)); + const kw = Math.ceil(((d - startOfWeek) / 86400000 + 1) / 7); + return `KW ${kw}`; +} + +export function linkedClientForNote(n, data) { + if (n.clientId) return (data.persons||[]).find(c => c.id === n.clientId) || null; + return n.clientManual ? { name: n.clientManual, address: n.deliveryAddress || "" } : null; +} + +export function getWeekNumber(date) { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7)); + const week1 = new Date(d.getFullYear(), 0, 4); + return Math.round(((d - week1) / 86400000 + ((week1.getDay() + 6) % 7) - 2) / 7) + 1; +} + +export function formatKW(dateStr) { + if (!dateStr) return "—"; + return `KW ${getWeekNumber(dateStr)} / ${new Date(dateStr).getFullYear()}`; +} + +// Generiert nächste Protokollnummer (legacy, nicht mehr direkt genutzt) +export function nextProtoNumber(protocols, projectNumber) { + const prefix = projectNumber ? `${projectNumber}-P` : "P"; + const existing = (protocols || []).map(p => { + const m = (p.nummer || p.number || "").match(/(\d+)$/); + return m ? parseInt(m[1]) : 0; + }); + const max = existing.length ? Math.max(...existing) : 0; + return `${prefix}${String(max + 1).padStart(2, "0")}`; +} + +// Nächste Sequenznummer aus bestehenden Protokollnummern ableiten +// Ignoriert Jahr-artige Zahlen (4-stellig, 2000–2099) +export function nextProtoSeq(protocols) { + let max = 0; + (protocols || []).forEach(p => { + const groups = (p.nummer || "").match(/\d+/g) || []; + groups.forEach(n => { + const num = parseInt(n); + if (n.length <= 3 || !(num >= 2000 && num <= 2099)) { + if (num > max) max = num; + } + }); + }); + return max + 1; +} + +// Wendet konfigurierbares Protokollnummer-Format an +// Platzhalter: YYYY YY MM DD PP(Projektnr.) TT(Typkürzel) NN/NNN(Sequenz) +export function applyProtoNumberFormat(fmt, { date, projectNumber, seq, typKuerzel }) { + const d = date ? new Date(date + "T12:00:00") : new Date(); + const yyyy = String(d.getFullYear()); + const yy = yyyy.slice(2); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + // Normalize separators in project number, then strip a leading year segment + // so PP can be combined with YYYY/YY without producing a double year. + const ppNorm = (projectNumber || "") + .replace(/[\/\s.]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); + const pp4 = ppNorm.replace(/^\d{4}-/, ""); + const pp2 = ppNorm.replace(/^\d{2}-/, ""); + const ppStripped = pp4 !== ppNorm ? pp4 : pp2 !== ppNorm ? pp2 : ppNorm; + const pp = ppStripped || ppNorm || "P"; + const tt = typKuerzel || "SO"; + const tokens = { + YYYY: yyyy, YY: yy, MM: mm, DD: dd, + PPP: ppNorm || "P", PP: pp, TT: tt, + NNN: String(seq).padStart(3, "0"), + NN: String(seq).padStart(2, "0"), + }; + return (fmt || "PP-TT-NN").replace(/YYYY|YY|NNN|NN|MM|DD|PPP|PP|TT/g, m => tokens[m] ?? m); +} + +// Convert plain text (legacy templates) to HTML +export function textToHtml(text) { + if (!text) return ""; + if (text.trim().startsWith("<")) return text; // already HTML + return text + .split("\n\n") + .map(para => `

${para.replace(/\n/g, "
")}

`) + .join(""); +} + +// Convert HTML back to plain text for print (fallback) +export function htmlToText(html) { + const div = document.createElement("div"); + div.innerHTML = html; + return div.innerText || div.textContent || ""; +} + +export function getFeiertageForYear(feiertage, year) { + return (feiertage || []).filter(f => { + if (f.repeatsYearly) return true; + return f.date.startsWith(String(year)); + }).map(f => { + if (f.repeatsYearly) return { ...f, date: `${year}-${f.date.slice(5, 10)}` }; + return f; + }); +} + +export function getAbsenzTypes(data) { + const custom = data.absenzTypes || []; + const overrideMap = new Map(custom.map(t => [t.id, t])); + const defaultIds = new Set(DEFAULT_ABSENZ_TYPES.map(t => t.id)); + const defaults = DEFAULT_ABSENZ_TYPES + .map(t => overrideMap.get(t.id) || t) + .filter(t => !overrideMap.get(t.id)?.deleted); + const newCustom = custom.filter(t => !defaultIds.has(t.id) && !t.deleted); + return [...defaults, ...newCustom]; +} + +export function getWorkdaysInMonth(year, month, feiertage) { + // month: 0-based; use ISO string to avoid local-midnight timezone offset + const days = []; + const monthStr = `${year}-${String(month + 1).padStart(2, "0")}`; + const d = new Date(`${monthStr}-01`); + while (d.toISOString().slice(0, 7) === monthStr) { + const ds = d.toISOString().slice(0, 10); + const dow = d.getDay(); + if (dow !== 0 && dow !== 6) { + const ft = (feiertage || []).find(f => f.date === ds); + days.push({ date: ds, feiertag: ft || null }); + } + d.setDate(d.getDate() + 1); + } + return days; +} + +export function getSollStunden(employee, date, feiertage) { + // Returns Soll-Stunden for a given workday + const pensum = (employee.pensum || 100) / 100; + const wochenstunden = (employee.wochenstunden || 35); + const tagessoll = (wochenstunden * pensum) / 5; + const ft = (feiertage || []).find(f => f.date === date); + if (ft) return tagessoll + (ft.stundenDelta || 0); // e.g. -1 for Halbfeiertag, 0 for full + return tagessoll; +} + +// ─── LOHNBERECHNUNG HELPER ────────────────────────────────────── +export function calcLohn(emp, monat, spesen, bonus) { + const pensumFactor = (emp.pensum || 100) / 100; + const bruttoBase = emp.monatslohn || 0; + const brutto = Math.round(bruttoBase * pensumFactor * 100) / 100; + // 13. Monatslohn: 1/12 pro Monat + const dreizehnter = emp.dreizehnterLohn ? Math.round(brutto / 12 * 100) / 100 : 0; + const bonusBetrag = Math.round((bonus || 0) * 100) / 100; + const bruttoTotal = brutto + dreizehnter + bonusBetrag; // Bonus ist AHV-pflichtig + + const ahv = Math.round(bruttoTotal * ((emp.ahvSatz ?? 5.3) / 100) * 100) / 100; + const alv = Math.round(bruttoTotal * ((emp.alvSatz ?? 1.1) / 100) * 100) / 100; + const bvg = Math.round(bruttoTotal * ((emp.bvgSatz ?? 8.0) / 100) * 100) / 100; + const nbu = Math.round(bruttoTotal * ((emp.nbuSatz ?? 1.5) / 100) * 100) / 100; + const ktg = Math.round(bruttoTotal * ((emp.ktgSatz ?? 0.5) / 100) * 100) / 100; + const qst = emp.quellensteuerPflichtig + ? Math.round(bruttoTotal * ((emp.quellensteuerSatz ?? 10) / 100) * 100) / 100 : 0; + const bvgAG = Math.round(bruttoTotal * ((emp.pkAGSatz ?? 8.0) / 100) * 100) / 100; + + const totalAbzuege = ahv + alv + bvg + nbu + ktg + qst; + const netto = Math.round((bruttoTotal - totalAbzuege) * 100) / 100; + const spesenTotal = Math.round((spesen || []).reduce((s, e) => s + (e.amount || 0), 0) * 100) / 100; + const auszahlung = Math.round((netto + spesenTotal) * 100) / 100; + + return { brutto, bruttoBase, dreizehnter, bonusBetrag, bruttoTotal, ahv, alv, bvg, nbu, ktg, qst, totalAbzuege, netto, spesenTotal, auszahlung, bvgAG }; +} + +// ─── Dashboard layout helpers ────────────────────────────────────────────────── + +const KPI_IDS = ["kpi-projekte","kpi-stunden","kpi-ausstehend","kpi-umsatz"]; +const MID_IDS = ["aktive-projekte","unverrechnete-stunden","umsatz-sparkline","offene-offerten"]; +const FULL_IDS = ["warnungen","letzte-zeiteintraege","meine-zeiteintraege"]; + +export function widgetsToRows(widgetIds) { + const uid = () => Math.random().toString(36).slice(2, 8); + const kpi = widgetIds.filter(w => KPI_IDS.includes(w)); + const mid = widgetIds.filter(w => MID_IDS.includes(w)); + const full = widgetIds.filter(w => FULL_IDS.includes(w)); + const rows = []; + if (kpi.length) rows.push({ id: uid(), cols: Math.min(4, Math.max(2, kpi.length)), minH: 0, widgets: kpi }); + for (let i = 0; i < mid.length; i += 3) { + const chunk = mid.slice(i, i + 3); + rows.push({ id: uid(), cols: Math.max(2, chunk.length), minH: 0, widgets: chunk }); + } + full.forEach(w => rows.push({ id: uid(), cols: 1, minH: 0, widgets: [w] })); + return rows; +} + +export function migrateDashboardLayout(val) { + if (!val || !Array.isArray(val) || val.length === 0) return null; + if (typeof val[0] === "object" && "widgets" in val[0]) return val; + return widgetsToRows(val); +} + +// PDF-Dateiname aus Format und Content ableiten +export function buildPdfName(format, content, settings) { + const studio = (settings?.name || "RAPPORT").replace(/\s+/g, "-"); + const typeLabels = { + "invoice": "Rechnung", "invoice+qr": "Rechnung", "qrbill": "QR-Rechnung", + "quote": "Offerte", "lieferschein": "Lieferschein", "protokoll": "Protokoll", + "lohn": "Lohnabrechnung", "letter": "Brief", "projectDetail": "Projekt", + "projectsOverview": "Projekte", "buchhaltung": "Buchhaltung", "studioBudget": "Budget", + }; + const typ = typeLabels[content?.type] || content?.type || "Dokument"; + let nummer = ""; + let kunde = ""; + let datum = new Date().toISOString().slice(0, 10); + if (content?.inv) { + nummer = content.inv.number || ""; + datum = content.inv.date || datum; + kunde = content.client?.company || content.client?.name || ""; + } else if (content?.quote) { + nummer = content.quote.number || ""; + datum = content.quote.date || datum; + kunde = content.client?.company || content.client?.name || ""; + } else if (content?.note) { + nummer = content.note.number || ""; + kunde = content.client?.company || content.client?.name || ""; + } else if (content?.protokoll) { + datum = content.protokoll.date || datum; + nummer = content.protokoll.number || ""; + } else if (content?.emp) { + kunde = content.emp.name || ""; + nummer = content.monatLabel || ""; + } else if (content?.filterYear) { + nummer = String(content.filterYear); + } + const sanitize = (s) => (s || "").replace(/[^a-zA-Z0-9äöüÄÖÜ_\-\.]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); + const fmt = format || "{studio}_{typ}_{nummer}"; + const result = fmt + .replace("{studio}", sanitize(studio)) + .replace("{typ}", sanitize(typ)) + .replace("{nummer}", sanitize(nummer)) + .replace("{kunde}", sanitize(kunde)) + .replace("{datum}", sanitize(datum)); + return result.replace(/-+/g, "-").replace(/^-|-$/g, "") || "RAPPORT"; +} diff --git a/src/views/Buchhaltung.jsx b/src/views/Buchhaltung.jsx new file mode 100755 index 0000000..f294418 --- /dev/null +++ b/src/views/Buchhaltung.jsx @@ -0,0 +1,374 @@ +import React, { useState } from "react"; +import { formatCHF, formatDate, exportBuchhaltungCSV } from "../utils.js"; +import { Header, StatusBadge } from "../components/UI.jsx"; +import { MahnModal } from "./Protokolle.jsx"; +import { ReceiptViewer } from "./Spesen.jsx"; + +export default +function Buchhaltung({ data, update, setView, setPrintContent }) { + const mwstRate = data.settings.mwstRate || 8.1; + const currentYear = new Date().getFullYear().toString(); + const [filterYear, setFilterYear] = useState(currentYear); + + const availableYears = Array.from(new Set([ + ...data.invoices.map(i => (i.date || "").slice(0, 4)), + ...(data.expenses || []).map(e => (e.date || "").slice(0, 4)), + ].filter(Boolean))).sort().reverse(); + if (!availableYears.includes(currentYear)) availableYears.unshift(currentYear); + + // Gefilterte Daten + const invoices = data.invoices.filter(i => !filterYear || (i.date || "").startsWith(filterYear)); + const expenses = (data.expenses || []).filter(e => !filterYear || (e.date || "").startsWith(filterYear)); + const lohnEntries = (data.lohnEntries || []).filter(l => !filterYear || l.monat.startsWith(filterYear)); + + // Einnahmen + const totalInvoiced = invoices.reduce((s, i) => s + (i.sub || 0), 0); + const totalTax = invoices.reduce((s, i) => s + (i.tax || 0), 0); + // Akonto-MwSt ist erst bei Schlussrechnung steuerrelevant — separat ausweisen + const akontoInvoices = invoices.filter(i => i.invoiceKind === "akonto"); + const akontoTax = akontoInvoices.reduce((s, i) => s + (i.tax || 0), 0); + const akontoSub = akontoInvoices.reduce((s, i) => s + (i.sub || 0), 0); + const taxWithoutAkonto = totalTax - akontoTax; + const totalPaid = invoices.filter(i => i.status === "bezahlt").reduce((s, i) => s + (i.total || 0), 0); + const totalOpen = invoices.filter(i => i.status === "gesendet" || i.status === "entwurf").reduce((s, i) => s + (i.total || 0), 0); + const totalOverdue = invoices.filter(i => i.status === "überfällig").reduce((s, i) => s + (i.total || 0), 0); + const totalDraftSub = invoices.filter(i => i.status === "entwurf").reduce((s, i) => s + (i.sub || 0), 0); + const totalBilledSub = totalInvoiced - totalDraftSub; + + // Ausgaben Spesen + const totalExpBrutto = expenses.reduce((s, e) => s + (e.amount || 0), 0); + const totalExpNet = expenses.reduce((s, e) => { const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; return s + net; }, 0); + const totalExpTax = totalExpBrutto - totalExpNet; + + // Interne Ausgaben + const internalExpenses = (data.internalExpenses || []).filter(e => !filterYear || (e.date || "").startsWith(filterYear)); + const totalIntExpBrutto = internalExpenses.reduce((s, e) => s + (e.amount || 0), 0); + const totalIntExpNet = internalExpenses.reduce((s, e) => { const net = e.inclMwst ? e.amount / (1 + (e.mwstRate || 0) / 100) : e.amount; return s + net; }, 0); + const totalIntExpTax = totalIntExpBrutto - totalIntExpNet; + + // Personalaufwand (abgeschlossene Lohnabrechnungen — Auszahlung an MA + AG-Abgaben) + const totalLoehne = lohnEntries.reduce((s, l) => s + (l.auszahlung || 0), 0); + const totalLoehneAGAbgaben = lohnEntries.reduce((s, l) => s + (l.bvgAG || 0), 0); + const totalLoehneGesamt = totalLoehne + totalLoehneAGAbgaben; + const totalLoehneAnzahl = lohnEntries.length; + + const totalAusgaben = totalExpNet + totalIntExpNet + totalLoehneGesamt; + const result = totalInvoiced - totalAusgaben; + + // Überfällige Rechnungen für Mahnwesen + const today = new Date().toISOString().slice(0, 10); + const [mahnModal, setMahnModal] = useState(null); + const [mahnMode, setMahnMode] = useState("new"); + const [mahnSentDate, setMahnSentDate] = useState(new Date().toISOString().slice(0, 10)); + const [receiptView, setReceiptView] = useState(null); + + const overdueInvoices = data.invoices.filter(i => + (i.status === "gesendet" || i.status === "überfällig") && i.dueDate && i.dueDate < today + ).sort((a, b) => (a.dueDate || "").localeCompare(b.dueDate || "")); + + const sendReminder = (inv) => { + const reminders = inv.reminders || []; + setMahnMode(reminders.length === 0 ? "new" : "reprint"); + setMahnSentDate(new Date().toISOString().slice(0, 10)); + setMahnModal({ inv }); + }; + + return ( +
+
+ + + +
+ } /> + + {/* KPI-Karten */} +
+ {[ + { label: "UMSATZ NETTO", value: formatCHF(totalInvoiced), sub: totalDraftSub > 0 ? `${invoices.length} Rechnungen — davon ${formatCHF(totalDraftSub)} erwartet` : `${invoices.length} Rechnungen`, color: "#2d6a4f" }, + { label: "DAVON BEZAHLT", value: formatCHF(totalPaid), sub: "eingegangen", color: "#2d6a4f" }, + { label: "OFFEN / ÜBERFÄLLIG", value: formatCHF(totalOpen + totalOverdue), sub: `${overdueInvoices.length} überfällig`, color: totalOverdue > 0 ? "#8a1a1a" : "#7a6a00" }, + { label: "AUSGABEN TOTAL", value: formatCHF(totalAusgaben), sub: `Spesen + ${totalLoehneAnzahl} Lohnabrechnungen (inkl. AG-Abgaben)`, color: "#555" }, + ].map(c => ( +
+
{c.label}
+
{c.value}
+
{c.sub}
+
+ ))} +
+ +
+ {/* Ergebnis-Übersicht */} +
+
JAHRESERGEBNIS {filterYear || "GESAMT"}
+ {[ + { label: "Einnahmen (Netto)", value: totalInvoiced, bold: false }, + totalDraftSub > 0 && { label: "→ Davon fakturiert", value: totalBilledSub, note: true }, + totalDraftSub > 0 && { label: "→ Davon erwartet (Entwürfe)", value: totalDraftSub, note: true, expected: true }, + { label: "Spesen (Netto)", value: -totalExpNet, bold: false }, + { label: "Interne Ausgaben (Netto)", value: -totalIntExpNet, bold: false }, + { label: "Personalaufwand (Löhne)", value: -totalLoehne, bold: false }, + totalLoehneAGAbgaben > 0 && { label: "→ AG-Sozialabgaben (PK/BVG)", value: -totalLoehneAGAbgaben, note: true }, + { label: "Ergebnis vor MWST", value: result, bold: true, sep: true }, + { label: `MWST auf Einnahmen (${mwstRate}%, excl. Akonto)`, value: taxWithoutAkonto, bold: false, small: true }, + akontoTax > 0 && { label: `↳ Akonto-MWST ausstehend (bei Schlussrechn.)`, value: akontoTax, bold: false, small: true, pending: true }, + { label: "Vorsteuer (Spesen + Ausgaben)", value: -(totalExpTax + totalIntExpTax), bold: false, small: true }, + { label: "MWST-Schuld (excl. Akonto)", value: taxWithoutAkonto - totalExpTax - totalIntExpTax, bold: true, small: true }, + ].filter(Boolean).map((row, i) => ( +
+ + {row.label} + {row.expected && noch nicht fakturiert} + {row.pending && ausstehend} + + + {formatCHF(row.value)} + +
+ ))} +
+ + {/* Monatsumsatz */} +
+
MONATSUMSATZ {filterYear || new Date().getFullYear()}
+ {(() => { + const year = filterYear || String(new Date().getFullYear()); + const months = ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]; + const monthData = months.map((label, i) => { + const key = `${year}-${String(i + 1).padStart(2, "0")}`; + const invs = data.invoices.filter(inv => (inv.date || "").startsWith(key)); + const paid = invs.filter(inv => inv.status === "bezahlt").reduce((s, inv) => s + (inv.sub || 0), 0); + const open = invs.filter(inv => inv.status === "gesendet" || inv.status === "überfällig").reduce((s, inv) => s + (inv.sub || 0), 0); + const draft = invs.filter(inv => inv.status === "entwurf").reduce((s, inv) => s + (inv.sub || 0), 0); + return { label, key, paid, open, draft, total: paid + open + draft }; + }); + const maxVal = Math.max(...monthData.map(m => m.total), 1); + const yearTotal = monthData.reduce((s, m) => s + m.total, 0); + const currentMonth = new Date().toISOString().slice(0, 7); + return ( + <> +
+ Total {formatCHF(yearTotal)} · {data.invoices.filter(i => (i.date||"").startsWith(year)).length} Rechnungen +
+
+ {monthData.map(m => ( +
+
+ {m.total === 0 ? ( +
+ ) : ( + <> + {m.draft > 0 &&
} + {m.open > 0 &&
} + {m.paid > 0 &&
} + + )} +
+
+ ))} +
+
+ {monthData.map(m => ( +
{m.label}
+ ))} +
+
+ {[ + { color: "#2d6a4f", label: "Bezahlt" }, + { color: "#b07848", label: "Ausstehend" }, + { color: "#ccc", label: "Entwurf" }, + ].map(l => ( +
+
+ {l.label} +
+ ))} +
+ + ); + })()} +
+
+ + {/* Mahnwesen */} +
+
+ MAHNWESEN — ÜBERFÄLLIGE RECHNUNGEN + {overdueInvoices.length === 0 && ✓ Keine überfälligen Rechnungen} +
+ {overdueInvoices.length > 0 && ( + + + + + + {overdueInvoices.map(inv => { + const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); + const daysPast = Math.floor((new Date() - new Date(inv.dueDate)) / 86400000); + const reminders = inv.reminders || []; + const nextNr = reminders.length + 1; + const mahnLabel = nextNr === 1 ? "Zahlungserinnerung" : `${nextNr}. Mahnung`; + const mahnColor = nextNr >= 3 ? "#8a1a1a" : nextNr === 2 ? "#b5621e" : "#7a6a00"; + return ( + + + + + + + + + ); + })} + +
Nr.KundeFällig seitMahnungenBetrag
{inv.number}{client?.name || "—"} + 30 ? "#8a1a1a" : "#b5621e", fontWeight: 500 }}> + {formatDate(inv.dueDate)} ({daysPast} Tage) + + + {reminders.length === 0 ? ( + Keine + ) : ( +
+ {reminders.map((r, i) => ( + + {i === 0 ? "Erinnerung" : `${i + 1}. Mahnung`} · {formatDate(r.date)} + + ))} +
+ )} +
{formatCHF(inv.total)} + +
+ )} +
+ + {/* Letzte Rechnungen */} +
+
+
LETZTE RECHNUNGEN
+ +
+ + + + {[...data.invoices].sort((a, b) => (b.date || "").localeCompare(a.date || "")).slice(0, 6).map(inv => { + const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === inv.clientId); + return ( + + + + + + + + ); + })} + +
Nr.KundeDatumBetragStatus
{inv.number}{client?.name || "—"}{formatDate(inv.date)}{formatCHF(inv.total)}
+
+ + {/* Spesen & Belege */} + {expenses.length > 0 && ( +
+
+
+ SPESEN {filterYear || ""} + + {expenses.filter(e => e.receiptData).length} von {expenses.length} mit Beleg + +
+ +
+ + + + + + + + + + + + + + {[...expenses].sort((a, b) => (b.date || "").localeCompare(a.date || "")).map(e => { + const emp = (data.employees || []).find(em => em.id === e.employeeId); + const expStatus = e.status || "offen"; + const statusColors = { offen: "#b07848", genehmigt: "#2d6a4f", "auf nächsten Lohn": "#1a4e8a", ausbezahlt: "#2d6a4f" }; + return ( + + + + + + + + + + ); + })} + + + + + + + + +
DatumKategorieBeschreibungMitarbeiterBruttoBelegStatus
{formatDate(e.date)}{e.category}{e.description || "—"}{emp?.name || "—"}{formatCHF(e.amount)} + {e.receiptData ? ( + + ) : ( + + )} + + + {expStatus.charAt(0).toUpperCase() + expStatus.slice(1)} + +
{expenses.length} Einträge{formatCHF(totalExpBrutto)}
+
+ )} + + {/* Mahnung-Dialog */} + {mahnModal && ( + setMahnModal(null)} + mahnMode={mahnMode} + setMahnMode={setMahnMode} + mahnSentDate={mahnSentDate} + setMahnSentDate={setMahnSentDate} + /> + )} + setReceiptView(null)} /> +
+ ); +} diff --git a/src/views/Clients.jsx b/src/views/Clients.jsx new file mode 100755 index 0000000..bdffdf4 --- /dev/null +++ b/src/views/Clients.jsx @@ -0,0 +1,452 @@ +import React, { useState } from "react"; +import { generateId } from "../utils.js"; +import { Header, Modal, FormField, useConfirm } from "../components/UI.jsx"; + +export default +function Clients({ data, update, modal, setModal, setView }) { + const clients = data.clients || []; + const { askConfirm, ConfirmModalEl } = useConfirm(); + + const [selectedId, setSelectedId] = useState(() => { + const id = window.__navToClient || null; + window.__navToClient = null; + return id; + }); + const [search, setSearch] = useState(""); + const [groupBy, setGroupBy] = useState("alpha"); + const [contactModal, setContactModal] = useState(null); + const [contactForm, setContactForm] = useState({ name: "", position: "", email: "", phone: "" }); + const [showHauptPicker, setShowHauptPicker] = useState(false); + + const emptyForm = { + name: "", street: "", zip: "", city: "", country: "CH", + email: "", phone: "", website: "", + contacts: [], + _contactName: "", _contactPosition: "", + }; + const [form, setForm] = useState(emptyForm); + + const selectedClient = clients.find(c => c.id === selectedId) || null; + + // ── Client speichern ── + const save = () => { + if (!form.name.trim()) return; + const { _contactName, _contactPosition, ...clientData } = form; + let contacts = clientData.contacts || []; + if (_contactName.trim() && !modal?.id) { + contacts = [{ id: generateId(), name: _contactName.trim(), position: _contactPosition.trim(), email: "", phone: "" }]; + } + const client = { ...clientData, contacts, id: modal?.id || generateId() }; + update("clients", modal?.id ? clients.map(c => c.id === modal.id ? client : c) : [...clients, client]); + setModal(null); + }; + + const openNew = () => { setForm(emptyForm); setModal({ type: "client" }); }; + const openEdit = (c) => { + setForm({ ...emptyForm, ...c, _contactName: "", _contactPosition: "" }); + setModal({ type: "client", id: c.id }); + }; + const del = async (id) => { + if (await askConfirm("Kunde löschen? Alle zugehörigen Projekte verlieren die Kundenzuordnung.")) { + update("clients", clients.filter(c => c.id !== id)); + if (selectedId === id) setSelectedId(null); + } + }; + + // ── Kontakt speichern ── + const saveContact = () => { + if (!contactForm.name.trim()) return; + const client = clients.find(c => c.id === contactModal.clientId); + if (!client) return; + const contacts = client.contacts || []; + const updated = contactModal.contactId + ? contacts.map(ct => ct.id === contactModal.contactId ? { ...ct, ...contactForm } : ct) + : [...contacts, { ...contactForm, id: generateId() }]; + update("clients", clients.map(c => c.id === client.id ? { ...c, contacts: updated } : c)); + setContactModal(null); + }; + const delContact = async (clientId, contactId) => { + if (await askConfirm("Kontaktperson löschen?")) { + const client = clients.find(c => c.id === clientId); + update("clients", clients.map(c => c.id === clientId ? { ...c, contacts: (c.contacts || []).filter(ct => ct.id !== contactId) } : c)); + } + }; + + // ── Detail-Ansicht ── + if (selectedId && selectedClient) { + const projs = (data.projects || []).filter(p => p.clientId === selectedId).sort((a, b) => (b.startDate || "").localeCompare(a.startDate || "")); + const invoices = (data.invoices || []).filter(i => i.clientId === selectedId).sort((a, b) => (b.date || "").localeCompare(a.date || "")); + const quotes = (data.quotes || []).filter(q => q.clientId === selectedId).sort((a, b) => (b.date || "").localeCompare(a.date || "")); + const contacts = selectedClient.contacts || []; + const hauptkontakt = contacts[0] || null; + const addressLine = [selectedClient.street, [selectedClient.zip, selectedClient.city].filter(Boolean).join(" ")].filter(Boolean).join(", "); + + const navTo = (view) => { window.__navClientId = selectedId; setView(view); }; + + const formatCHF = (v) => v != null ? `CHF ${Number(v).toLocaleString("de-CH", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"; + const fmtDate = (s) => s ? new Date(s).toLocaleDateString("de-CH") : "—"; + + return ( +
+ {ConfirmModalEl} + + +
+
+

{selectedClient.name}

+ {addressLine &&
{addressLine}
} +
+ +
+ +
+ {/* Firmeninfo */} +
+
FIRMENINFO
+ {[ + { label: "E-Mail", value: selectedClient.email, href: `mailto:${selectedClient.email}` }, + { label: "Telefon", value: selectedClient.phone }, + { label: "Website", value: selectedClient.website, href: selectedClient.website?.startsWith("http") ? selectedClient.website : selectedClient.website ? `https://${selectedClient.website}` : null }, + { label: "Adresse", value: addressLine || null }, + ].filter(r => r.value).map(({ label, value, href }) => ( +
+ {label} + {href ? {value} : {value}} +
+ ))} + {contacts.length > 0 && ( +
+
+
HAUPTKONTAKT
+ {contacts.length > 1 && ( + + )} +
+ {showHauptPicker ? ( +
+ {contacts.map((ct, i) => ( + + ))} +
+ ) : hauptkontakt ? ( + <> +
{hauptkontakt.name}
+ {hauptkontakt.position &&
{hauptkontakt.position}
} +
+ {hauptkontakt.email && {hauptkontakt.email}} + {hauptkontakt.phone && {hauptkontakt.phone}} +
+ + ) : null} +
+ )} +
+ + {/* Ansprechpartner */} +
+
0 ? "1px solid #ece8e2" : "none" }}> +
ANSPRECHPARTNER ({contacts.length})
+ +
+ {contacts.length === 0 ? ( +
Noch keine Ansprechpartner erfasst.
+ ) : ( + contacts.map((ct, i) => ( +
+
+
+
+ {ct.name} + {i === 0 && HAUPT} +
+ {ct.position &&
{ct.position}
} +
+ {ct.email && {ct.email}} + {ct.phone && {ct.phone}} +
+
+
+ + +
+
+
+ )) + )} +
+
+ + {/* Projekte */} +
+
0 ? "1px solid #ece8e2" : "none" }}> + PROJEKTE ({projs.length}) + {projs.length > 0 && } +
+ {projs.length === 0 + ?
Noch keine Projekte.
+ : <> + + + + {projs.slice(0, 5).map(p => ( + + + + + + + ))} + +
ProjektKategorieStatusBudget
{p.name}{p.number && {p.number}}{p.category || "—"}{p.status}{p.budget > 0 ? formatCHF(p.budget) : "—"}
+ {projs.length > 5 &&
+{projs.length - 5} weitere —
} + + } +
+ + {/* Rechnungen */} +
+
0 ? "1px solid #ece8e2" : "none" }}> + RECHNUNGEN ({invoices.length}) + {invoices.length > 0 && } +
+ {invoices.length === 0 + ?
Noch keine Rechnungen.
+ : <> + + + + {invoices.slice(0, 5).map(inv => { + const proj = inv.projectId ? (data.projects || []).find(p => p.id === inv.projectId) : null; + return ( + + + + + + + + ); + })} + +
Nr.DatumProjektStatusBetrag
{inv.number}{fmtDate(inv.date)}{proj?.name || "—"}{inv.status}{formatCHF(inv.total)}
+ {invoices.length > 5 &&
+{invoices.length - 5} weitere —
} + + } +
+ + {/* Offerten */} +
+
0 ? "1px solid #ece8e2" : "none" }}> + OFFERTEN ({quotes.length}) + {quotes.length > 0 && } +
+ {quotes.length === 0 + ?
Noch keine Offerten.
+ : <> + + + + {quotes.slice(0, 5).map(q => ( + + + + + + + + ))} + +
Nr.DatumModusStatusHonorar
{q.number}{fmtDate(q.date)}{q.mode === "sia" ? "SIA 102" : q.mode === "manual" ? "Aufwand" : "Frei"}{q.status || "—"}{formatCHF(q.total)}
+ {quotes.length > 5 &&
+{quotes.length - 5} weitere —
} + + } +
+ + {/* Kontakt-Modal */} + {contactModal && ( + setContactModal(null)} onSave={saveContact}> +
+ setContactForm({ ...contactForm, name: e.target.value })} autoFocus /> + setContactForm({ ...contactForm, position: e.target.value })} placeholder="z.B. Geschäftsführer, Bauleiter…" /> +
+
+ setContactForm({ ...contactForm, email: e.target.value })} /> + setContactForm({ ...contactForm, phone: e.target.value })} /> +
+
+ )} + + {/* Client-Edit-Modal */} + {modal?.type === "client" && modal.id && ( + setModal(null)} onSave={save} wide> + {clientFormFields(form, setForm)} + + )} +
+ ); + } + + // ── Listen-Ansicht ── + const filteredClients = clients.filter(c => { + if (!search) return true; + const q = search.toLowerCase(); + return [c.name, c.city, c.email, c.street, ...(c.contacts || []).map(ct => ct.name)].some(v => v?.toLowerCase().includes(q)); + }); + + const clientGroups = (() => { + if (groupBy === "none") return [{ key: "_all", label: null, items: filteredClients }]; + if (groupBy === "alpha") { + const g = {}; + [...filteredClients].sort((a, b) => a.name.localeCompare(b.name, "de")) + .forEach(c => { const k = c.name[0]?.toUpperCase() || "#"; (g[k] = g[k] || []).push(c); }); + return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); + } + if (groupBy === "city") { + const g = {}; + [...filteredClients].sort((a, b) => a.name.localeCompare(b.name, "de")) + .forEach(c => { const k = c.city || "Ohne Ort"; (g[k] = g[k] || []).push(c); }); + return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); + } + })(); + + const ClientTable = ({ items }) => ( +
+ + + + + + + + + + + + + {items.length === 0 && } + {items.map(c => { + const projs = (data.projects || []).filter(p => p.clientId === c.id).length; + const cts = c.contacts || []; + const hauptkontakt = cts[0]; + const city = [c.zip, c.city].filter(Boolean).join(" "); + return ( + setSelectedId(c.id)}> + + + + + + + + ); + })} + +
FirmennameAdresseHauptkontaktKontakteProjekte
Keine Treffer
+ {c.name} + {c.email &&
{c.email}
} +
+ {c.street &&
{c.street}
} + {city &&
{city}
} +
+ {hauptkontakt ? ( + <> +
{hauptkontakt.name}
+ {hauptkontakt.position &&
{hauptkontakt.position}
} + + ) : } +
{cts.length || "—"}{projs || "—"} e.stopPropagation()}> + + +
+
+ ); + + return ( +
+ {ConfirmModalEl} +
+ Neuer Kunde} /> + +
+ setSearch(e.target.value)} + style={{ flex: "1 1 200px", maxWidth: 300, fontSize: 12 }} /> + +
+ + {clients.length === 0 ? ( +
Noch keine Kunden erfasst.
+ ) : filteredClients.length === 0 ? ( +
Keine Treffer
+ ) : clientGroups.map(group => ( +
+ {group.label && ( +
+ {group.label.toUpperCase()} {group.items.length} +
+ )} + +
+ ))} + + {modal?.type === "client" && ( + setModal(null)} onSave={save} wide> + {clientFormFields(form, setForm, !modal.id)} + + )} +
+ ); +} + +function clientFormFields(form, setForm, isNew = false) { + return ( + <> + + setForm({ ...form, name: e.target.value })} autoFocus placeholder="z.B. Müller Immobilien AG" /> + +
+ setForm({ ...form, street: e.target.value })} placeholder="Bahnhofstrasse 1" /> + setForm({ ...form, zip: e.target.value })} style={{ maxWidth: 100 }} /> + setForm({ ...form, city: e.target.value })} /> + setForm({ ...form, country: e.target.value.toUpperCase() })} maxLength={2} style={{ maxWidth: 70 }} /> +
+
+ setForm({ ...form, email: e.target.value })} /> + setForm({ ...form, phone: e.target.value })} /> + setForm({ ...form, website: e.target.value })} placeholder="www.beispiel.ch" /> +
+ + {isNew && ( + <> +
+ HAUPTKONTAKT (optional) +
+
+ + setForm({ ...form, _contactName: e.target.value })} placeholder="z.B. Hans Müller" /> + + + setForm({ ...form, _contactPosition: e.target.value })} placeholder="z.B. Geschäftsführer" /> + +
+
Weitere Ansprechpartner können in der Kundendetailseite hinzugefügt werden.
+ + )} + + ); +} diff --git a/src/views/Contacts.jsx b/src/views/Contacts.jsx new file mode 100755 index 0000000..a12eefa --- /dev/null +++ b/src/views/Contacts.jsx @@ -0,0 +1,456 @@ +import React, { useState } from "react"; +import { generateId } from "../utils.js"; +import { Header, Modal, FormField, useConfirm , DateInput } from "../components/UI.jsx"; + +const CONTACT_TYPES = [ + "Elektroplaner", "HLKSE-Planer", "Statiker", "Tragwerksplaner", + "Kostenplaner", "Landschaftsarchitekt", "Bauphysiker", + "Vermessungsingenieur", "Brandschutzspezialist", "Geologe", + "Generalunternehmer", "Fachplaner", "Sonstiges", +]; + +const fmtCHF = (v) => v != null ? `CHF ${Number(v).toLocaleString("de-CH", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"; +const fmtDate = (s) => s ? new Date(s).toLocaleDateString("de-CH") : "—"; + +export default +function Contacts({ data, update }) { + const contacts = data.contacts || []; + const { askConfirm, ConfirmModalEl } = useConfirm(); + + const [selectedId, setSelectedId] = useState(() => { + const id = window.__navToContact || null; + window.__navToContact = null; + return id; + }); + const [search, setSearch] = useState(""); + const [typeFilter, setTypeFilter] = useState(""); + const [groupBy, setGroupBy] = useState("alpha"); + + const emptyFirm = { + name: "", type: "", street: "", zip: "", city: "", email: "", phone: "", website: "", note: "", + contacts: [], honorarOffers: [], + _personName: "", _personPosition: "", + }; + const [firmModal, setFirmModal] = useState(null); + const [firmForm, setFirmForm] = useState(emptyFirm); + + const [personModal, setPersonModal] = useState(null); + const [personForm, setPersonForm] = useState({ name: "", position: "", email: "", phone: "" }); + + const [honorarModal, setHonorarModal] = useState(null); + const [honorarForm, setHonorarForm] = useState({ date: "", amount: "", phase: "", description: "", note: "" }); + + const selectedContact = contacts.find(c => c.id === selectedId) || null; + + // ── Firm CRUD ── + const saveFirm = () => { + if (!firmForm.name.trim()) return; + const { _personName, _personPosition, ...firmData } = firmForm; + let persons = firmData.contacts || []; + if (_personName.trim() && !firmModal?.id) { + persons = [{ id: generateId(), name: _personName.trim(), position: _personPosition.trim(), email: "", phone: "" }]; + } + const firm = { ...firmData, contacts: persons, id: firmModal?.id || generateId() }; + update("contacts", firmModal?.id ? contacts.map(c => c.id === firmModal.id ? firm : c) : [...contacts, firm]); + setFirmModal(null); + }; + const openNew = () => { setFirmForm(emptyFirm); setFirmModal({}); }; + const openEdit = (c) => { setFirmForm({ ...emptyFirm, ...c, _personName: "", _personPosition: "" }); setFirmModal({ id: c.id }); }; + const delFirm = async (id) => { + if (await askConfirm("Kontakt löschen?")) { + update("contacts", contacts.filter(c => c.id !== id)); + if (selectedId === id) setSelectedId(null); + } + }; + + // ── Person CRUD ── + const savePerson = () => { + if (!personForm.name.trim()) return; + const firm = contacts.find(c => c.id === personModal.contactId); + if (!firm) return; + const persons = firm.contacts || []; + const updated = personModal.personId + ? persons.map(p => p.id === personModal.personId ? { ...p, ...personForm } : p) + : [...persons, { ...personForm, id: generateId() }]; + update("contacts", contacts.map(c => c.id === firm.id ? { ...c, contacts: updated } : c)); + setPersonModal(null); + }; + const delPerson = async (contactId, personId) => { + if (await askConfirm("Person löschen?")) { + update("contacts", contacts.map(c => c.id === contactId + ? { ...c, contacts: (c.contacts || []).filter(p => p.id !== personId) } : c)); + } + }; + + // ── Honorar CRUD ── + const saveHonorar = () => { + const firm = contacts.find(c => c.id === honorarModal.contactId); + if (!firm) return; + const offers = firm.honorarOffers || []; + const offer = { id: honorarModal.offerId || generateId(), date: honorarForm.date, amount: parseFloat(honorarForm.amount) || 0, phase: honorarForm.phase, description: honorarForm.description, note: honorarForm.note }; + const updated = honorarModal.offerId ? offers.map(o => o.id === honorarModal.offerId ? offer : o) : [...offers, offer]; + update("contacts", contacts.map(c => c.id === firm.id ? { ...c, honorarOffers: updated } : c)); + setHonorarModal(null); + }; + const delHonorar = async (contactId, offerId) => { + if (await askConfirm("Honorarangebot löschen?")) { + update("contacts", contacts.map(c => c.id === contactId + ? { ...c, honorarOffers: (c.honorarOffers || []).filter(o => o.id !== offerId) } : c)); + } + }; + + // ── Form fields (shared new/edit) ── + const firmFormFields = (isNew) => ( + <> +
+ + setFirmForm(f => ({ ...f, name: e.target.value }))} autoFocus placeholder="z.B. Elektroplaner AG" /> + + + + +
+
+ setFirmForm(f => ({ ...f, street: e.target.value }))} /> + setFirmForm(f => ({ ...f, zip: e.target.value }))} style={{ maxWidth: 90 }} /> + setFirmForm(f => ({ ...f, city: e.target.value }))} /> +
+
+ setFirmForm(f => ({ ...f, email: e.target.value }))} /> + setFirmForm(f => ({ ...f, phone: e.target.value }))} /> + setFirmForm(f => ({ ...f, website: e.target.value }))} placeholder="www.beispiel.ch" /> +
+ setFirmForm(f => ({ ...f, note: e.target.value }))} /> + {isNew && ( + <> +
+ HAUPTKONTAKT (optional) +
+
+ + setFirmForm(f => ({ ...f, _personName: e.target.value }))} placeholder="z.B. Max Muster" /> + + + setFirmForm(f => ({ ...f, _personPosition: e.target.value }))} placeholder="z.B. Projektleiter" /> + +
+
Weitere Personen können in der Detailansicht hinzugefügt werden.
+ + )} + + ); + + // ── Detail view ── + if (selectedId && selectedContact) { + const persons = selectedContact.contacts || []; + const offers = selectedContact.honorarOffers || []; + const hauptperson = persons[0] || null; + const linkedProjects = (data.projects || []).filter(p => (p.projectContacts || []).some(pc => pc.contactId === selectedId)); + const addressLine = [selectedContact.street, [selectedContact.zip, selectedContact.city].filter(Boolean).join(" ")].filter(Boolean).join(", "); + + return ( +
+ {ConfirmModalEl} + + +
+
+ {selectedContact.type &&
{selectedContact.type.toUpperCase()}
} +

{selectedContact.name}

+ {addressLine &&
{addressLine}
} +
+
+ + +
+
+ +
+ {/* Firmeninfo */} +
+
FIRMENINFO
+ {[ + { label: "E-Mail", value: selectedContact.email, href: selectedContact.email ? `mailto:${selectedContact.email}` : null }, + { label: "Telefon", value: selectedContact.phone }, + { label: "Website", value: selectedContact.website, href: selectedContact.website ? (selectedContact.website.startsWith("http") ? selectedContact.website : `https://${selectedContact.website}`) : null }, + { label: "Adresse", value: addressLine || null }, + ].filter(r => r.value).map(({ label, value, href }) => ( +
+ {label} + {href ? {value} : {value}} +
+ ))} + {selectedContact.note &&
{selectedContact.note}
} + {persons.length > 0 && hauptperson && ( +
+
HAUPTKONTAKT
+
{hauptperson.name}
+ {hauptperson.position &&
{hauptperson.position}
} +
+ {hauptperson.email && {hauptperson.email}} + {hauptperson.phone && {hauptperson.phone}} +
+
+ )} +
+ + {/* Ansprechpartner */} +
+
0 ? "1px solid #ece8e2" : "none" }}> +
ANSPRECHPARTNER ({persons.length})
+ +
+ {persons.length === 0 + ?
Noch keine Ansprechpartner erfasst.
+ : persons.map((p, i) => ( +
+
+
+
+ {p.name} + {i === 0 && HAUPT} +
+ {p.position &&
{p.position}
} +
+ {p.email && {p.email}} + {p.phone && {p.phone}} +
+
+
+ + +
+
+
+ )) + } +
+
+ + {/* Honorar-Angebote */} +
+
0 ? "1px solid #ece8e2" : "none" }}> +
HONORAR-ANGEBOTE ({offers.length})
+ +
+ {offers.length === 0 + ?
Noch keine Honorar-Angebote erfasst.
+ : ( + + + + {[...offers].sort((a, b) => (b.date || "").localeCompare(a.date || "")).map(o => ( + + + + + + + + ))} + + {offers.length > 1 && ( + + + + + + + )} +
DatumBeschriebPhaseBetrag
{fmtDate(o.date)} +
{o.description || }
+ {o.note &&
{o.note}
} +
{o.phase || "—"}{fmtCHF(o.amount)} + + +
Total{fmtCHF(offers.reduce((s, o) => s + (parseFloat(o.amount) || 0), 0))} +
+ ) + } +
+ + {/* Beteiligt an */} + {linkedProjects.length > 0 && ( +
+
BETEILIGT AN ({linkedProjects.length})
+ + + + {linkedProjects.map(proj => { + const client = (data.clients || []).find(c => c.id === proj.clientId); + return ( + + + + + + ); + })} + +
ProjektKundeStatus
{proj.number ? {proj.number} : null}{proj.name}{client?.name || "—"}{proj.status}
+
+ )} + + {/* Person modal */} + {personModal && ( + setPersonModal(null)} onSave={savePerson}> +
+ setPersonForm(f => ({ ...f, name: e.target.value }))} autoFocus /> + setPersonForm(f => ({ ...f, position: e.target.value }))} placeholder="z.B. Projektleiter" /> +
+
+ setPersonForm(f => ({ ...f, email: e.target.value }))} /> + setPersonForm(f => ({ ...f, phone: e.target.value }))} /> +
+
+ )} + + {/* Honorar modal */} + {honorarModal && ( + setHonorarModal(null)} onSave={saveHonorar}> +
+ setHonorarForm(f => ({ ...f, date: e.target.value }))} /> + setHonorarForm(f => ({ ...f, amount: e.target.value }))} placeholder="0" /> +
+ setHonorarForm(f => ({ ...f, description: e.target.value }))} placeholder="z.B. Elektroplanung Rohbau" /> + setHonorarForm(f => ({ ...f, phase: e.target.value }))} placeholder="z.B. Phase 31–33" /> + setHonorarForm(f => ({ ...f, note: e.target.value }))} /> +
+ )} + + {/* Edit modal */} + {firmModal && ( + setFirmModal(null)} onSave={saveFirm} wide> + {firmFormFields(false)} + + )} +
+ ); + } + + // ── List view ── + const allTypes = [...new Set(contacts.map(c => c.type).filter(Boolean))].sort(); + const filtered = contacts + .filter(c => + (!typeFilter || c.type === typeFilter) && + (!search || c.name.toLowerCase().includes(search.toLowerCase()) || + (c.type || "").toLowerCase().includes(search.toLowerCase()) || + (c.contacts || []).some(p => p.name.toLowerCase().includes(search.toLowerCase()))) + ) + .sort((a, b) => a.name.localeCompare(b.name, "de")); + + const contactGroups = (() => { + if (groupBy === "none") return [{ key: "_all", label: null, items: filtered }]; + if (groupBy === "alpha") { + const g = {}; + filtered.forEach(c => { const k = c.name[0]?.toUpperCase() || "#"; (g[k] = g[k] || []).push(c); }); + return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); + } + if (groupBy === "type") { + const g = {}; + filtered.forEach(c => { const k = c.type || "Ohne Typ"; (g[k] = g[k] || []).push(c); }); + return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); + } + })(); + + const ContactTable = ({ items }) => ( +
+ + + + + + + + + + + + + + {items.length === 0 && } + {items.map(c => { + const persons = c.contacts || []; + const haupt = persons[0]; + const city = [c.zip, c.city].filter(Boolean).join(" "); + const projCount = (data.projects || []).filter(p => (p.projectContacts || []).some(pc => pc.contactId === c.id)).length; + return ( + setSelectedId(c.id)}> + + + + + + + + + ); + })} + +
FirmaTypAdresseHauptkontaktPersonenProjekte
Keine Treffer
+ {c.name} + {c.email &&
{c.email}
} +
{c.type || } + {c.street &&
{c.street}
} + {city &&
{city}
} +
+ {haupt ? ( + <> +
{haupt.name}
+ {haupt.position &&
{haupt.position}
} + + ) : } +
{persons.length || "—"} 0 ? "#1a4e8a" : "#ccc", fontWeight: projCount > 0 ? 600 : 400 }}>{projCount || "—"} e.stopPropagation()}> + + +
+
+ ); + + return ( +
+ {ConfirmModalEl} +
+ Neuer Kontakt} /> + +
+ setSearch(e.target.value)} placeholder="Suchen…" + style={{ flex: "1 1 200px", maxWidth: 300, fontSize: 12 }} /> + {allTypes.length > 0 && ( + + )} + +
+ + {contacts.length === 0 ? ( +
Noch keine Kontakte erfasst.
+ ) : filtered.length === 0 ? ( +
Keine Treffer
+ ) : contactGroups.map(group => ( +
+ {group.label && ( +
+ {group.label.toUpperCase()} {group.items.length} +
+ )} + +
+ ))} + + {firmModal && ( + setFirmModal(null)} onSave={saveFirm} wide> + {firmFormFields(!firmModal.id)} + + )} +
+ ); +} diff --git a/src/views/Dashboard.jsx b/src/views/Dashboard.jsx new file mode 100755 index 0000000..5b4c0fd --- /dev/null +++ b/src/views/Dashboard.jsx @@ -0,0 +1,762 @@ +import React, { useState, useRef, useEffect } from "react"; +import { SIA_PHASES, DASHBOARD_WIDGETS } from "../constants.js"; +import { formatCHF, formatDate, formatHours, migrateDashboardLayout, widgetsToRows, generateId } from "../utils.js"; + +const HEIGHT_OPTS = [ + { v: 0, l: "Auto" }, + { v: 160, l: "S" }, + { v: 280, l: "M" }, + { v: 420, l: "L" }, +]; + +function deepCloneLayout(layout) { + return (layout || []).map(r => ({ ...r, widgets: [...r.widgets] })); +} + +export default function Dashboard({ data, setView, currentUser, saveAll }) { + const today = new Date().toISOString().slice(0, 10); + const thisMonth = today.slice(0, 7); + const thisYear = today.slice(0, 4); + const lastMonth = (() => { const d = new Date(); d.setMonth(d.getMonth() - 1); return d.toISOString().slice(0, 7); })(); + + // ─── Layout resolution ───────────────────────────────────────────── + const myUser = (data.users || []).find(u => u.id === currentUser?.id); + const myRole = (data.appRoles || []).find(r => r.id === (currentUser?.appRoleId || myUser?.appRoleId)); + const myTpl = (data.dashboardTemplates || []).find(t => t.id === myRole?.dashboardTemplateId); + const roleLayout = myTpl + ? migrateDashboardLayout(myTpl.layout) + : migrateDashboardLayout(myRole?.dashboardWidgets) // fallback for old data + || widgetsToRows(DASHBOARD_WIDGETS.map(w => w.id)); + const savedLayout = myUser?.dashboardWidgets ? migrateDashboardLayout(myUser.dashboardWidgets) : null; + + const [editMode, setEditMode] = useState(false); + const [layout, setLayout] = useState([]); + const [dragOver, setDragOver] = useState(null); + const [addPopoverRowId, setAddPopoverRowId] = useState(null); + const [saveTemplateOpen, setSaveTemplateOpen] = useState(false); + const [newPublicName, setNewPublicName] = useState(""); + const [newPrivateName, setNewPrivateName] = useState(""); + const dragRef = useRef(null); + + const activeLayout = editMode ? layout : (savedLayout || roleLayout); + + // ─── Data ────────────────────────────────────────────────────────── + const activeProjects = data.projects.filter(p => p.status === "aktiv"); + const projMins = id => data.timeEntries.filter(e => e.projectId === id).reduce((s, e) => s + (e.minutes || 0), 0); + const monthMins = data.timeEntries.filter(e => (e.date||"").startsWith(thisMonth)).reduce((s,e)=>s+(e.minutes||0),0); + const lastMoMins = data.timeEntries.filter(e => (e.date||"").startsWith(lastMonth)).reduce((s,e)=>s+(e.minutes||0),0); + const myEmpId = currentUser?.employeeId; + const myMonthMins = data.timeEntries.filter(e => (e.date||"").startsWith(thisMonth) && e.employeeId===myEmpId).reduce((s,e)=>s+(e.minutes||0),0); + const recentTime = [...data.timeEntries].sort((a,b)=>(b.date||"").localeCompare(a.date||"")).slice(0,6); + const myRecentTime = [...data.timeEntries].filter(e=>e.employeeId===myEmpId).sort((a,b)=>(b.date||"").localeCompare(a.date||"")).slice(0,6); + const openInvoices = data.invoices.filter(i => i.status==="gesendet"||i.status==="überfällig"); + const overdueInvoices = data.invoices.filter(i => i.status==="überfällig"); + const openAmount = openInvoices.reduce((s,i)=>s+(i.total||0),0); + const paidThisYear = data.invoices.filter(i=>i.status==="bezahlt"&&(i.date||"").startsWith(thisYear)).reduce((s,i)=>s+(i.sub||0),0); + const pendingQuotes = (data.quotes||[]).filter(q=>q.status==="gesendet"); + const expiredQuotes = (data.quotes||[]).filter(q=>q.status==="gesendet"&&q.validUntil&&q.validUntil{ + const mins = data.timeEntries.filter(e=>e.projectId===p.id&&!e.invoiceId).reduce((s,e)=>s+(e.minutes||0),0); + return {...p, unbilledMins:mins, unbilledAmt:(mins/60)*(p.hourlyRate||0)}; + }).filter(p=>p.unbilledMins>0&&(p.billingType||p.type)==="stundensatz").sort((a,b)=>b.unbilledMins-a.unbilledMins); + const totalUnbilled = unbilledProjects.reduce((s,p)=>s+p.unbilledMins,0); + const last6Months = Array.from({length:6},(_,i)=>{const d=new Date();d.setMonth(d.getMonth()-(5-i));return d.toISOString().slice(0,7);}); + const monthlyRevenue = last6Months.map(m=>({m,paid:data.invoices.filter(i=>i.status==="bezahlt"&&(i.date||"").startsWith(m)).reduce((s,i)=>s+(i.sub||0),0)})); + const maxRev = Math.max(...monthlyRevenue.map(m=>m.paid),1); + + // ─── Permission ──────────────────────────────────────────────────── + const canSaveTemplate = !myRole || myRole.permissions === null || (myRole.permissions||[]).includes("dashboard-vorlage"); + + // ─── Edit mode ───────────────────────────────────────────────────── + const enterEdit = () => { setLayout(deepCloneLayout(savedLayout || roleLayout)); setEditMode(true); }; + const cancelEdit = () => { setEditMode(false); setAddPopoverRowId(null); setSaveTemplateOpen(false); setNewPublicName(""); setNewPrivateName(""); }; + const saveEdit = () => { + if (currentUser && saveAll) { + const users = (data.users||[]).map(u => u.id===currentUser.id ? {...u, dashboardWidgets: layout} : u); + saveAll({ ...data, users }); + } + setEditMode(false); setAddPopoverRowId(null); setSaveTemplateOpen(false); setNewPublicName(""); setNewPrivateName(""); + }; + const loadTemplate = tplId => { + const tpl = (data.dashboardTemplates||[]).find(t=>t.id===tplId); + if (tpl?.layout) setLayout(deepCloneLayout(migrateDashboardLayout(tpl.layout))); + }; + const saveAsTemplate = (tplId) => { + if (!saveAll) return; + const dashboardTemplates = (data.dashboardTemplates||[]).map(t => t.id===tplId ? {...t, layout} : t); + saveAll({ ...data, dashboardTemplates }); + setSaveTemplateOpen(false); + }; + const createTemplate = (name, isPublic) => { + if (!name.trim() || !saveAll) return; + const tpl = { id: generateId(), name: name.trim(), isPublic, layout, ...(!isPublic ? { createdBy: currentUser?.id } : {}) }; + saveAll({ ...data, dashboardTemplates: [...(data.dashboardTemplates||[]), tpl] }); + setSaveTemplateOpen(false); + if (isPublic) setNewPublicName(""); else setNewPrivateName(""); + }; + // Add widget to row, moving it out of any other row it's already in + const addWidgetExclusive = (rowId, wid) => { + setLayout(l => l.map(r => { + if (r.id === rowId) return { ...r, widgets: r.widgets.includes(wid) ? r.widgets : [...r.widgets, wid] }; + return { ...r, widgets: r.widgets.filter(w => w !== wid) }; + })); + setAddPopoverRowId(null); + }; + // Close popovers on outside click + useEffect(() => { + if (!addPopoverRowId && !saveTemplateOpen) return; + const close = e => { if (!e.target.closest("[data-popover]")) { setAddPopoverRowId(null); setSaveTemplateOpen(false); } }; + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, [addPopoverRowId, saveTemplateOpen]); + + // ─── Row operations ──────────────────────────────────────────────── + const updateRow = (rowId, patch) => setLayout(l => l.map(r => r.id===rowId ? {...r,...patch} : r)); + const addRow = (afterId) => { + const row = { id: generateId(), cols: 2, minH: 0, widgets: [] }; + setLayout(l => { + const i = l.findIndex(r=>r.id===afterId); + const next = [...l]; + next.splice(i+1, 0, row); + return next; + }); + }; + const deleteRow = rowId => setLayout(l => l.filter(r=>r.id!==rowId)); + const moveRow = (rowId, dir) => setLayout(l => { + const i = l.findIndex(r=>r.id===rowId); + const j = i + dir; + if (j<0||j>=l.length) return l; + const next=[...l]; + [next[i],next[j]]=[next[j],next[i]]; + return next; + }); + const addWidgetToRow = (rowId, wid) => + setLayout(l => l.map(r => r.id===rowId ? {...r, widgets:[...r.widgets,wid]} : r)); + const removeWidgetFromRow = (rowId, wid) => + setLayout(l => l.map(r => r.id===rowId ? {...r, widgets:r.widgets.filter(w=>w!==wid)} : r)); + + // ─── Drag & Drop ─────────────────────────────────────────────────── + const handleDragStart = (fromRowId, widgetId) => { + dragRef.current = { fromRowId, widgetId }; + }; + const handleDragEnd = () => { + dragRef.current = null; + setDragOver(null); + }; + const handleDrop = (toRowId, beforeWidgetId) => { + const src = dragRef.current; + if (!src) return; + const { fromRowId, widgetId } = src; + setLayout(l => { + const next = deepCloneLayout(l); + const fromRow = next.find(r=>r.id===fromRowId); + const toRow = next.find(r=>r.id===toRowId); + if (!fromRow||!toRow) return l; + fromRow.widgets = fromRow.widgets.filter(w=>w!==widgetId); + if (beforeWidgetId) { + const idx = toRow.widgets.indexOf(beforeWidgetId); + toRow.widgets.splice(idx<0 ? toRow.widgets.length : idx, 0, widgetId); + } else { + if (!toRow.widgets.includes(widgetId)) toRow.widgets.push(widgetId); + } + return next; + }); + dragRef.current = null; + setDragOver(null); + }; + + // ─── Styles ──────────────────────────────────────────────────────── + const ctrlBtn = "pill"; + const activeCtrlBtn = "pill active"; + + // ─── Widget content renderers ────────────────────────────────────── + const wContent = { + "kpi-projekte": () => p.status==="abgeschlossen").length} abgeschlossen`} />, + "kpi-stunden": () => 0?`Vormonat: ${formatHours(lastMoMins)}`:undefined} />, + "kpi-ausstehend": () => 0?"#8a1a1a":"#7a6a00"} go="invoices" setView={!editMode?setView:undefined} sub={overdueInvoices.length>0?`${overdueInvoices.length} überfällig`:`${openInvoices.length} gesendet`} />, + "kpi-umsatz": () => , + "warnungen": () => { + const has = overdueInvoices.length>0||expiredQuotes.length>0; + if (!editMode&&!has) return null; + return has ? ( +
+ {overdueInvoices.length>0&&
setView("invoices"):undefined} style={{flex:1,minWidth:180,padding:"12px 16px",background:"#fff3f3",border:"1.5px solid #e0b0b0",borderRadius:8,cursor:editMode?"default":"pointer",display:"flex",alignItems:"center",gap:12}}> +
+
{overdueInvoices.length} überfällige Rechnung{overdueInvoices.length>1?"en":""}
{formatCHF(overdueInvoices.reduce((s,i)=>s+i.total,0))} ausstehend
+
} + {expiredQuotes.length>0&&
setView("quotes"):undefined} style={{flex:1,minWidth:180,padding:"12px 16px",background:"#fffbe8",border:"1.5px solid #e0d090",borderRadius:8,cursor:editMode?"default":"pointer",display:"flex",alignItems:"center",gap:12}}> +
+
{expiredQuotes.length} Offerte{expiredQuotes.length>1?"n":""} abgelaufen
Gültigkeit überschritten
+
} +
+ ) :
Warnungen (keine aktuellen)
; + }, + "aktive-projekte": () => ( +
+
+
AKTIVE PROJEKTE
+ +
+ {activeProjects.length===0?
Keine aktiven Projekte
:activeProjects.slice(0,6).map(p=>{ + const used=projMins(p.id),budget=p.budgetHours||0,pct=budget>0?Math.min((used/60)/budget,1):0,over=budget>0&&(used/60)>budget; + const cl=(data.persons||[]).filter(x=>x.isAuftraggeber).find(c=>c.id===p.clientId); + return (
+
+
{p.name}
{cl&&
{cl.name}
}
+
{formatHours(used)}{budget>0?` / ${budget}h`:""}
+
+ {budget>0&&
0.8?"#b5621e":"#2d6a4f",borderRadius:2,transition:"width 0.3s"}}/>
} +
); + })} +
+ ), + "unverrechnete-stunden": () => ( +
+
+
UNVERRECHNETE STUNDEN
+ +
+ {totalUnbilled===0?
✓ Alles verrechnet
:<> +
{formatHours(totalUnbilled)}
+
≈ {formatCHF(unbilledProjects.reduce((s,p)=>s+p.unbilledAmt,0))}
+ {unbilledProjects.slice(0,5).map(p=>
{p.name}{formatHours(p.unbilledMins)}
)} + } +
+ ), + "umsatz-sparkline": () => ( +
+
UMSATZ LETZTE 6 MONATE
+
+ {monthlyRevenue.map(({m,paid})=>( +
+
0?`${Math.max((paid/maxRev)*54,4)}px`:"2px",background:m===thisMonth?"#b07848":paid>0?"#2d6a4f":"var(--border2)",borderRadius:"2px 2px 0 0",transition:"height 0.3s"}} title={formatCHF(paid)}/> +
{new Date(m+"-01").toLocaleString("de-CH",{month:"short"})}
+
+ ))} +
+
+ ), + "offene-offerten": () => ( +
+
+
OFFENE OFFERTEN
+ +
+ {pendingQuotes.length===0?
Keine pendenten Offerten
:pendingQuotes.slice(0,5).map(q=>{ + const cl=(data.persons||[]).filter(p=>p.isAuftraggeber).find(c=>c.id===q.clientId); + const expired=q.validUntil&&q.validUntil +
{q.number}
{cl&&
{cl.name}
}
+
{formatCHF(q.total)}
{expired&&
abgelaufen
}
+
); + })} +
+ ), + "letzte-zeiteintraege": () => ( +
+
+
LETZTE ZEITEINTRÄGE
+ +
+ +
+ ), + "meine-zeiteintraege": () => ( +
+
+
MEINE ZEITEINTRÄGE
+ +
+ +
+ ), + + "meine-projekte": () => { + const myProjects = (data.projects||[]).filter(p=>p.status==="aktiv"&&(p.internalMembers||[]).includes(myEmpId)); + return ( +
+
+
MEINE PROJEKTE
+ +
+ {myProjects.length===0 + ?
Keinen Projekten zugewiesen
+ : myProjects.map(p=>{ + const myMins=data.timeEntries.filter(e=>e.projectId===p.id&&e.employeeId===myEmpId).reduce((s,e)=>s+(e.minutes||0),0); + const cl=(data.persons||[]).find(c=>c.id===p.clientId); + return ( +
+
+
+
{p.name}
+ {cl&&
{cl.name}
} +
+
{formatHours(myMins)}
+
+
+ ); + }) + } +
+ ); + }, + + "meine-ferien": () => { + const myEmp=(data.employees||[]).find(e=>e.id===myEmpId); + if(!myEmpId||!myEmp) return
Kein Mitarbeiterprofil verknüpft
; + const anspruchTage=(myEmp.ferienWochen||5)*5; + const approvedEntries=(data.ferienEntries||[]).filter(f=>f.employeeId===myEmpId&&(f.status==="approved"||!f.status)&&(f.dateFrom||"").startsWith(thisYear)); + const countWorkdays=(from,to)=>{ + let n=0;const d=new Date(from);const end=new Date(to); + while(d<=end){const dow=d.getDay();if(dow!==0&&dow!==6)n++;d.setDate(d.getDate()+1);} + return n; + }; + const bezogenTage=approvedEntries.reduce((s,f)=>s+countWorkdays(f.dateFrom,f.dateTo),0); + const restTage=anspruchTage-bezogenTage; + const pct=Math.min(bezogenTage/anspruchTage,1); + const upcoming=(data.ferienEntries||[]).filter(f=>f.employeeId===myEmpId&&(f.status==="approved"||!f.status)&&f.dateFrom>today).slice(0,2); + return ( +
+
FERIENSTAND {thisYear}
+
+
+
{restTage}
+
Verbleibend
+
+
+
{bezogenTage}
+
Bezogen
+
+
+
{anspruchTage}
+
Anspruch
+
+
+
+
+
+ {upcoming.length>0&&<> +
NÄCHSTE FERIEN
+ {upcoming.map(f=>
{formatDate(f.dateFrom)} – {formatDate(f.dateTo)}
)} + } +
+ ); + }, + + "ueberstunden": () => { + const myEmp=(data.employees||[]).find(e=>e.id===myEmpId); + if(!myEmpId||!myEmp) return
Kein Mitarbeiterprofil verknüpft
; + const pensum=(myEmp.pensum||100)/100; + const tagessollH=((myEmp.wochenstunden||35)*pensum)/5; + const startOfYear=`${thisYear}-01-01`; + const fts=data.feiertage||[]; + // Respect eintrittsdatum: only count from when the employee actually started + const effectiveYearStart=myEmp.eintrittsdatum&&myEmp.eintrittsdatum>startOfYear?myEmp.eintrittsdatum:startOfYear; + const todayD=new Date(today); + let workdays=0;const d=new Date(effectiveYearStart); + while(d<=todayD){const dow=d.getDay();const ds=d.toISOString().slice(0,10);const ft=fts.find(f=>f.date===ds);const isFt=ft&&(ft.stundenDelta===0||ft.stundenDelta===null||ft.stundenDelta===undefined);if(dow!==0&&dow!==6&&!isFt)workdays++;d.setDate(d.getDate()+1);} + const sollMin=workdays*tagessollH*60; + const istMin=data.timeEntries.filter(e=>e.employeeId===myEmpId&&(e.date||"")>=effectiveYearStart&&(e.date||"")<=today).reduce((s,e)=>s+(e.minutes||0),0); + const deltaMin=istMin-sollMin; + const isPos=deltaMin>=0; + // This month — also respect eintrittsdatum + const effectiveMonthStart=myEmp.eintrittsdatum&&myEmp.eintrittsdatum>`${thisMonth}-01`?myEmp.eintrittsdatum:`${thisMonth}-01`; + const sollMonthMin=(() => { + let wd=0;const me=new Date(new Date(`${thisMonth}-01`).getFullYear(),new Date(`${thisMonth}-01`).getMonth()+1,0); + const end=mef.date===ds);const isFt=ft&&(ft.stundenDelta===0||ft.stundenDelta===null||ft.stundenDelta===undefined);if(dow!==0&&dow!==6&&!isFt)wd++;dd.setDate(dd.getDate()+1);} + return wd*tagessollH*60; + })(); + const istMonthMin=data.timeEntries.filter(e=>e.employeeId===myEmpId&&(e.date||"").startsWith(thisMonth)).reduce((s,e)=>s+(e.minutes||0),0); + const deltaMonth=istMonthMin-sollMonthMin; + return ( +
+
STUNDENSALDO
+
+
JAHRESSALDO {thisYear}
+
+ {isPos?"+":""}{formatHours(Math.abs(deltaMin))} + {isPos?"Überstunden":"Minusstunden"} +
+
{formatHours(istMin)} von {formatHours(sollMin)} Soll
+
+
+
DIESER MONAT
+
=0?"#2d6a4f":"#8a1a1a"}}> + {deltaMonth>=0?"+":""}{formatHours(Math.abs(deltaMonth))} +
+
{formatHours(istMonthMin)} von {formatHours(sollMonthMin)} Soll
+
+
+ ); + }, + + "stunden-woche": () => { + const myEmp=(data.employees||[]).find(e=>e.id===myEmpId); + const pensum=(myEmp?.pensum||100)/100; + const sollH=(myEmp?.wochenstunden||35)*pensum; + const getMonday=(dateStr)=>{const d=new Date(dateStr);const day=d.getDay();d.setDate(d.getDate()-(day===0?6:day-1));return d;}; + const monday=getMonday(today); + const weeks=Array.from({length:5},(_,i)=>{ + const mon=new Date(monday);mon.setDate(mon.getDate()-(4-i)*7); + const sun=new Date(mon);sun.setDate(sun.getDate()+6); + const monStr=mon.toISOString().slice(0,10); + const sunStr=sun.toISOString().slice(0,10); + const isCurrent=i===4; + const mins=(myEmpId?data.timeEntries.filter(e=>e.employeeId===myEmpId&&e.date>=monStr&&e.date<=sunStr):data.timeEntries.filter(e=>e.date>=monStr&&e.date<=sunStr)).reduce((s,e)=>s+(e.minutes||0),0); + const kw=(() => { const d2=new Date(mon);d2.setHours(0,0,0,0);d2.setDate(d2.getDate()+3-(d2.getDay()||7)+1); const w1=new Date(d2.getFullYear(),0,4);return 1+Math.round(((d2-w1)/86400000+((w1.getDay()||7)-1))/7); })(); + return {kw,mins,isCurrent}; + }); + const maxMins=Math.max(...weeks.map(w=>w.mins),sollH*60,1); + const sollPct=(sollH*60)/maxMins; + return ( +
+
STUNDEN PRO WOCHE
+
+
+ {weeks.map(({kw,mins,isCurrent})=>{ + const pct=mins/maxMins; + const over=mins>sollH*60; + return ( +
+
{mins>0?formatHours(mins):""}
+
0?4:1)}px`,background:isCurrent?(over?"#2d6a4f":"#b07848"):over?"#2d6a4f33":"var(--border2)",borderRadius:"3px 3px 0 0",transition:"height 0.3s"}}/> +
KW{kw}
+
+ ); + })} +
+
+ — Soll: {sollH}h / Woche +
+
+ ); + }, + + "interner-blog": () => { + const posts = (data.blogPosts || []); + const recent = [...posts].sort((a,b) => { + if (a.pinned !== b.pinned) return a.pinned ? -1 : 1; + return b.createdAt.localeCompare(a.createdAt); + }).slice(0, 3); + const getMonday = d => { const dd=new Date(d); dd.setDate(dd.getDate()-(dd.getDay()===0?6:dd.getDay()-1)); return dd; }; + const weekStart = getMonday(new Date(today)); + const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate()+6); + const feiertageWeek = (data.feiertage||[]).filter(f=>f.date>=weekStart.toISOString().slice(0,10)&&f.date<=weekEnd.toISOString().slice(0,10)); + const TYPE_COLOR = { beitrag:"#1a4e8a", ankuendigung:"#b5621e", event:"#2d6a4f" }; + const TYPE_LABEL = { beitrag:"Beitrag", ankuendigung:"Ankündigung", event:"Event" }; + return ( +
+
+
PINNWAND
+ +
+ {feiertageWeek.length>0&&( +
+ 🎉 + Feiertag diese Woche: {feiertageWeek.map(f=>f.name).join(", ")} +
+ )} + {recent.length===0 + ?
Keine Beiträge
+ : recent.map(p=>( +
+
+ {(TYPE_LABEL[p.type]||"").toUpperCase()} + {p.pinned&&📌} +
+ {p.title&&
{p.title}
} +
{p.body}
+
{p.authorName} · {new Date(p.createdAt).toLocaleDateString("de-CH")}
+
+ )) + } +
+ ); + }, + + "team-auslastung": () => { + const activeEmps=(data.employees||[]).filter(e=>e.aktiv!==false); + const pensum=e=>(e.pensum||100)/100; + const sollH=e=>((e.wochenstunden||35)*pensum(e))/5*(() => { + let wd=0;const d=new Date(`${thisMonth}-01`);const end=new Date(d.getFullYear(),d.getMonth()+1,0);const todayD=new Date(today); + const cap=end{ + const istMin=data.timeEntries.filter(t=>t.employeeId===e.id&&(t.date||"").startsWith(thisMonth)).reduce((s,t)=>s+(t.minutes||0),0); + const sollMin=sollH(e)*60; + const pct=sollMin>0?Math.min(istMin/sollMin,1.5):0; + return{...e,istMin,sollMin,pct}; + }).sort((a,b)=>b.istMin-a.istMin); + return ( +
+
TEAM-AUSLASTUNG {new Date().toLocaleString("de-CH",{month:"long"}).toUpperCase()}
+ {empData.length===0 + ?
Keine Mitarbeitenden
+ : empData.map(e=>{ + const over=e.pct>1; + return ( +
+
+
{e.name}
+
{formatHours(e.istMin)}{e.sollMin>0?` / ${formatHours(e.sollMin)}`:""}
+
+
+
0.8?"#b07848":"var(--border3)",borderRadius:2,transition:"width 0.3s"}}/> +
+
+ ); + }) + } +
+ ); + }, + }; + + // ─── Row renderer ────────────────────────────────────────────────── + const renderRow = (row, rowIdx) => { + const isDragOverRow = dragOver?.rowId === row.id && dragOver?.before === null; + const content = wContent[row.id] ? null : null; // widget lookup happens below + + return ( +
+ {/* Row toolbar */} + {editMode && ( +
+ SPALTEN + {[1,2,3,4].map(n=>( + + ))} + HÖHE + {HEIGHT_OPTS.map(opt=>( + + ))} +
+ + {addPopoverRowId===row.id&&( +
+ {DASHBOARD_WIDGETS.map(d=>{ + const inThis=row.widgets.includes(d.id); + const inOther=!inThis&&layout.some(r=>r.id!==row.id&&r.widgets.includes(d.id)); + return ( + + ); + })} +
+ )} +
+
+ {rowIdx>0&&} + {rowIdxmoveRow(row.id,1)} className={ctrlBtn}>↓} + + +
+
+ )} + + {/* Row grid */} +
{e.preventDefault();setDragOver({rowId:row.id,before:null});}:undefined} + onDrop={editMode?e=>{e.preventDefault();handleDrop(row.id,null);}:undefined} + > + {row.widgets.map(wid=>{ + const content = wContent[wid]; + if (!content) return null; + const rendered = content(); + if (rendered===null&&!editMode) return null; + const isBefore = dragOver?.rowId===row.id&&dragOver?.before===wid; + const isDragging = dragRef.current?.widgetId===wid; + return ( +
handleDragStart(row.id,wid):undefined} + onDragEnd={editMode?handleDragEnd:undefined} + onDragOver={editMode?e=>{e.preventDefault();e.stopPropagation();setDragOver({rowId:row.id,before:wid});}:undefined} + onDrop={editMode?e=>{e.preventDefault();e.stopPropagation();handleDrop(row.id,wid);}:undefined} + style={{ + position:"relative", + height:"100%", + opacity:isDragging?0.35:1, + boxShadow:isBefore?"inset 3px 0 0 var(--text)":undefined, + borderRadius:10, + cursor:editMode?"grab":"default", + transition:"opacity 0.15s", + }} + > + {rendered===null?
{DASHBOARD_WIDGETS.find(d=>d.id===wid)?.label}
:rendered} + {editMode&&( + + )} +
+ ); + })} + + {/* Empty slot drop target */} + {editMode&&( +
{e.preventDefault();setDragOver({rowId:row.id,before:null});}} + onDrop={e=>{e.preventDefault();handleDrop(row.id,null);}} + style={{minHeight:80,border:"1.5px dashed var(--border3)",borderRadius:10,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text5)",fontSize:13}} + > + ablegen +
+ )} +
+
+ ); + }; + + // ─── Render ──────────────────────────────────────────────────────── + return ( +
+ {/* Header */} +
+
+

+ {data.settings.name||"Studio"} +

+
+ {new Date().toLocaleDateString("de-CH",{weekday:"long",day:"numeric",month:"long",year:"numeric"}).toUpperCase()} +
+
+
+ {editMode?( + <> + +
+ + {saveTemplateOpen&&( +
+ {canSaveTemplate&&(<> +
ÖFFENTLICHE VORLAGE
+ {(data.dashboardTemplates||[]).filter(t=>t.isPublic).map(t=>( + + ))} +
+
+ setNewPublicName(e.target.value)} placeholder="Neue öffentliche Vorlage…" + onKeyDown={e=>e.key==="Enter"&&createTemplate(newPublicName,true)} + style={{flex:1,height:26,border:"1px solid var(--border)",borderRadius:4,padding:"0 8px",fontSize:11,background:"var(--surface)",color:"var(--text)",fontFamily:"inherit",outline:"none"}} /> + +
+
+
+ )} +
PERSÖNLICHE VORLAGE
+ {(data.dashboardTemplates||[]).filter(t=>!t.isPublic&&t.createdBy===currentUser?.id).map(t=>( + + ))} +
+
+ setNewPrivateName(e.target.value)} placeholder="Neue persönliche Vorlage…" + onKeyDown={e=>e.key==="Enter"&&createTemplate(newPrivateName,false)} + style={{flex:1,height:26,border:"1px solid var(--border)",borderRadius:4,padding:"0 8px",fontSize:11,background:"var(--surface)",color:"var(--text)",fontFamily:"inherit",outline:"none"}} /> + +
+
+
+ )} +
+ + + + ):( + + )} +
+
+ + {/* Edit banner */} + {editMode&&( +
+ Dashboard anpassen — Spaltenanzahl und Höhe pro Zeile wählen. Widgets per Drag & Drop verschieben oder mit × entfernen. +
+ )} + + {/* Rows */} + {activeLayout.map((row,idx) => renderRow(row,idx))} + + {/* Edit mode: add first row / add row at bottom */} + {editMode&&( +
+ +
+ )} +
+ ); +} + +function KpiCard({label,value,sub,color,go,setView}) { + return ( +
setView(go):undefined} + style={{borderTop:`3px solid ${color||"var(--border)"}`,cursor:go&&setView?"pointer":"default",transition:"transform 0.15s",height:"100%",boxSizing:"border-box"}} + onMouseEnter={go&&setView?e=>{e.currentTarget.style.transform="translateY(-2px)";}:undefined} + onMouseLeave={go&&setView?e=>{e.currentTarget.style.transform="";}:undefined} + > +
{label}
+
{value}
+ {sub&&
{sub}
} +
+ ); +} + +function TimeTable({entries,data}) { + if (!entries.length) return
Noch keine Zeiteinträge
; + return ( + + + + {entries.map(e=>{ + const proj=data.projects.find(p=>p.id===e.projectId); + const phase=SIA_PHASES.find(ph=>ph.id===e.phaseId); + return ( + + + + + ); + })} + +
DatumProjektBeschreibungDauer
{formatDate(e.date)}
{proj?.name||"—"}
{phase&&
Phase {phase.id}
}
{e.description||"—"}{formatHours(e.minutes)}
+ ); +} diff --git a/src/views/Dashboard.jsx.bak b/src/views/Dashboard.jsx.bak new file mode 100755 index 0000000..888abb2 --- /dev/null +++ b/src/views/Dashboard.jsx.bak @@ -0,0 +1,252 @@ +import React from "react"; +import { SIA_PHASES } from "../constants.js"; +import { formatCHF, formatDate, formatHours } from "../utils.js"; + +export default function Dashboard({ data, setView }) { + const today = new Date().toISOString().slice(0, 10); + const thisMonth = today.slice(0, 7); + const thisYear = today.slice(0, 4); + const lastMonth = (() => { const d = new Date(); d.setMonth(d.getMonth() - 1); return d.toISOString().slice(0, 7); })(); + + // Projekte + const activeProjects = data.projects.filter(p => p.status === "aktiv"); + const projectMinutes = (id) => data.timeEntries.filter(e => e.projectId === id).reduce((s, e) => s + (e.minutes || 0), 0); + + // Zeit + const monthMinutes = data.timeEntries.filter(e => (e.date || "").startsWith(thisMonth)).reduce((s, e) => s + (e.minutes || 0), 0); + const lastMonthMinutes = data.timeEntries.filter(e => (e.date || "").startsWith(lastMonth)).reduce((s, e) => s + (e.minutes || 0), 0); + const recentTime = [...data.timeEntries].sort((a, b) => (b.date || "").localeCompare(a.date || "")).slice(0, 6); + + // Rechnungen + const openInvoices = data.invoices.filter(i => i.status === "gesendet" || i.status === "überfällig"); + const overdueInvoices = data.invoices.filter(i => i.status === "überfällig"); + const openAmount = openInvoices.reduce((s, i) => s + (i.total || 0), 0); + const paidThisYear = data.invoices.filter(i => i.status === "bezahlt" && (i.date || "").startsWith(thisYear)).reduce((s, i) => s + (i.sub || 0), 0); + + // Offerten + const pendingQuotes = (data.quotes || []).filter(q => q.status === "gesendet"); + const expiredQuotes = (data.quotes || []).filter(q => q.status === "gesendet" && q.validUntil && q.validUntil < today); + + // Unverrechnete Stunden + const unbilledProjects = activeProjects + .map(p => { + const mins = data.timeEntries.filter(e => e.projectId === p.id && !e.invoiceId).reduce((s, e) => s + (e.minutes || 0), 0); + return { ...p, unbilledMins: mins, unbilledAmount: (mins / 60) * (p.hourlyRate || 0) }; + }) + .filter(p => p.unbilledMins > 0 && (p.billingType || p.type) === "stundensatz") + .sort((a, b) => b.unbilledMins - a.unbilledMins); + const totalUnbilledMins = unbilledProjects.reduce((s, p) => s + p.unbilledMins, 0); + + // Monats-Umsatz für Sparkline (letzten 6 Monate) + const last6Months = Array.from({ length: 6 }, (_, i) => { + const d = new Date(); d.setMonth(d.getMonth() - (5 - i)); + return d.toISOString().slice(0, 7); + }); + const monthlyRevenue = last6Months.map(m => ({ + m, paid: data.invoices.filter(i => i.status === "bezahlt" && (i.date || "").startsWith(m)).reduce((s, i) => s + (i.sub || 0), 0), + })); + const maxRevMonth = Math.max(...monthlyRevenue.map(m => m.paid), 1); + + const Card = ({ label, value, sub, color, go, children }) => ( +
setView(go) : undefined} + style={{ borderTop: `3px solid ${color || "var(--border)"}`, cursor: go ? "pointer" : "default", transition: "transform 0.15s" }} + onMouseEnter={go ? e => { e.currentTarget.style.transform = "translateY(-2px)"; } : undefined} + onMouseLeave={go ? e => { e.currentTarget.style.transform = ""; } : undefined}> +
{label}
+ {value !== undefined &&
{value}
} + {sub &&
{sub}
} + {children} +
+ ); + + return ( +
+ {/* Header */} +
+

+ {data.settings.name || "Studio"} +

+
+ {new Date().toLocaleDateString("de-CH", { weekday: "long", day: "numeric", month: "long", year: "numeric" }).toUpperCase()} +
+
+ + {/* KPI Cards */} +
+ p.status === "abgeschlossen").length} abgeschlossen`} /> + 0 ? `Vormonat: ${formatHours(lastMonthMinutes)}` : "Noch keine Vormonatsdaten"} /> + 0 ? "#8a1a1a" : "#7a6a00"} go="invoices" + sub={overdueInvoices.length > 0 ? `${overdueInvoices.length} überfällig` : `${openInvoices.length} gesendet`} /> + +
+ + {/* Warnungen */} + {(overdueInvoices.length > 0 || expiredQuotes.length > 0) && ( +
+ {overdueInvoices.length > 0 && ( +
setView("invoices")} style={{ flex: 1, minWidth: 200, padding: "12px 16px", background: "#fff3f3", border: "1.5px solid #e0b0b0", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", gap: 12 }}> +
+
+
{overdueInvoices.length} überfällige Rechnung{overdueInvoices.length > 1 ? "en" : ""}
+
{formatCHF(overdueInvoices.reduce((s, i) => s + i.total, 0))} ausstehend
+
+
+ )} + {expiredQuotes.length > 0 && ( +
setView("quotes")} style={{ flex: 1, minWidth: 200, padding: "12px 16px", background: "#fffbe8", border: "1.5px solid #e0d090", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", gap: 12 }}> +
+
+
{expiredQuotes.length} Offerte{expiredQuotes.length > 1 ? "n" : ""} abgelaufen
+
Gültigkeit überschritten
+
+
+ )} +
+ )} + + {/* Hauptbereich: 3 Spalten */} +
+ + {/* Aktive Projekte mit Budget */} +
+
+
AKTIVE PROJEKTE
+ +
+ {activeProjects.length === 0 ? ( +
Keine aktiven Projekte
+ ) : activeProjects.slice(0, 6).map(p => { + const used = projectMinutes(p.id); + const budget = p.budgetHours || 0; + const pct = budget > 0 ? Math.min((used / 60) / budget, 1) : 0; + const overBudget = budget > 0 && (used / 60) > budget; + const client = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === p.clientId); + return ( +
+
+
+
{p.name}
+ {client &&
{client.name}
} +
+
+ {formatHours(used)}{budget > 0 ? ` / ${budget}h` : ""} +
+
+ {budget > 0 && ( +
+
0.8 ? "#b5621e" : "#2d6a4f", borderRadius: 2, transition: "width 0.3s" }} /> +
+ )} +
+ ); + })} +
+ + {/* Unverrechnete Stunden */} +
+
+
UNVERRECHNETE STUNDEN
+ +
+ {totalUnbilledMins === 0 ? ( +
✓ Alles verrechnet
+ ) : ( + <> +
{formatHours(totalUnbilledMins)}
+
≈ {formatCHF(unbilledProjects.reduce((s, p) => s + p.unbilledAmount, 0))}
+ {unbilledProjects.slice(0, 5).map(p => ( +
+ {p.name} + {formatHours(p.unbilledMins)} +
+ ))} + + )} +
+ + {/* Monatsumsatz Sparkline + Offerte */} +
+
+
UMSATZ LETZTE 6 MONATE
+
+ {monthlyRevenue.map(({ m, paid }) => ( +
+
0 ? `${Math.max((paid / maxRevMonth) * 54, 4)}px` : "2px", background: m === thisMonth ? "#d4a85a" : paid > 0 ? "#2d6a4f" : "var(--border2)", borderRadius: "2px 2px 0 0", transition: "height 0.3s" }} title={formatCHF(paid)} /> +
+ {new Date(m + "-01").toLocaleString("de-CH", { month: "short" })} +
+
+ ))} +
+
+ +
+
+
OFFENE OFFERTEN
+ +
+ {pendingQuotes.length === 0 ? ( +
Keine pendenten Offerten
+ ) : pendingQuotes.slice(0, 4).map(q => { + const cl = ((data.persons||[]).filter(p=>p.isAuftraggeber)).find(c => c.id === q.clientId); + const expired = q.validUntil && q.validUntil < today; + return ( +
+
+
{q.number}
+ {cl &&
{cl.name}
} +
+
+
{formatCHF(q.total)}
+ {expired &&
abgelaufen
} +
+
+ ); + })} +
+
+
+ + {/* Untere Zeile: Letzte Zeiteinträge */} +
+
+
LETZTE ZEITEINTRÄGE
+ +
+ {recentTime.length === 0 ? ( +
Noch keine Zeiteinträge
+ ) : ( + + + + + + + + {recentTime.map(e => { + const proj = data.projects.find(p => p.id === e.projectId); + const phase = SIA_PHASES.find(ph => ph.id === e.phaseId); + return ( + + + + + + + ); + })} + +
DatumProjektBeschreibungDauer
{formatDate(e.date)} +
{proj?.name || "—"}
+ {phase &&
Phase {phase.id}
} +
{e.description || "—"}{formatHours(e.minutes)}
+ )} +
+
+ ); +} diff --git a/src/views/Dokumente.jsx b/src/views/Dokumente.jsx new file mode 100755 index 0000000..5fa3e1f --- /dev/null +++ b/src/views/Dokumente.jsx @@ -0,0 +1,194 @@ +import React from "react"; +import { PROTOKOLL_TYPES } from "../constants.js"; +import { formatDate } from "../utils.js"; +import { Header } from "../components/UI.jsx"; + +export default function Dokumente({ data, setView }) { + const protocols = (data.protocols || []).sort((a, b) => (b.date || "").localeCompare(a.date || "")); + const deliveryNotes = (data.deliveryNotes || []).sort((a, b) => (b.date || "").localeCompare(a.date || "")); + const letterTemplates = data.letterTemplates || []; + + const recentProtos = protocols.slice(0, 6); + const recentNotes = deliveryNotes.slice(0, 5); + + const protoByType = PROTOKOLL_TYPES.map(t => ({ + type: t, + count: protocols.filter(p => p.type === t).length, + })).filter(r => r.count > 0).sort((a, b) => b.count - a.count); + const maxTypeCount = protoByType[0]?.count || 1; + + const getClient = (n) => { + if (n.clientId) return (data.persons||[]).filter(p=>p.isAuftraggeber).find(c => c.id === n.clientId)?.name || "—"; + if (n.projectId) { + const proj = data.projects?.find(p => p.id === n.projectId); + if (proj?.clientId) return (data.persons||[]).filter(p=>p.isAuftraggeber).find(c => c.id === proj.clientId)?.name || "—"; + } + return "—"; + }; + + const getProject = (n) => { + if (!n.projectId) return null; + return data.projects?.find(p => p.id === n.projectId)?.name || null; + }; + + const SectionCard = ({ title, count, action, onAction, children, accent }) => ( +
+
+
+ {title} + {count > 0 && ( + {count} + )} +
+ +
+ {children} +
+ ); + + return ( +
+
+ + {/* KPI-Zeile */} +
+ {[ + { label: "Protokolle", count: protocols.length, sub: `${recentProtos.length > 0 ? formatDate(recentProtos[0]?.date) : "—"} zuletzt`, view: "protokolle", color: "#2d6a4f", bg: "#e8f5ee" }, + { label: "Lieferscheine", count: deliveryNotes.length, sub: `${recentNotes.length > 0 ? formatDate(recentNotes[0]?.date) : "—"} zuletzt`, view: "lieferscheine", color: "#1a4e8a", bg: "#e8f0fa" }, + { label: "Briefvorlagen", count: letterTemplates.length, sub: "Vorlagen", view: "letters", color: "#7a3e8a", bg: "#f3eafa" }, + ].map(({ label, count, sub, view: v, color, bg }) => ( +
setView(v)}> +
{label.toUpperCase()}
+
{count}
+
{sub}
+
+ → Öffnen +
+
+ ))} +
+ +
+ + {/* Linke Spalte */} +
+ {/* Letzte Protokolle */} + setView("protokolle")} accent="#e8f5ee"> + {recentProtos.length === 0 ? ( +
Noch keine Protokolle erfasst.
+ ) : ( + + + + + + + + + + + + {recentProtos.map(p => { + const proj = getProject(p); + return ( + + + + + + + + ); + })} + +
DatumTypTitelProjektEinträge
{formatDate(p.date)} + {p.type || "—"} + {p.title || }{proj || }{(p.entries || []).length}
+ )} +
+ + {/* Letzte Lieferscheine */} + setView("lieferscheine")} accent="#e8f0fa"> + {recentNotes.length === 0 ? ( +
Noch keine Lieferscheine erfasst.
+ ) : ( + + + + + + + + + + + + {recentNotes.map(n => ( + + + + + + + + ))} + +
Nr.DatumKundeProjektPositionen
{n.number || "—"}{formatDate(n.date)}{getClient(n)}{getProject(n) || }{(n.items || []).length}
+ )} +
+
+ + {/* Rechte Spalte */} +
+ {/* Protokoll-Typen */} + {protoByType.length > 0 && ( +
+
PROTOKOLLE NACH TYP
+ {protoByType.map(({ type, count }) => { + const pct = (count / maxTypeCount) * 100; + return ( +
+
+ {type} + {count} +
+
+
+
+
+ ); + })} +
+ )} + + {/* Briefvorlagen */} +
+
0 ? "1px solid #ece8e2" : "none" }}> + BRIEFVORLAGEN + +
+ {letterTemplates.length === 0 ? ( +
Noch keine Briefvorlagen erstellt.
+ ) : ( +
+ {letterTemplates.map(t => ( +
+ {t.name || "Unbenannt"} + + {t.body ? `${t.body.replace(/<[^>]+>/g, "").slice(0, 40)}…` : "—"} + +
+ ))} +
+ )} +
+
+
+
+ ); +} diff --git a/src/views/Invoices.jsx b/src/views/Invoices.jsx new file mode 100755 index 0000000..7996700 --- /dev/null +++ b/src/views/Invoices.jsx @@ -0,0 +1,1467 @@ +import React, { useState, useEffect } from "react"; +import { SIA_PHASES } from "../constants.js"; +import { generateId, formatCHF, formatDate, formatHours, roundCHF, isQRIban, generateQRReference, formatReference } from "../utils.js"; +import { Header, Modal, FormField, StatusBadge, StatusSelect, useConfirm , DateInput } from "../components/UI.jsx"; +import { MahnModal } from "./Protokolle.jsx"; + +export default +function Invoices({ data, update, saveAll, modal, setModal, setPrintContent, setView }) { + const invoices = data.invoices || []; + const clients = (data.persons || []).filter(p => p.isAuftraggeber); + const [form, setForm] = useState({ number: "", clientId: "", contactId: "", projectId: "", date: new Date().toISOString().slice(0, 10), dueDate: "", items: [{ id: generateId(), desc: "", qty: 1, price: 0 }], mwst: true, notes: "", status: "entwurf" }); + const [filter, setFilter] = useState(() => { const cid = window.__navClientId || ""; window.__navClientId = null; return { status: "", clientId: cid, year: "", search: "" }; }); + const [groupBy, setGroupBy] = useState("date"); + const [sort, setSort] = useState({ col: "date", dir: -1 }); + const toggleSort = (col) => setSort(s => ({ col, dir: s.col === col ? -s.dir : -1 })); + const SortTh = ({ col, children, style }) => ( + toggleSort(col)} style={{ cursor: "pointer", userSelect: "none", whiteSpace: "nowrap", ...style }}> + {children} {sort.col === col ? (sort.dir === 1 ? "▲" : "▼") : "⇅"} + + ); + + const nextNumber = () => { + const fmt = data.settings.invoiceNumberFormat || "YYYY-NNN"; + const now = new Date(); + const yyyy = String(now.getFullYear()); + const yy = yyyy.slice(2); + // Build regex to extract the sequence number from existing invoice numbers + const pattern = fmt + .replace(/[-/\^$*+?.()|[\]{}]/g, "\\$&") + .replace(/YYYY/g, yyyy).replace(/YY/g, yy) + .replace(/N+/, "(\\d+)"); + const rx = new RegExp("^" + pattern + "$"); + const nums = invoices.map(i => { + const m = (i.number || "").match(rx); + return m ? parseInt(m[1]) : null; + }).filter(n => n !== null); + const nextSeq = nums.length ? Math.max(...nums) + 1 : 1; + const padLen = (fmt.match(/N+/) || ["NNN"])[0].length; + return fmt + .replace(/YYYY/g, yyyy).replace(/YY/g, yy) + .replace(/N+/, String(nextSeq).padStart(padLen, "0")); + }; + + const openNew = () => { + const due = new Date(); due.setDate(due.getDate() + 30); + setForm({ number: nextNumber(), clientId: "", projectId: "", date: new Date().toISOString().slice(0, 10), dueDate: due.toISOString().slice(0, 10), items: [{ id: generateId(), desc: "", qty: 1, price: 0 }], mwst: true, mwstRate: data.settings.mwstRate, notes: "Zahlbar innert 30 Tagen netto.", status: "entwurf" }); + setModal({ type: "invoice" }); + }; + + const editingId = modal?.id || null; + + const calcTotal = (items, mwst, rate, discountType, discountValue) => { + const sub = (items || []).reduce((s, i) => { + const line = i.qty * i.price; + const disc = (i.discount || 0) > 0 ? line * ((i.discount || 0) / 100) : 0; + return s + line - disc; + }, 0); + const globalDisc = discountType === "percent" ? sub * ((discountValue || 0) / 100) + : discountType === "amount" ? (discountValue || 0) : 0; + const subAfterDisc = sub - globalDisc; + const tax = mwst ? subAfterDisc * (rate / 100) : 0; + const total = roundCHF(subAfterDisc + tax); + return { sub, globalDisc, subAfterDisc, tax, total }; + }; + + const calcLineTotal = (item) => { + const line = (item.qty || 0) * (item.price || 0); + return line * (1 - ((item.discount || 0) / 100)); + }; + + // Offene Zeiteinträge + die bereits zu dieser Rechnung gehören (beim Bearbeiten) + const openEntriesForProject = (projectId) => { + if (!projectId) return []; + return data.timeEntries + .filter(e => e.projectId === projectId && (!e.invoiceId || e.invoiceId === editingId)) + .sort((a, b) => (a.date || "").localeCompare(b.date || "")); + }; + + const [sendModal, setSendModal] = useState(null); + const [newInvModal, setNewInvModal] = useState(false); + const [ni, setNi] = useState({ tab: "projekt", projectId: "", quoteId: "", mode: "teilrechnung", pct: 30, amt: 0, phaseId: "" }); + + useEffect(() => { + if (modal?.type === "newInvoice" && modal.quoteId) { + setNi({ tab: "offerte", quoteId: modal.quoteId, mode: "schluss", pct: 30, amt: 0 }); + setNewInvModal(true); + setModal(null); + } + }, [modal?.type, modal?.quoteId]); + + // ── Akonto-Planer ────────────────────────────────────────────── + const defaultStartDate = () => { + const d = new Date(); d.setDate(1); d.setMonth(d.getMonth() + 1); + return d.toISOString().slice(0, 10); + }; + const emptyAp = { projectId: "", clientId: "", totalAmt: 0, mode: "interval", intervalMonths: 4, count: 4, startDate: defaultStartDate(), manualRows: [] }; + const [akontoModal, setAkontoModal] = useState(false); + const [ap, setAp] = useState(emptyAp); + + const apRows = (() => { + if (ap.mode === "interval") { + const start = new Date(ap.startDate || new Date()); + const perInv = ap.count > 0 ? Math.round((ap.totalAmt / ap.count) * 100) / 100 : 0; + return Array.from({ length: Math.max(1, Math.min(ap.count, 24)) }, (_, i) => { + const d = new Date(start); + d.setMonth(d.getMonth() + i * (ap.intervalMonths || 1)); + return { id: String(i), date: d.toISOString().slice(0, 10), amt: perInv }; + }); + } + return ap.manualRows; + })(); + + const generateSequentialNumbers = (count) => { + const fmt = data.settings.invoiceNumberFormat || "YYYY-NNN"; + const now = new Date(); + const yyyy = String(now.getFullYear()); const yy = yyyy.slice(2); + const escaped = fmt.replace(/[-/\^$*+?.()|[\]{}]/g, "\\$&").replace(/YYYY/g, yyyy).replace(/YY/g, yy).replace(/N+/, "(\\d+)"); + const rx = new RegExp("^" + escaped + "$"); + const nums = invoices.map(i => { const m = (i.number || "").match(rx); return m ? parseInt(m[1]) : null; }).filter(n => n !== null); + let nextSeq = nums.length ? Math.max(...nums) + 1 : 1; + const padLen = (fmt.match(/N+/) || ["NNN"])[0].length; + return Array.from({ length: count }, () => { + const n = fmt.replace(/YYYY/g, yyyy).replace(/YY/g, yy).replace(/N+/, String(nextSeq).padStart(padLen, "0")); + nextSeq++; + return n; + }); + }; + + const createAkontoInvoices = () => { + const rows = apRows.filter(r => r.date && r.amt > 0); + if (rows.length === 0) return; + const proj = data.projects.find(p => p.id === ap.projectId); + const numbers = generateSequentialNumbers(rows.length); + const newInvoices = rows.map((row, i) => { + const due = new Date(row.date); due.setDate(due.getDate() + 30); + const { sub, tax, total, subAfterDisc, globalDisc } = calcTotal( + [{ id: generateId(), desc: `Akontorechnung – ${proj?.name || ""}`, qty: 1, price: row.amt, discount: 0 }], + true, data.settings.mwstRate, "none", 0 + ); + return { + id: generateId(), number: numbers[i], date: row.date, + dueDate: due.toISOString().slice(0, 10), + clientId: ap.clientId || proj?.clientId || "", + projectId: ap.projectId, invoiceKind: "akonto", + items: [{ id: generateId(), desc: `Akontorechnung – ${proj?.name || ""}`, qty: 1, price: row.amt, discount: 0 }], + mwst: true, mwstRate: data.settings.mwstRate, + notes: "Zahlbar innert 30 Tagen netto.", + status: "entwurf", discountType: "none", discountValue: 0, + sub, tax, total, subAfterDisc, globalDisc, + createdAt: new Date().toISOString(), + }; + }); + update("invoices", [...invoices, ...newInvoices]); + setAkontoModal(false); + setAp(emptyAp); + }; + + const confirmNewInvoice = () => { + const todayStr = new Date().toISOString().slice(0, 10); + const due = new Date(); due.setDate(due.getDate() + 30); + const base = { + number: nextNumber(), date: todayStr, dueDate: due.toISOString().slice(0, 10), + mwst: true, mwstRate: data.settings.mwstRate, + notes: "Zahlbar innert 30 Tagen netto.", status: "entwurf", + discountType: "none", discountValue: 0, discountLabel: "Rabatt", + }; + + if (ni.tab === "frei") { + setForm({ ...base, clientId: "", projectId: "", entrySelections: {}, items: [{ id: generateId(), desc: "", qty: 1, price: 0, discount: 0 }] }); + } else if (ni.tab === "projekt") { + const proj = data.projects.find(p => p.id === ni.projectId); + if (!proj) return; + const bType = proj.billingType || proj.type; + const projBudget = proj.budget || proj.budgetAmount || 0; + if (ni.mode === "teilrechnung") { + const entries = openEntriesForProject(proj.id); + const entrySelections = {}; + entries.forEach(e => { entrySelections[e.id] = { selected: true, billedMinutes: e.minutes }; }); + const hours = Math.round(entries.reduce((s, e) => s + e.minutes, 0) / 60 * 100) / 100; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections, invoiceKind: "teilrechnung", + items: [{ id: generateId(), desc: `Teilrechnung – Architektur-/Grafikleistungen – ${proj.name}`, qty: hours, price: proj.hourlyRate || 0, discount: 0 }] }); + } else if (ni.mode === "teilrechnung-percent") { + const amt = projBudget * (ni.pct / 100); + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "teilrechnung", + items: [{ id: generateId(), desc: `Teilrechnung – ${proj.name} (${ni.pct.toFixed(1)}%)`, qty: 1, price: Math.round(amt * 100) / 100, discount: 0 }] }); + } else if (ni.mode === "teilrechnung-amount") { + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "teilrechnung", + items: [{ id: generateId(), desc: `Teilrechnung – ${proj.name}`, qty: 1, price: ni.amt, discount: 0 }] }); + } else if (ni.mode === "akonto-percent") { + const amt = projBudget * (ni.pct / 100); + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "akonto", + items: [{ id: generateId(), desc: `Akontorechnung – ${proj.name} (${ni.pct.toFixed(1)}%)`, qty: 1, price: Math.round(amt * 100) / 100, discount: 0 }] }); + } else if (ni.mode === "akonto-amount") { + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "akonto", + items: [{ id: generateId(), desc: `Akontorechnung – ${proj.name}`, qty: 1, price: ni.amt, discount: 0 }] }); + } else if (ni.mode === "akonto-stunden") { + const entries = openEntriesForProject(proj.id).filter(e => (e.date || "") <= todayStr); + const entrySelections = {}; + entries.forEach(e => { entrySelections[e.id] = { selected: true, billedMinutes: e.minutes }; }); + const hours = Math.round(entries.reduce((s, e) => s + e.minutes, 0) / 60 * 100) / 100; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections, invoiceKind: "akonto", + items: [{ id: generateId(), desc: `Akontorechnung – ${proj.name}`, qty: hours, price: proj.hourlyRate || 0, discount: 0 }] }); + } else if (ni.mode === "phase") { + const ph = (proj.phasesBudget || []).find(p => p.id === ni.phaseId); + const phInfo = SIA_PHASES.find(p => p.id === ni.phaseId); + const amt = Math.round((ph?.hours || 0) * (proj.hourlyRate || 0) * 100) / 100; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "teilrechnung", + items: [{ id: generateId(), desc: `Teilrechnung ${phInfo?.label || ni.phaseId} – ${proj.name}`, qty: 1, price: amt, discount: 0 }] }); + } else if (ni.mode === "akonto-phase") { + const ph = (proj.phasesBudget || []).find(p => p.id === ni.phaseId); + const phInfo = SIA_PHASES.find(p => p.id === ni.phaseId); + const amt = Math.round((ph?.hours || 0) * (proj.hourlyRate || 0) * 100) / 100; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "akonto", + items: [{ id: generateId(), desc: `Akontorechnung ${phInfo?.label || ni.phaseId} – ${proj.name}`, qty: 1, price: amt, discount: 0 }] }); + } else { + // schluss + if (bType === "stundensatz") { + const entries = openEntriesForProject(proj.id).filter(e => (e.date || "") <= todayStr); + const entrySelections = {}; + entries.forEach(e => { entrySelections[e.id] = { selected: true, billedMinutes: e.minutes }; }); + const hours = Math.round(entries.reduce((s, e) => s + e.minutes, 0) / 60 * 100) / 100; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections, invoiceKind: "schluss", + items: [{ id: generateId(), desc: `Schlussrechnung – ${proj.name}`, qty: hours, price: proj.hourlyRate || 0, discount: 0 }] }); + } else { + // Pauschal-Schluss: Gesamthonorar ausweisen, bisherige Akontozahlungen abziehen + const akontoInvs = invoices.filter(i => i.projectId === proj.id && i.invoiceKind === "akonto" && i.status !== "entwurf"); + const akontoTotal = akontoInvs.reduce((s, i) => s + (i.subAfterDisc ?? i.sub ?? 0), 0); + const schlussItems = [ + { id: generateId(), desc: `Pauschalhonorar – ${proj.name}`, qty: 1, price: projBudget, discount: 0 }, + ...(akontoTotal > 0 ? [{ id: generateId(), desc: `Abzgl. geleistete Akontozahlungen (${akontoInvs.map(i => i.number).filter(Boolean).join(", ")})`, qty: 1, price: -akontoTotal, discount: 0 }] : []), + ]; + const notes = akontoTotal > 0 + ? `Schlussrechnung gemäss Honorarvereinbarung.\nBisherige Akontozahlungen:\n${akontoInvs.map(i => ` – ${i.number}: CHF ${(i.subAfterDisc ?? i.sub ?? 0).toFixed(2)}`).join("\n")}\n\nZahlbar innert 30 Tagen netto.` + : "Zahlbar innert 30 Tagen netto."; + setForm({ ...base, clientId: proj.clientId || "", projectId: proj.id, entrySelections: {}, invoiceKind: "schluss", items: schlussItems, notes }); + } + } + } else if (ni.tab === "offerte") { + const q = (data.quotes || []).find(x => x.id === ni.quoteId); + if (!q) return; + const existing = (invoices).filter(i => i.quoteId === q.id); + const totalInvoiced = existing.reduce((s, i) => s + (i.sub || 0), 0); + const remaining = Math.max(0, (q.sub || 0) - totalInvoiced); + let items = [], invoiceKind = "akonto"; + if (ni.mode === "schluss") { + invoiceKind = "schluss"; + const akontoInvs = existing.filter(i => i.invoiceKind === "akonto" && i.status !== "entwurf"); + const akontoTotal = akontoInvs.reduce((s, i) => s + (i.subAfterDisc ?? i.sub ?? 0), 0); + items = [ + { id: generateId(), desc: `Honorar gemäss Offerte ${q.number}`, qty: 1, price: q.sub || 0, discount: 0 }, + ...(akontoTotal > 0 ? [{ id: generateId(), desc: `Abzgl. geleistete Akontozahlungen (${akontoInvs.map(i => i.number).filter(Boolean).join(", ")})`, qty: 1, price: -akontoTotal, discount: 0 }] : []), + ]; + } else if (ni.mode === "teilrechnung-percent") { + invoiceKind = "teilrechnung"; + const amt = (q.sub || 0) * (ni.pct / 100); + items = [{ id: generateId(), desc: `Teilrechnung gemäss Offerte ${q.number} (${ni.pct.toFixed(1)}%)`, qty: 1, price: Math.round(amt * 100) / 100, discount: 0 }]; + } else if (ni.mode === "teilrechnung-amount") { + invoiceKind = "teilrechnung"; + items = [{ id: generateId(), desc: `Teilrechnung gemäss Offerte ${q.number}`, qty: 1, price: ni.amt, discount: 0 }]; + } else if (ni.mode === "akonto-percent") { + const amt = (q.sub || 0) * (ni.pct / 100); + items = [{ id: generateId(), desc: `Akontorechnung gemäss Offerte ${q.number} (${ni.pct.toFixed(1)}%)`, qty: 1, price: Math.round(amt * 100) / 100, discount: 0 }]; + } else { + items = [{ id: generateId(), desc: `Akontorechnung gemäss Offerte ${q.number}`, qty: 1, price: ni.amt, discount: 0 }]; + } + let notes = `Gemäss Offerte ${q.number}. Zahlbar innert 30 Tagen netto.`; + if (invoiceKind === "schluss" && existing.filter(i => i.invoiceKind === "akonto").length > 0) { + const akontoInvs = existing.filter(i => i.invoiceKind === "akonto" && i.status !== "entwurf"); + notes = `Schlussrechnung gemäss Offerte ${q.number}.\nBisherige Akontozahlungen:\n${akontoInvs.map(i => ` – ${i.number}: CHF ${(i.subAfterDisc ?? i.sub ?? 0).toFixed(2)}`).join("\n")}\n\nZahlbar innert 30 Tagen netto.`; + } + setForm({ ...base, clientId: q.clientId || "", projectId: q.projectId || "", quoteId: q.id, entrySelections: {}, invoiceKind, items, notes }); + } + setNewInvModal(false); + setModal({ type: "invoice" }); + }; + + const save = (statusOverride) => { + if (!form.clientId) { + alert("Bitte einen Kunden auswählen."); + return; + } + const { sub, globalDisc, subAfterDisc, tax, total } = calcTotal(form.items, form.mwst, form.mwstRate ?? data.settings.mwstRate, form.discountType, form.discountValue); + const invoiceId = editingId || generateId(); + const status = statusOverride || form.status || "entwurf"; + const inv = { ...form, id: invoiceId, sub, globalDisc, subAfterDisc, tax, total, mwstRate: form.mwstRate ?? data.settings.mwstRate, status }; + delete inv.entrySelections; + + const selectedIds = Object.entries(form.entrySelections || {}) + .filter(([_, sel]) => sel.selected) + .map(([id]) => id); + + const updatedEntries = data.timeEntries.map(e => { + if (e.invoiceId === editingId) { + return selectedIds.includes(e.id) ? { ...e, invoiceId } : { ...e, invoiceId: null }; + } + if (selectedIds.includes(e.id)) return { ...e, invoiceId }; + return e; + }); + + const updatedInvoices = editingId + ? invoices.map(i => i.id === editingId ? { ...i, ...inv } : i) + : [...invoices, { ...inv, createdAt: new Date().toISOString() }]; + + saveAll({ ...data, invoices: updatedInvoices, timeEntries: updatedEntries }); + setModal(null); + if (!editingId && !statusOverride) setSendModal(inv); + }; + + const openEdit = (inv) => { + const linked = data.timeEntries.filter(e => e.invoiceId === inv.id); + const entrySelections = {}; + linked.forEach(e => { entrySelections[e.id] = { selected: true, billedMinutes: e.minutes }; }); + setForm({ + discountType: "none", discountValue: 0, discountLabel: "Rabatt", + ...inv, + items: (inv.items || []).map(it => ({ discount: 0, ...it })), + entrySelections, + }); + setModal({ type: "invoice", id: inv.id }); + }; + + const del = async (id) => { + if (!(await askConfirm("Rechnung löschen? Zugehörige Zeiteinträge werden wieder als offen markiert."))) return; + const updatedEntries = data.timeEntries.map(e => e.invoiceId === id ? { ...e, invoiceId: null } : e); + saveAll({ ...data, invoices: invoices.filter(i => i.id !== id), timeEntries: updatedEntries }); + }; + const setStatus = (id, status) => update("invoices", invoices.map(i => i.id === id ? { ...i, status } : i)); + + const addItem = () => setForm({ ...form, items: [...form.items, { id: generateId(), desc: "", qty: 1, price: 0, discount: 0 }] }); + const removeItem = (idx) => setForm({ ...form, items: form.items.filter((_, i) => i !== idx) }); + const updateItem = (idx, key, val) => setForm({ ...form, items: form.items.map((it, i) => i === idx ? { ...it, [key]: val } : it) }); + const [openDropdown, setOpenDropdown] = useState(null); + const [dropdownData, setDropdownData] = useState(null); + const [mahnModal, setMahnModal] = useState(null); + const [mahnMode, setMahnMode] = useState("new"); + const [mahnSentDate, setMahnSentDate] = useState(new Date().toISOString().slice(0, 10)); + const { askConfirm, ConfirmModalEl } = useConfirm(); + + const openMahnung = (inv) => { + setMahnMode((inv.reminders || []).length === 0 ? "new" : "reprint-0"); + setMahnSentDate(new Date().toISOString().slice(0, 10)); + setMahnModal({ inv }); + }; + + const toggleEntrySelection = (entryId, entry) => { + const current = form.entrySelections?.[entryId]; + const newSelections = { ...form.entrySelections }; + if (current?.selected) { + newSelections[entryId] = { ...current, selected: false }; + } else { + newSelections[entryId] = { selected: true, billedMinutes: current?.billedMinutes ?? entry.minutes }; + } + setForm(f => ({ ...f, entrySelections: newSelections })); + }; + + const updateBilledMinutes = (entryId, minutes) => { + const newSelections = { ...form.entrySelections }; + newSelections[entryId] = { ...(newSelections[entryId] || { selected: true }), billedMinutes: Math.max(0, minutes) }; + setForm(f => ({ ...f, entrySelections: newSelections })); + }; + + const prefillFromProject = (projectId) => { + if (!projectId) { setForm(f => ({ ...f, projectId: "", entrySelections: {} })); return; } + const proj = data.projects.find(p => p.id === projectId); + if (!proj) return; + const bType = proj.billingType || proj.type; + const openEntries = openEntriesForProject(projectId); + if (bType === "stundensatz") { + const entrySelections = {}; + openEntries.forEach(e => { entrySelections[e.id] = { selected: true, billedMinutes: e.minutes }; }); + const totalMins = openEntries.reduce((s, e) => s + e.minutes, 0); + const hours = Math.round(totalMins / 60 * 100) / 100; + setForm(f => ({ + ...f, projectId, clientId: proj.clientId || f.clientId, entrySelections, + items: hours > 0 + ? [{ id: generateId(), desc: `Architektur-/Grafikleistungen – ${proj.name}`, qty: hours, price: proj.hourlyRate, discount: 0 }] + : [{ id: generateId(), desc: "", qty: 1, price: 0, discount: 0 }] + })); + } else if (bType === "pauschal") { + setForm(f => ({ ...f, projectId, clientId: proj.clientId || f.clientId, entrySelections: {}, + items: [{ id: generateId(), desc: `Pauschalhonorar – ${proj.name}`, qty: 1, price: proj.budget, discount: 0 }] })); + } else { + setForm(f => ({ ...f, projectId, clientId: proj.clientId || f.clientId, entrySelections: {} })); + } + }; + + useEffect(() => { + if (!openDropdown) return; + const close = () => { setOpenDropdown(null); setDropdownData(null); }; + document.addEventListener("click", close); + return () => document.removeEventListener("click", close); + }, [openDropdown]); + + useEffect(() => { + if (!form.projectId) return; + const proj = data.projects.find(p => p.id === form.projectId); + if (!proj || (proj.billingType || proj.type) !== "stundensatz") return; + const totalBilledMins = Object.values(form.entrySelections || {}) + .filter(s => s.selected) + .reduce((sum, s) => sum + (s.billedMinutes || 0), 0); + const hours = Math.round(totalBilledMins / 60 * 100) / 100; + setForm(f => { + const existing = f.items[0] || {}; + if (existing.qty === hours && existing.price === proj.hourlyRate) return f; + const newItems = [...f.items]; + newItems[0] = { id: existing.id || generateId(), desc: existing.desc || `Architektur-/Grafikleistungen – ${proj.name}`, qty: hours, price: proj.hourlyRate, discount: existing.discount || 0 }; + return { ...f, items: newItems }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form.entrySelections, form.projectId]); + + const { sub, globalDisc, subAfterDisc, tax, total } = calcTotal(form.items, form.mwst, form.mwstRate ?? data.settings.mwstRate, form.discountType, form.discountValue); + + // Verfügbare Jahre aus Rechnungen sammeln + const availableYears = Array.from(new Set(invoices.map(i => (i.date || "").slice(0, 4)).filter(Boolean))).sort().reverse(); + + // Filter-Matching + const matchesFilter = (inv) => { + if (filter.status && inv.status !== filter.status) return false; + if (filter.clientId && inv.clientId !== filter.clientId) return false; + if (filter.year && !(inv.date || "").startsWith(filter.year)) return false; + if (filter.search) { + const q = filter.search.toLowerCase(); + const client = clients.find(c => c.id === inv.clientId); + const haystack = [ + inv.number, + client?.name, + client?.company, + inv.notes, + ...(inv.items || []).map(it => it.desc) + ].filter(Boolean).join(" ").toLowerCase(); + if (!haystack.includes(q)) return false; + } + return true; + }; + + const filtered = [...invoices].filter(matchesFilter).sort((a, b) => { + let va, vb; + if (sort.col === "number") { va = a.number || ""; vb = b.number || ""; } + else if (sort.col === "date") { va = a.date || ""; vb = b.date || ""; } + else if (sort.col === "dueDate") { va = a.dueDate || ""; vb = b.dueDate || ""; } + else if (sort.col === "client") { va = clients.find(c => c.id === a.clientId)?.name || ""; vb = clients.find(c => c.id === b.clientId)?.name || ""; } + else if (sort.col === "total") { va = a.total || 0; vb = b.total || 0; } + else if (sort.col === "status") { va = a.status || ""; vb = b.status || ""; } + else { va = a.date || ""; vb = b.date || ""; } + return typeof va === "number" ? (va - vb) * sort.dir : va.localeCompare(vb) * sort.dir; + }); + const filteredTotal = filtered.reduce((s, i) => s + (i.total || 0), 0); + const filteredOpen = filtered.filter(i => i.status === "gesendet" || i.status === "überfällig").reduce((s, i) => s + (i.total || 0), 0); + const filteredOverdue = filtered.filter(i => i.status === "überfällig").reduce((s, i) => s + (i.total || 0), 0); + const filteredPaid = filtered.filter(i => i.status === "bezahlt").reduce((s, i) => s + (i.total || 0), 0); + + // Unverrechnete Stunden über alle aktiven Stundensatz-Projekte (global) + const unbilledProjects = data.projects + .filter(p => (p.billingType || p.type) === "stundensatz" && p.status === "aktiv") + .map(p => { + const mins = data.timeEntries.filter(e => e.projectId === p.id && !e.invoiceId).reduce((s, e) => s + (e.minutes || 0), 0); + const amount = (mins / 60) * (p.hourlyRate || 0); + return { ...p, mins, amount }; + }) + .filter(p => p.mins > 0) + .sort((a, b) => b.mins - a.mins); + const totalUnbilledMins = unbilledProjects.reduce((s, p) => s + p.mins, 0); + const totalUnbilledAmount = unbilledProjects.reduce((s, p) => s + p.amount, 0); + + // Gruppieren + const groupedInvoices = (() => { + if (groupBy === "date") { + const months = {}; + filtered.forEach(inv => { + const key = (inv.date || "").slice(0, 7); + if (!months[key]) months[key] = []; + months[key].push(inv); + }); + return Object.entries(months).sort((a, b) => b[0].localeCompare(a[0])).map(([key, items]) => ({ + key, label: key ? new Date(key + "-01").toLocaleDateString("de-CH", { month: "long", year: "numeric" }) : "Ohne Datum", items, + })); + } + if (groupBy === "client") { + const clients = {}; + filtered.forEach(inv => { + const cl = clients.find(c => c.id === inv.clientId); + const key = inv.clientId || "__none__"; + const label = cl?.name || "Kein Kunde"; + if (!clients[key]) clients[key] = { label, items: [] }; + clients[key].items.push(inv); + }); + return Object.entries(clients).sort((a, b) => a[1].label.localeCompare(b[1].label)).map(([key, val]) => ({ key, label: val.label, items: val.items })); + } + if (groupBy === "status") { + const statuses = {}; + filtered.forEach(inv => { + const key = inv.status || "unbekannt"; + if (!statuses[key]) statuses[key] = []; + statuses[key].push(inv); + }); + return Object.entries(statuses).sort((a, b) => a[0].localeCompare(b[0])).map(([key, items]) => ({ key, label: key.charAt(0).toUpperCase() + key.slice(1), items })); + } + return [{ key: "all", label: "", items: filtered }]; + })(); + + return ( +
+ {ConfirmModalEl} +
+ +
+
+ + +
+ } /> + + {/* KPI-Cards */} +
+
+
BEZAHLT
+
{formatCHF(filteredPaid)}
+
{filtered.filter(i => i.status === "bezahlt").length} Rechnung(en)
+
+
0 ? "3px solid #8a1a1a" : "3px solid #7a6a00" }}> +
AUSSTEHEND
+
0 ? "#8a1a1a" : "#7a6a00" }}>{formatCHF(filteredOpen)}
+
+ {filteredOverdue > 0 ? davon {formatCHF(filteredOverdue)} überfällig : `${filtered.filter(i => i.status === "gesendet" || i.status === "überfällig").length} Rechnung(en)`} +
+
+
0 ? "3px solid #b5621e" : "3px solid #aaa" }}> +
UNVERRECHNETE STUNDEN
+
0 ? "#b5621e" : "#aaa" }}>{formatHours(totalUnbilledMins)}
+
{totalUnbilledAmount > 0 ? `≈ ${formatCHF(totalUnbilledAmount)}` : "Alle verrechnet ✓"}
+
+
+
PROJEKTE MIT PENDENZEN
+ {unbilledProjects.length === 0 ? ( +
Keine offenen Stunden
+ ) : ( + <> +
{unbilledProjects.length} Projekt{unbilledProjects.length !== 1 ? "e" : ""}
+ {unbilledProjects.slice(0, 4).map(p => ( +
+
+ {p.name} + {formatHours(p.mins)} +
+
+
+
+
+ ))} + + )} +
+
+ +
+ setFilter({ ...filter, search: e.target.value })} style={{ minWidth: 220 }} /> + + + + {(filter.status || filter.clientId || filter.year || filter.search) && ( + + )} +
+ +
+ GRUPPIEREN: + {[{ id: "none", label: "Keine" }, { id: "date", label: "Monat" }, { id: "client", label: "Kunde" }, { id: "status", label: "Status" }].map(g => ( + + ))} +
+
{filtered.length} {filtered.length === 1 ? "Rechnung" : "Rechnungen"} · {formatCHF(filteredTotal)}
+ {filteredOpen > 0 &&
davon offen: {formatCHF(filteredOpen)}
} +
+
+ +
+ + Nr.KundeDatumFälligBetragStatus + + {filtered.length === 0 && } + {groupedInvoices.map(group => ( + + {groupBy !== "none" && group.label && ( + + + + )} + {group.items.map(inv => { + const client = clients.find(c => c.id === inv.clientId); + const kindLabel = inv.invoiceKind === "akonto" ? "Akonto" : inv.invoiceKind === "teilrechnung" ? "Teilrechnung" : inv.invoiceKind === "schluss" ? "Schluss" : inv.invoiceKind === "voll" ? "Vollrechnung" : "—"; + const kindColor = inv.invoiceKind === "akonto" ? "#7a6a00" : inv.invoiceKind === "teilrechnung" ? "#2d5a8e" : inv.invoiceKind === "schluss" ? "#1a4e8a" : "transparent"; + return ( + + + + + + + + + + + ); + })} + + ))} + +
Art
{invoices.length === 0 ? "Noch keine Rechnungen" : "Keine Treffer"}
+ {group.label} + + {group.items.length} · {formatCHF(group.items.reduce((s, i) => s + (i.total || 0), 0))} + +
+ {inv.number} + {(inv.reminders || []).length > 0 && ( +
+ {(inv.reminders || []).length === 1 ? "Erinnerung" : `${(inv.reminders || []).length}× Mahnung`} · {formatDate((inv.reminders || []).at(-1)?.date)} +
+ )} +
{client?.name || "—"} + {inv.invoiceKind && inv.invoiceKind !== "voll" + ? {kindLabel} + : } + {formatDate(inv.date)}{formatDate(inv.dueDate)}{formatCHF(inv.total)} + setStatus(inv.id, v)} /> + + {inv.status === "überfällig" && ( + + )} +
+ + +
+ + +
+
+ + {openDropdown && dropdownData && ( +
e.stopPropagation()} + > + {[ + { label: "Rechnung + QR", type: "invoice+qr" }, + { label: "Nur Rechnung", type: "invoice" }, + { label: "Nur QR", type: "qrbill" }, + ].map(opt => ( + + ))} +
+ )} + + {mahnModal && ( + setMahnModal(null)} + mahnMode={mahnMode} + setMahnMode={setMahnMode} + mahnSentDate={mahnSentDate} + setMahnSentDate={setMahnSentDate} + /> + )} + + {modal?.type === "invoice" && ( + setModal(null)} onSave={() => save()} wide> +
+ setForm({ ...form, number: e.target.value })} /> + setForm({ ...form, date: e.target.value })} /> + setForm({ ...form, dueDate: e.target.value })} /> +
+
+ + + + {form.clientId && (() => { + const cl = clients.find(c => c.id === form.clientId); + const contacts = cl?.contacts || []; + if (contacts.length === 0) return null; + return ( + + + + ); + })()} + + + +
+ + {/* Offene Zeiteinträge des Projekts */} + {(() => { + if (!form.projectId) return null; + const proj = data.projects.find(p => p.id === form.projectId); + if (!proj) return null; + const bType = proj.billingType || proj.type; + if (bType !== "stundensatz") return null; + const openEntries = openEntriesForProject(form.projectId); + if (openEntries.length === 0) return ( +
+ Keine offenen Zeiteinträge für dieses Projekt. +
+ ); + const totalErfasst = openEntries.reduce((s, e) => s + e.minutes, 0); + const totalVerrechnet = Object.entries(form.entrySelections || {}) + .filter(([id, s]) => s.selected && openEntries.some(e => e.id === id)) + .reduce((s, [_, sel]) => s + (sel.billedMinutes || 0), 0); + const diff = totalErfasst - totalVerrechnet; + const allSelected = openEntries.every(e => form.entrySelections?.[e.id]?.selected); + const toggleAll = () => { + const newSel = { ...form.entrySelections }; + if (allSelected) { + openEntries.forEach(e => { newSel[e.id] = { ...newSel[e.id], selected: false }; }); + } else { + openEntries.forEach(e => { newSel[e.id] = { selected: true, billedMinutes: newSel[e.id]?.billedMinutes ?? e.minutes }; }); + } + setForm(f => ({ ...f, entrySelections: newSel })); + }; + return ( +
+
+ +
+ Erfasst {formatHours(totalErfasst)} · Verrechnet {formatHours(totalVerrechnet)} + {diff > 0 && Nicht verrechnet: {formatHours(diff)}} +
+
+
+ + + + + + + + + + + + + {openEntries.map(e => { + const sel = form.entrySelections?.[e.id]; + const selected = sel?.selected; + const billedMinutes = selected ? (sel?.billedMinutes ?? e.minutes) : 0; + const hoursBilled = Math.round((billedMinutes / 60) * 100) / 100; + const phase = SIA_PHASES.find(p => p.id === e.phaseId); + const pos = (proj?.positions || []).find(p => p.code === e.positionId); + return ( + + + + + + + + + ); + })} + +
+ + DatumPhaseTätigkeitErfasstVerrechnet
+ toggleEntrySelection(e.id, e)} style={{ width: "auto", cursor: "pointer" }} /> + {formatDate(e.date)} + {phase?.id || "—"} + {pos && {pos.code}} + {e.description || "—"}{formatHours(e.minutes)} + {selected ? ( + updateBilledMinutes(e.id, Math.round(+ev.target.value * 60))} + style={{ width: 70, height: 26, fontSize: 11, textAlign: "right", padding: "2px 6px" }} /> + ) : } + {selected && h} +
+
+
+ ); + })()} + +
+
+ + +
+
+
Beschreibung
+
Menge
+
Preis
+
Rabatt %
+
Total
+
+
+ {form.items.map((item, idx) => { + const lineTotal = calcLineTotal(item); + const hasDiscount = (item.discount || 0) > 0; + return ( +
+ updateItem(idx, "desc", e.target.value)} style={{ flex: 3 }} /> + updateItem(idx, "qty", +e.target.value)} style={{ width: 70, textAlign: "right" }} /> + updateItem(idx, "price", +e.target.value)} style={{ width: 90, textAlign: "right" }} /> + updateItem(idx, "discount", +e.target.value)} style={{ width: 60, textAlign: "right", background: hasDiscount ? "#fff8ed" : undefined }} /> +
+ {hasDiscount &&
{formatCHF(item.qty * item.price)}
} +
{formatCHF(lineTotal)}
+
+ +
+ ); + })} +
+ + {/* Gesamt-Rabatt */} +
+
+ + + {form.discountType && form.discountType !== "none" && ( + <> + setForm({ ...form, discountValue: +e.target.value })} style={{ width: 90, textAlign: "right" }} /> + setForm({ ...form, discountLabel: e.target.value })} style={{ flex: 1, minWidth: 180 }} /> + + )} +
+
+ +
+
+ + {form.mwst && ( +
+ setForm({ ...form, mwstRate: +e.target.value })} style={{ width: 60, fontSize: 12, height: 28 }} /> + % +
+ )} +
+
+
Zwischentotal {formatCHF(sub)}
+ {globalDisc > 0 &&
{form.discountLabel || "Rabatt"} −{formatCHF(globalDisc)}
} + {globalDisc > 0 &&
Nach Rabatt {formatCHF(subAfterDisc)}
} + {form.mwst &&
MWST {formatCHF(tax)}
} +
Total {formatCHF(total)}
+
+
+ +