commit 93b9781aeb40f6b094e5229ec0fb371cd73e3174 Author: Arun Reddy Date: Fri Oct 9 12:16:48 2026 +0530 Initial commit of Sidrah Audit App diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b901702 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 4 +max_line_length = 99 + +# JSON files - mostly doctype schema files +[{*.json}] +insert_final_newline = false +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c5e7d68 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,124 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "frappe": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "posthog": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba04025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +*.pyc +*.egg-info +*.swp +tags +node_modules +__pycache__ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3012161 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +exclude: 'node_modules|.git' +default_stages: [pre-commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: "sidrah_business.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.1 + hooks: + - id: ruff + name: "Run ruff import sorter" + args: ["--select=I", "--fix"] + + - id: ruff + name: "Run ruff linter" + + - id: ruff-format + name: "Run ruff formatter" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + sidrah_business/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + sidrah_business/templates/includes/.*| + sidrah_business/public/js/lib/.* + )$ + + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + sidrah_business/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + sidrah_business/templates/includes/.*| + sidrah_business/public/js/lib/.* + )$ + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/README.md b/README.md new file mode 100644 index 0000000..bae91f5 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +### Sidrah Business + +Sidrah Business + +### Installation + +You can install this app using the [bench](https://github.com/frappe/bench) CLI: + +```bash +cd $PATH_TO_YOUR_BENCH +bench get-app $URL_OF_THIS_REPO --branch develop +bench install-app sidrah_business +``` + +### Contributing + +This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: + +```bash +cd apps/sidrah_business +pre-commit install +``` + +Pre-commit is configured to use the following tools for checking and formatting your code: + +- ruff +- eslint +- prettier +- pyupgrade + +### License + +mit diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..eb8699f --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_URL=http://172.19.127.96:8000 \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..8179beb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,106 @@ +// README - Modular Sidrah Business App Structure + +## 📁 FILE STRUCTURE + +``` +src/ +├── App.tsx (MAIN - Login + Router) +├── components/ +│ ├── Sidebar.tsx (Navigation) +│ ├── Header.tsx (Page Title/Subtitle) +│ └── common.tsx (Colors, Styles, Helpers) +├── pages/ +│ ├── Dashboard.tsx (Overview Page) +│ ├── incidents/ +│ │ ├── IncidentForm.tsx (Submit Form) +│ │ └── IncidentsList.tsx (View All) +│ ├── ideas/ +│ │ ├── IdeaForm.tsx (Submit Form) +│ │ └── IdeasList.tsx (View All) +│ ├── orders/ +│ │ ├── OrderForm.tsx (Submit Form) +│ │ └── OrdersList.tsx (View All) +│ └── organizations/ +│ └── OrganizationsList.tsx (View All) +├── types/ +│ └── index.ts (All Interfaces) +└── utils/ + └── api.ts (API Calls) +``` + +## 🚀 SETUP INSTRUCTIONS + +### 1. Create Folder Structure +```bash +mkdir -p src/{components,pages/{incidents,ideas,orders,organizations},types,utils} +``` + +### 2. Copy Files to Correct Locations + +**Root Level:** +- `App.tsx` → `src/App.tsx` + +**Components:** +- `components_Sidebar.tsx` → `src/components/Sidebar.tsx` +- `components_Header.tsx` → `src/components/Header.tsx` +- `components_common.tsx` → `src/components/common.tsx` + +**Types:** +- `types_index.ts` → `src/types/index.ts` + +**Utils:** +- `utils_api.ts` → `src/utils/api.ts` + +**Pages:** +- `pages_Dashboard.tsx` → `src/pages/Dashboard.tsx` +- `pages_incidents_IncidentForm.tsx` → `src/pages/incidents/IncidentForm.tsx` +- `pages_incidents_IncidentsList.tsx` → `src/pages/incidents/IncidentsList.tsx` +- `pages_ideas_IdeaForm.tsx` → `src/pages/ideas/IdeaForm.tsx` +- `pages_ideas_IdeasList.tsx` → `src/pages/ideas/IdeasList.tsx` +- `pages_orders_OrderForm.tsx` → `src/pages/orders/OrderForm.tsx` +- `pages_orders_OrdersList.tsx` → `src/pages/orders/OrdersList.tsx` +- `pages_organizations_OrganizationsList.tsx` → `src/pages/organizations/OrganizationsList.tsx` + +## 📦 KEY BENEFITS + +✅ **Modular** - Each page in its own file +✅ **Scalable** - Easy to add new pages +✅ **Maintainable** - Clear folder structure +✅ **Reusable** - Shared components & utilities +✅ **Type-Safe** - Centralized type definitions +✅ **Clean** - Separated concerns + +## 🔧 HOW TO ADD NEW PAGE + +1. Create folder: `src/pages/newfeature/` +2. Create component files: + - `NewFeatureForm.tsx` + - `NewFeatureList.tsx` +3. Import in `App.tsx` +4. Add to Sidebar navigation +5. Done! 🎉 + +## 📝 EDIT COLORS + +Edit `src/components/common.tsx`: +```tsx +export const colors = { + primary: '#3b82f6', // Change this + secondary: '#10b981', + // ... etc +} +``` + +## 🔒 ENABLE ROLE RESTRICTIONS + +In `src/App.tsx`, uncomment lines to enable roles: +```tsx +// const isManager = user.role === 'Manager' +// const isReadOnly = user.role === 'Auditor' +``` + +Then pass to page components to conditionally render. + +--- + +**All modular, organized, production-ready!** 🚀 \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1dfeecc --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + Sidrah + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..466b4a5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3327 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^26.0.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "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/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "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/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "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": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "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": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "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/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.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/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4bbebf0 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^26.0.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", + "vite": "^5.4.10" + } +} diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..5187b26 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..09bd910 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,1485 @@ +import { useEffect, useState } from 'react' +import { colors, DOCTYPES, LIST_EXPORT_CONFIGS, SETTINGS_NAV } from './constants' +import { isWorkflowClosed } from './components/common' +import ExportModal from './components/ExportModal' +import DeleteConfirmModal from './components/DeleteConfirmModal' +import AppSidebar from './components/AppSidebar' +import { useT } from './i18n' +import { deleteResource, restoreResource, get, getResource, post, updateDoc, getUsers, logoutApi, getUserRoles, getSessionUser, getSBUsers, getSBUserRoleMap, disableUserApi, createDelegation, revokeDelegation, getDelegations, getDelegation, getMyDelegatedFunctions, getMyDelegatedOrganizations, getMyOrganization, getGroupCompanies, setUserPassword } from './api' +import { fetchProcess } from './utils/processAPI' +import { parseRoute, freshLoadPage, buildHash } from './routing' +import { Incident, Idea, Order, Process, Organization, Management, Department, Branch, IncidentType, IncidentClass, IncidentField, IdeaType, OrderType, ProcessType, IncidentStatus, IdeaStatus, OrderStatus, ProcessStatus, SBUser, User, Page, SBCycle, SBKpiInput, Delegation, SBGroupCompany } from './types' + +import LoginPage from './pages/LoginPage' +import DashboardPage from './pages/DashboardPage' +import IncidentsPage from './pages/IncidentsPage' +import IncidentFormPage from './pages/IncidentFormPage' +import IdeasPage from './pages/IdeasPage' +import IdeaFormPage from './pages/IdeaFormPage' +import OrdersPage from './pages/OrdersPage' +import OrderFormPage from './pages/OrderFormPage' +import ProcessesPage from './pages/ProcessesPage' +import ProcessFormPage from './pages/ProcessFormPage' +import TopBar from './components/TopBar' +import UserProfilePage from './pages/UserProfilePage' +import OrganizationsPage from './pages/OrganizationsPage' +import OrganizationFormPage from './pages/OrganizationFormPage' +import GroupCompaniesPage from './pages/GroupCompaniesPage' +import GroupCompanyFormPage from './pages/GroupCompanyFormPage' +import ManagementPage from './pages/ManagementPage' +import ManagementFormPage from './pages/ManagementFormPage' +import DepartmentPage from './pages/DepartmentPage' +import DepartmentFormPage from './pages/DepartmentFormPage' +import BranchPage from './pages/BranchPage' +import BranchFormPage from './pages/BranchFormPage' +import IncidentTypePage from './pages/IncidentTypePage' +import IncidentTypeFormPage from './pages/IncidentTypeFormPage' +import IdeaTypePage from './pages/IdeaTypePage' +import IdeaTypeFormPage from './pages/IdeaTypeFormPage' +import OrderTypePage from './pages/OrderTypePage' +import OrderTypeFormPage from './pages/OrderTypeFormPage' +import ProcessTypePage from './pages/ProcessTypePage' +import ProcessTypeFormPage from './pages/ProcessTypeFormPage' +import IncidentStatusPage from './pages/IncidentStatusPage' +import IncidentStatusFormPage from './pages/IncidentStatusFormPage' +import IdeaStatusPage from './pages/IdeaStatusPage' +import IdeaStatusFormPage from './pages/IdeaStatusFormPage' +import OrderStatusPage from './pages/OrderStatusPage' +import OrderStatusFormPage from './pages/OrderStatusFormPage' +import ProcessStatusPage from './pages/ProcessStatusPage' +import ProcessStatusFormPage from './pages/ProcessStatusFormPage' +import UsersPage from './pages/UsersPage' +import UserFormPage from './pages/UserFormPage' +import CyclesPage from './pages/CyclesPage' +import CycleFormPage from './pages/CycleFormPage' +import KpiInputPage from './pages/KpiInputPage' +import KpiInputFormPage from './pages/KpiInputFormPage' +import DelegationsPage from './pages/DelegationsPage' +import DelegationFormPage from './pages/DelegationFormPage' +import InboxPage from './pages/InboxPage' +import ReportsPage from './pages/ReportsPage' + +const today = () => new Date().toISOString().split('T')[0] +const in30Days = () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + +const nowTime = () => new Date().toTimeString().slice(0, 5) +const defaultIncForm = () => ({ name: '', number: '', title: '', organization: '', group_company: '', event_category: '', type: '', incident_class: '', incident_field: '', date: today(), time: nowTime(), management: '', department: '', section_field: '', branch: '', assigned_to: '', status: '', description: '', summary: '', workflow_state: 'New', hses_field: '', priority: '', economy_class: '', product_relation: 0, repeated: 0, entity_scope: '', solution: '', cost: 0, auditor_comment: '' }) +const defaultIdeaForm = () => ({ name: '', number: '', title: '', organization: '', group_company: '', type: '', management: '', department: '', section_field: '', branch: '', current_condition: '', proposed_condition: '', status: '', assigned_to: '', workflow_state: 'New', idea_class: '', hses_field: '', date: today(), priority: '', economy_class: '', product_relation: 0, repeated: 0, entity_scope: '', solution: '', cost: 0, auditor_comment: '', sb_order: '' }) +const defaultOrderForm = () => ({ name: '', number: '', title: '', organization: '', group_company: '', type: 'Buy L', management: '', department: '', section_field: '', branch: '', starting_date: today(), ending_date: in30Days(), assigned_to: '', actor: '', status: '', budget: 0, actual_cost: 0, currency: 'SAR', description: '', notes: '', cycle: '', stage: 1, completion_pct: 0, escalation_level: 0, workflow_state: 'Draft', planned: 0, priority: '', product_relation: 0, entity_scope: '', economy_class: '', hses_field: '', auditor_comment: '', sb_idea: '', cycle_type: 'Normal', long_cycle_date_1: '', long_cycle_date_2: '', long_cycle_date_3: '', long_cycle_date_4: '' }) +const defaultProcessForm = (): Process => ({ name: '', title: '', date_start: today(), date_end: in30Days(), number: '', status: '', cycle_type: 'Normal', organization: '', group_company: '', type: '', management: '', department: '', section_field: '', branch: '', progress_indicators: [], cycle: '', actor: '', escalation_level: 0, workflow_state: '', chain_location: '', economy_class: '', impact_level: '', product_relation: 0, entity_scope: '', hses_field: '', kpi_name: '', kpi_type: '', kpi_measure: '', cost_per_cycle: 0, auditor_comment: '' }) +const defaultOrgForm = () => ({ name: '', organization_name: '', group_company: '', type: 'Government', city: '', contact_email: '', creation_date: new Date().toISOString().slice(0,10), is_active: 1, acronym: '', incident_triage_window_days: 3, incident_resolution_window_days: 5 }) +const defaultMgmtForm = (): Management => ({ name: '', organization: '', group_company: '', name1: '', creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultDeptForm = (): Department => ({ name: '', custom_organization: '', group_company: '', department_name: '', custom_management: '', custom_creation_date: new Date().toISOString().slice(0,10), custom_status: 'Active' }) +const defaultBranchForm = (): Branch => ({ name: '', organization: '', group_company: '', branch: '', management: '', department: '', custom_creation_date: new Date().toISOString().slice(0,10), custom_status: 'Active' }) +const defaultIncidentTypeForm = (): IncidentType => ({ name: '', name1: '', creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultIdeaTypeForm = (): IdeaType => ({ name: '', name1: '', creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultOrderTypeForm = (): OrderType => ({ name: '', name1: '', creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultProcessTypeForm = (): ProcessType => ({ name: '', name1: '', creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultIncidentStatusForm = (): IncidentStatus => ({ name: '', name1: '', notify_manager: 0, creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultIdeaStatusForm = (): IdeaStatus => ({ name: '', name1: '', notify_manager: 0, creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultOrderStatusForm = (): OrderStatus => ({ name: '', name1: '', notify_manager: 0, creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultProcessStatusForm = (): ProcessStatus => ({ name: '', name1: '', notify_manager: 0, creation_date: new Date().toISOString().slice(0,10), status: 'Active' }) +const defaultUserForm = (): SBUser => ({ name: '', first_name: '', full_name: '', email: '', enabled: 1, creation: '', username: '', mobile_no: '', custom_organization: '', custom_group_company: '', custom_manager: '', custom_management: '', custom_department: '', custom_branch: '', custom_number: '', custom_category: '' }) +const defaultCycleForm = (): SBCycle => ({ name: '', name1: '', organization: '', group_company: '', frequency: 'Monthly', status: 'Active', start_date: today(), end_date: in30Days(), update_window_days: 2 }) +const defaultKpiForm = (): SBKpiInput => ({ name: '', organization: '', group_company: '', period: '', revenue_growth: 0, net_profit: 0, cash_flow: 0, strategic_goal_pct: 0, customer_satisfaction: 0, employee_turnover_pct: 0, open_safety_incidents: 0, compliance_score: 0 }) +const defaultDelegationForm = (): Delegation => ({ name: '', number: '', auditor: '', manager: '', organization: '', incident_initial_evaluation: 0, idea_initial_evaluation: 0, status: 'Pending', requested_on: '', remarks: '' }) + +const EMPLOYEE_EDITABLE_PAGES = new Set([ + 'incidents', 'incidents-form', + 'ideas', 'ideas-form', +]) + +const MANAGER_EDITABLE_PAGES = new Set([ + 'orders', 'orders-form', + 'incidents', 'incidents-form', + 'ideas', 'ideas-form', +]) + + +// Settings pages (and their form pages) are only in the sidebar for Admin/Auditor +// (see settingsNavForRole) — URL deep links must respect the same restriction. +const SETTINGS_PAGE_IDS = new Set(SETTINGS_NAV.flatMap(({ id }) => [id, `${id}-form`])) + +const DASHBOARD_ROLES = new Set(['Admin', 'Owner', 'Business Owner', 'Auditor', 'Manager', 'CEO', 'Chairman', 'XAuditor']) +const REPORTS_ROLES = new Set(['Admin', 'Auditor', 'Manager', 'CEO', 'Chairman', 'XAuditor']) +const DELEGATIONS_ROLES = new Set(['Admin', 'Auditor', 'Manager', 'CEO', 'Chairman']) + +function MainApp({ user, onLogout, darkMode, onToggleDark }: { user: User; onLogout: () => void; darkMode: boolean; onToggleDark: () => void }) { + const t = useT() + const role = user.role + const hasDashboard = DASHBOARD_ROLES.has(role) + const hasReports = REPORTS_ROLES.has(role) + const hasDelegations = DELEGATIONS_ROLES.has(role) + + const canModifyPage = (p: Page): boolean => { + if (role === 'CEO') return true + if (role === 'Business Owner' || role === 'Owner') { + if (p === 'organizations' || p === 'organizations-form') return true + if (p === 'departments' || p === 'departments-form') return true + if (p === 'users' || p === 'users-form') return true + return false + } + if (p === 'users' || p === 'users-form') return role === 'Admin' + if (role === 'Admin' || role === 'Auditor') return true + if (role === 'XAuditor') return false + if (role === 'Manager') { + if (MANAGER_EDITABLE_PAGES.has(p)) return true + return false + } + return EMPLOYEE_EDITABLE_PAGES.has(p) + } + + // Employee edit rights on Incident/Idea end at Submit — once triage has started the + // record is Auditor/Manager-owned, so a re-opened record must fall back to read-only. + const employeeLockedPastNew = (workflowState?: string): boolean => + role === 'Employee' && !!workflowState && workflowState !== 'New' + + const defaultPage: Page = hasDashboard ? 'dashboard' : 'inbox' + + // Pages reachable by typing a URL still have to respect the same role gating the + // sidebar applies — anything not allowed falls back to the role's default page. + const pageAllowed = (p: Page): boolean => { + if (role === 'CEO') return true + if (role === 'Business Owner' || role === 'Owner') { + if (p === 'dashboard') return true + if (p === 'organizations' || p === 'organizations-form') return true + if (p === 'departments' || p === 'departments-form') return true + if (p === 'users' || p === 'users-form') return true + if (p === 'profile') return true + // Form pages of main doctypes can be viewed (strictly read-only and comments allowed) + if (p === 'incidents-form' || p === 'ideas-form' || p === 'orders-form' || p === 'processes-form') return true + return false + } + if (p === 'dashboard') return hasDashboard + if (p === 'reports') return hasReports + if (p === 'delegations' || p === 'delegations-form') return hasDelegations + if (p === 'users' || p === 'users-form') return role === 'Admin' + if (SETTINGS_PAGE_IDS.has(p)) return role === 'Admin' || role === 'Auditor' + return true + } + + // Restore the page from the URL on first mount, so a refresh or a shared link + // (e.g. #/incidents) lands on that page instead of always resetting to the default. + const [page, setPage] = useState(() => { + const restored = freshLoadPage(parseRoute()) + return restored && pageAllowed(restored) ? restored : defaultPage + }) + + // Keep the URL hash in sync with the current page — one history entry per + // navigation, so refresh and back/forward work (same pattern as the HR app). + useEffect(() => { + const hash = buildHash(page) + if (window.location.hash !== hash) window.history.pushState(null, '', hash) + }, [page]) + + // Browser back/forward fires 'popstate' (not 'hashchange', since we navigate via + // pushState) — sync React state back from the URL. Form pages are allowed here + // because their in-memory form state still exists within the session. + useEffect(() => { + const onPopState = () => { + const p = parseRoute() + setPage(p && pageAllowed(p) ? p : defaultPage) + } + window.addEventListener('popstate', onPopState) + return () => window.removeEventListener('popstate', onPopState) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + // A user restricted to one Organization (User.custom_organization) always defaults + const myOrg = user.custom_organization || '' + const myGroupCo = user.custom_group_company || '' + const getOrgGroupCo = (orgName?: string) => organizations.find(o => o.name === orgName)?.group_company || '' + const [selectedOrg, setSelectedOrg] = useState('') + // One-shot status filter carried from a dashboard status-card click into the target + // list page's initial filter state — each list page reads it once on mount (same + // pattern as initialOrg/selectedOrg). Auto-clears on the next tick so it can't leak + // into a later, unrelated navigation to the same list page (e.g. via the sidebar). + const [dashStatusFilter, setDashStatusFilter] = useState('') + useEffect(() => { + if (!dashStatusFilter) return + const id = setTimeout(() => setDashStatusFilter(''), 0) + return () => clearTimeout(id) + }, [dashStatusFilter]) + const navigateToStatusList = (targetPage: Page, status: string) => { + setDashStatusFilter(status) + setPage(targetPage) + } + const [notifCount, setNotifCount] = useState(0) + const [incidents, setIncidents] = useState([]) + const [organizations, setOrganizations] = useState([]) + const [ideas, setIdeas] = useState([]) + const [orders, setOrders] = useState([]) + const [processes, setProcesses] = useState([]) + const [managements, setManagements] = useState([]) + const [departments, setDepartments] = useState([]) + const [branches, setBranches] = useState([]) + const [incidentTypes, setIncidentTypes] = useState([]) + const [incidentClasses, setIncidentClasses] = useState([]) + const [incidentFields, setIncidentFields] = useState([]) + const [ideaTypes, setIdeaTypes] = useState([]) + const [orderTypes, setOrderTypes] = useState([]) + const [processTypes, setProcessTypes] = useState([]) + const [incidentStatuses, setIncidentStatuses] = useState([]) + const [ideaStatuses, setIdeaStatuses] = useState([]) + const [orderStatuses, setOrderStatuses] = useState([]) + const [processStatuses, setProcessStatuses] = useState([]) + const [sbUsers, setSbUsers] = useState([]) + const [userRoleMap, setUserRoleMap] = useState>({}) + const [users, setUsers] = useState([]) + const [cycles, setCycles] = useState([]) + const [kpiInputs, setKpiInputs] = useState([]) + const [delegations, setDelegations] = useState([]) + const [delegatedFunctions, setDelegatedFunctions] = useState([]) + const [delegatedOrgs, setDelegatedOrgs] = useState>({}) + const [groupCompanies, setGroupCompanies] = useState([]) + const defaultGroupCoForm = (): SBGroupCompany => ({ name: '', company_name: '', acronym: '', code: '', status: 'Active', owner_user: '', notes: '' }) + const [groupCoForm, setGroupCoForm] = useState(defaultGroupCoForm()) + const [loading, setLoading] = useState(true) + const [formStatus, setFormStatus] = useState('idle') + const [isViewMode, setIsViewMode] = useState(false) + const [isSelfProfile, setIsSelfProfile] = useState(false) + const [profileUser, setProfileUser] = useState(null) + const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem('sb_sidebar') === '1') + const toggleSidebar = () => setSidebarCollapsed(c => { const n = !c; localStorage.setItem('sb_sidebar', n ? '1' : '0'); return n }) + const [exportOpen, setExportOpen] = useState(false) + const [selectedExportRows, setSelectedExportRows] = useState([]) + const [pageExportRows, setPageExportRows] = useState([]) + useEffect(() => { setExportOpen(false); setSelectedExportRows([]); setPageExportRows([]) }, [page]) + + const [incForm, setIncForm] = useState(defaultIncForm()) + const [ideaForm, setIdeaForm] = useState(defaultIdeaForm()) + const [orderForm, setOrderForm] = useState(defaultOrderForm()) + const [processForm, setProcessForm] = useState(defaultProcessForm()) + const [orgForm, setOrgForm] = useState(defaultOrgForm()) + const [mgmtForm, setMgmtForm] = useState(defaultMgmtForm()) + const [deptForm, setDeptForm] = useState(defaultDeptForm()) + const [branchForm, setBranchForm] = useState(defaultBranchForm()) + const [incidentTypeForm, setIncidentTypeForm] = useState(defaultIncidentTypeForm()) + const [ideaTypeForm, setIdeaTypeForm] = useState(defaultIdeaTypeForm()) + const [orderTypeForm, setOrderTypeForm] = useState(defaultOrderTypeForm()) + const [processTypeForm, setProcessTypeForm] = useState(defaultProcessTypeForm()) + const [incidentStatusForm, setIncidentStatusForm] = useState(defaultIncidentStatusForm()) + const [ideaStatusForm, setIdeaStatusForm] = useState(defaultIdeaStatusForm()) + const [orderStatusForm, setOrderStatusForm] = useState(defaultOrderStatusForm()) + const [processStatusForm, setProcessStatusForm] = useState(defaultProcessStatusForm()) + const [userForm, setUserForm] = useState(defaultUserForm()) + const [cycleForm, setCycleForm] = useState(defaultCycleForm()) + const [kpiForm, setKpiForm] = useState(defaultKpiForm()) + const [delegationForm, setDelegationForm] = useState(defaultDelegationForm()) + + const loadAll = async () => { + setLoading(true) + try { + const [inc, org, idea, ord, proc, usr, mgmt, dept, branch, incT, incCls, incFld, ideaT, ordT, procT, incS, ideaS, ordS, procS, sbU, rMap, cyc, kpi, dels, myFuncs, myOrgs, gCos] = await Promise.all([ + get(DOCTYPES.incidents), get(DOCTYPES.organizations), get(DOCTYPES.ideas), + get(DOCTYPES.orders), get(DOCTYPES.processes), getUsers(), + get(DOCTYPES.managements), get(DOCTYPES.departments), get(DOCTYPES.branches), + get(DOCTYPES.incidentTypes), get(DOCTYPES.incidentClass), get(DOCTYPES.incidentField), + get(DOCTYPES.ideaTypes), get(DOCTYPES.orderTypes), get(DOCTYPES.processTypes), + get(DOCTYPES.incidentStatuses), get(DOCTYPES.ideaStatuses), get(DOCTYPES.orderStatuses), get(DOCTYPES.processStatuses), + getSBUsers(), getSBUserRoleMap(), + get(DOCTYPES.cycles), get(DOCTYPES.kpiInput), + getDelegations(), getMyDelegatedFunctions(), getMyDelegatedOrganizations(), + getGroupCompanies(), + ]) + setIncidents(inc || []) + setOrganizations(org || []) + setIdeas(idea || []) + setOrders(ord || []) + setProcesses(proc || []) + setUsers(usr || []) + setManagements(mgmt || []) + setDepartments(dept || []) + setBranches(branch || []) + setIncidentTypes(incT || []) + setIncidentClasses(incCls || []) + setIncidentFields(incFld || []) + setIdeaTypes(ideaT || []) + setOrderTypes(ordT || []) + setProcessTypes(procT || []) + setIncidentStatuses(incS || []) + setIdeaStatuses(ideaS || []) + setOrderStatuses(ordS || []) + setProcessStatuses(procS || []) + setSbUsers(sbU || []) + setUserRoleMap(rMap || {}) + setCycles(cyc || []) + setKpiInputs(kpi || []) + setDelegations(dels || []) + setDelegatedFunctions(myFuncs || []) + setDelegatedOrgs(myOrgs || {}) + setGroupCompanies(gCos || []) + } catch (e) { + console.error('Error loading data:', e) + } + setLoading(false) + } + + const fetchNotifCount = async () => { + try { + const res = await fetch('/api/method/sidrah_business.api.get_notification_count', { + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (res.ok) { + const json = await res.json() + setNotifCount(Number(json.message) || 0) + } + } catch { /* non-critical */ } + } + + useEffect(() => { + loadAll() + fetchNotifCount() + // Keep the bell badge current — new notifications arrive from other users and + // the nightly engine, not only from this session's own actions. + const notifPoll = setInterval(fetchNotifCount, 60000) + return () => clearInterval(notifPoll) + }, [user.name]) + + const [deleteModalState, setDeleteModalState] = useState<{ + isOpen: boolean + doctype: string + name: string + title?: string + isDeleting: boolean + }>({ + isOpen: false, + doctype: '', + name: '', + title: '', + isDeleting: false + }) + + const deleteRecord = (doctype: string, name: string, title?: string) => { + setDeleteModalState({ + isOpen: true, + doctype, + name, + title: title || name, + isDeleting: false, + }) + } + + const navigateBackToList = (doctype: string) => { + const map: Record = { + [DOCTYPES.incidents]: 'incidents', + [DOCTYPES.ideas]: 'ideas', + [DOCTYPES.orders]: 'orders', + [DOCTYPES.processes]: 'processes', + [DOCTYPES.organizations]: 'organizations', + [DOCTYPES.managements]: 'managements', + [DOCTYPES.departments]: 'departments', + [DOCTYPES.branches]: 'branches', + [DOCTYPES.cycles]: 'cycles', + [DOCTYPES.kpiInput]: 'kpi-input', + [DOCTYPES.incidentTypes]: 'incident-types', + [DOCTYPES.ideaTypes]: 'idea-types', + [DOCTYPES.orderTypes]: 'order-types', + [DOCTYPES.processTypes]: 'process-types', + [DOCTYPES.incidentStatuses]: 'incident-statuses', + [DOCTYPES.ideaStatuses]: 'idea-statuses', + [DOCTYPES.orderStatuses]: 'order-statuses', + [DOCTYPES.processStatuses]: 'process-statuses', + [DOCTYPES.groupCompanies]: 'group-companies', + } + const target = map[doctype] + if (target) setPage(target) + } + + const handleConfirmSoftDelete = async (reason: string) => { + setDeleteModalState(prev => ({ ...prev, isDeleting: true })) + try { + const dt = deleteModalState.doctype + await deleteResource(deleteModalState.doctype, deleteModalState.name, reason, false) + setDeleteModalState(prev => ({ ...prev, isOpen: false, isDeleting: false })) + alert(t.rtl ? 'تم نقل السجل إلى المحذوفات بنجاح!' : 'Record moved to Deleted successfully!') + await loadAll() + if (page.endsWith('-form')) { + navigateBackToList(dt) + } + } catch (e: any) { + console.error('Delete error:', e) + alert(e?.message || 'Delete failed. Please try again.') + setDeleteModalState(prev => ({ ...prev, isDeleting: false })) + } + } + + const handleConfirmPermanentDelete = async (reason: string) => { + setDeleteModalState(prev => ({ ...prev, isDeleting: true })) + try { + const dt = deleteModalState.doctype + await deleteResource(deleteModalState.doctype, deleteModalState.name, reason, true) + setDeleteModalState(prev => ({ ...prev, isOpen: false, isDeleting: false })) + alert(t.rtl ? 'تم حذف السجل نهائياً بنجاح!' : 'Record permanently deleted successfully!') + await loadAll() + if (page.endsWith('-form')) { + navigateBackToList(dt) + } + } catch (e: any) { + console.error('Permanent delete error:', e) + alert(e?.message || 'Permanent delete failed. Please try again.') + setDeleteModalState(prev => ({ ...prev, isDeleting: false })) + } + } + + const restoreRecord = async (doctype: string, name: string) => { + if (!window.confirm(t.rtl ? 'هل أنت متأكد من استعادة هذا السجل؟' : 'Are you sure you want to restore this record?')) return + setLoading(true) + try { + await restoreResource(doctype, name) + alert(t.restoredOk || 'Record restored successfully!') + await loadAll() + } catch (e: any) { + console.error('Restore error:', e) + alert(e?.message || 'Restore failed. Please try again.') + } + setLoading(false) + } + + const disableUser = async (name: string) => { + try { + await disableUserApi(name) + await loadAll() + } catch (e) { console.error('Disable user error:', e) } + } + + const viewIncident = async (incident: Incident) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.incidents, incident.name) + setIncForm({ ...defaultIncForm(), ...doc }) + } catch { + setIncForm({ ...defaultIncForm(), ...incident }) + } + setPage('incidents-form') + setLoading(false) + } + + const viewIdea = async (idea: Idea) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.ideas, idea.name) + setIdeaForm({ ...defaultIdeaForm(), ...doc }) + } catch { + setIdeaForm({ ...defaultIdeaForm(), ...idea }) + } + setPage('ideas-form') + setLoading(false) + } + + const viewOrder = async (order: Order) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.orders, order.name) + setOrderForm({ ...defaultOrderForm(), ...doc }) + } catch { + setOrderForm({ ...defaultOrderForm(), ...order }) + } + setPage('orders-form') + setLoading(false) + } + + const viewProcess = async (process: Process) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await fetchProcess(process.name) + setProcessForm({ ...(doc as any), progress_indicators: doc.progress_indicators || [], cycle_logs: (doc as any).cycle_logs || [] }) + setPage('processes-form') + } catch { + setProcessForm({ ...process, progress_indicators: process.progress_indicators || [] }) + setPage('processes-form') + } + setLoading(false) + } + + const viewOrganization = (organization: Organization) => + openRecord(DOCTYPES.organizations, organization, setOrgForm, defaultOrgForm(), 'organizations-form', true) + + const viewGroupCompany = (gc: SBGroupCompany) => { + setIsViewMode(true) + setGroupCoForm(gc) + setPage('group-companies-form') + } + const editGroupCompany = (gc: SBGroupCompany) => { + setIsViewMode(false) + setGroupCoForm(gc) + setPage('group-companies-form') + } + + const viewManagement = async (m: Management) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.managements, m.name) + setMgmtForm({ ...defaultMgmtForm(), ...doc }) + } catch { + setMgmtForm({ ...defaultMgmtForm(), ...m }) + } + setPage('managements-form') + setLoading(false) + } + + const viewDepartment = async (d: Department) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.departments, d.name) + setDeptForm({ ...defaultDeptForm(), ...doc }) + } catch { + setDeptForm({ ...defaultDeptForm(), ...d }) + } + setPage('departments-form') + setLoading(false) + } + + const viewBranch = async (b: Branch) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.branches, b.name) + setBranchForm({ ...defaultBranchForm(), ...doc }) + } catch { + setBranchForm({ ...defaultBranchForm(), ...b }) + } + setPage('branches-form') + setLoading(false) + } + + const viewCycle = async (c: SBCycle) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.cycles, c.name) + setCycleForm({ ...defaultCycleForm(), ...doc }) + } catch { + setCycleForm({ ...defaultCycleForm(), ...c }) + } + setPage('cycles-form') + setLoading(false) + } + + const viewKpiInput = async (k: SBKpiInput) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getResource(DOCTYPES.kpiInput, k.name) + setKpiForm({ ...defaultKpiForm(), ...doc }) + } catch { + setKpiForm({ ...defaultKpiForm(), ...k }) + } + setPage('kpi-input-form') + setLoading(false) + } + + // `onCreated`, when given, keeps the user on the form after a first-time create (populating + // it with the saved doc) instead of bouncing back to the list — so the workflow panel (e.g. + // "Submit") is immediately usable without having to reopen the record. + const submitForm = async (doctype: string, data: any, _reset: () => void, _backPage: Page, onCreated?: (doc: any) => void) => { + setFormStatus('loading') + try { + if (data.name) { + await updateDoc(doctype, data.name, data) + setFormStatus('success') + loadAll() + setTimeout(() => setFormStatus('idle'), 1500) + } else { + const created = await post(doctype, data) + const savedDoc = created?.data ?? created + if (onCreated) { + onCreated(savedDoc) + } + setFormStatus('success') + loadAll() + setTimeout(() => setFormStatus('idle'), 1500) + } + } catch (e) { + console.error('Submit error:', e) + setFormStatus(e instanceof Error ? e.message : 'Error saving. Please try again.') + } + } + + const handleLogout = async () => { + await logoutApi() + onLogout() + } + + const openRecord = async ( + doctype: string, record: T, + setForm: (v: any) => void, defaultForm: any, + targetPage: Page, viewMode: boolean + ) => { + setIsViewMode(viewMode) + setLoading(true) + try { + const doc = await getResource(doctype, record.name) + setForm({ ...defaultForm, ...doc }) + } catch { + setForm({ ...defaultForm, ...record }) + } + setPage(targetPage) + setLoading(false) + } + + const editIncident = (i: Incident) => openRecord(DOCTYPES.incidents, i, setIncForm, defaultIncForm(), 'incidents-form', false) + const editIdea = (i: Idea) => openRecord(DOCTYPES.ideas, i, setIdeaForm, defaultIdeaForm(), 'ideas-form', false) + const editOrder = (o: Order) => openRecord(DOCTYPES.orders, o, setOrderForm, defaultOrderForm(), 'orders-form', false) + const editProcess = (p: Process) => { + setIsViewMode(false) + setLoading(true) + fetchProcess(p.name) + .then(doc => { setProcessForm({ ...(doc as any), progress_indicators: doc.progress_indicators || [], cycle_logs: (doc as any).cycle_logs || [] }) }) + .catch(() => { setProcessForm({ ...p, progress_indicators: p.progress_indicators || [] }) }) + .finally(() => { setPage('processes-form'); setLoading(false) }) + } + const editOrganization = (org: Organization) => + openRecord(DOCTYPES.organizations, org, setOrgForm, defaultOrgForm(), 'organizations-form', false) + const editManagement = (m: Management) => openRecord(DOCTYPES.managements, m, setMgmtForm, defaultMgmtForm(), 'managements-form', false) + const editDepartment = (d: Department) => openRecord(DOCTYPES.departments, d, setDeptForm, defaultDeptForm(), 'departments-form', false) + const editBranch = (b: Branch) => openRecord(DOCTYPES.branches, b, setBranchForm, defaultBranchForm(), 'branches-form', false) + const editCycle = (c: SBCycle) => openRecord(DOCTYPES.cycles, c, setCycleForm, defaultCycleForm(), 'cycles-form', false) + const editKpiInput = (k: SBKpiInput) => openRecord(DOCTYPES.kpiInput, k, setKpiForm, defaultKpiForm(), 'kpi-input-form', false) + + const viewDelegation = async (d: Delegation) => { + setIsViewMode(true) + setLoading(true) + try { + const doc = await getDelegation(d.name) + setDelegationForm({ ...defaultDelegationForm(), ...doc }) + } catch { + setDelegationForm({ ...defaultDelegationForm(), ...d }) + } + setPage('delegations-form') + setLoading(false) + } + + const submitDelegationForm = async () => { + setFormStatus('loading') + try { + const functions: string[] = [] + if (delegationForm.incident_initial_evaluation) functions.push('incident_initial_evaluation') + if (delegationForm.idea_initial_evaluation) functions.push('idea_initial_evaluation') + await createDelegation( + delegationForm.manager, functions, delegationForm.organization, delegationForm.remarks || '', + delegationForm.incident_name || '', delegationForm.idea_name || '' + ) + setDelegationForm(defaultDelegationForm()) + setFormStatus('success') + loadAll() + setTimeout(() => { setFormStatus('idle'); setPage('delegations') }, 1500) + } catch (e) { + console.error('Delegation submit error:', e) + setFormStatus(e instanceof Error ? e.message : 'Error saving. Please try again.') + } + } + + const revokeDelegationHandler = async (name: string) => { + if (!window.confirm(t.rtl ? 'هل أنت متأكد من إلغاء هذا التفويض؟' : 'Are you sure you want to revoke this delegation?')) return + setLoading(true) + try { + await revokeDelegation(name) + await loadAll() + } catch (e) { + console.error('Revoke delegation error:', e) + alert(e instanceof Error ? e.message : 'Revoke failed. Please try again.') + } + setLoading(false) + } + + const viewIncidentType = (t: IncidentType) => openRecord(DOCTYPES.incidentTypes, t, setIncidentTypeForm, defaultIncidentTypeForm(), 'incident-types-form', true) + const editIncidentType = (t: IncidentType) => openRecord(DOCTYPES.incidentTypes, t, setIncidentTypeForm, defaultIncidentTypeForm(), 'incident-types-form', false) + const viewIdeaType = (t: IdeaType) => openRecord(DOCTYPES.ideaTypes, t, setIdeaTypeForm, defaultIdeaTypeForm(), 'idea-types-form', true) + const editIdeaType = (t: IdeaType) => openRecord(DOCTYPES.ideaTypes, t, setIdeaTypeForm, defaultIdeaTypeForm(), 'idea-types-form', false) + const viewOrderType = (t: OrderType) => openRecord(DOCTYPES.orderTypes, t, setOrderTypeForm, defaultOrderTypeForm(), 'order-types-form', true) + const editOrderType = (t: OrderType) => openRecord(DOCTYPES.orderTypes, t, setOrderTypeForm, defaultOrderTypeForm(), 'order-types-form', false) + const viewProcessType = (t: ProcessType) => openRecord(DOCTYPES.processTypes, t, setProcessTypeForm, defaultProcessTypeForm(), 'process-types-form', true) + const editProcessType = (t: ProcessType) => openRecord(DOCTYPES.processTypes, t, setProcessTypeForm, defaultProcessTypeForm(), 'process-types-form', false) + const viewIncidentStatus = (t: IncidentStatus) => openRecord(DOCTYPES.incidentStatuses, t, setIncidentStatusForm, defaultIncidentStatusForm(), 'incident-statuses-form', true) + const editIncidentStatus = (t: IncidentStatus) => openRecord(DOCTYPES.incidentStatuses, t, setIncidentStatusForm, defaultIncidentStatusForm(), 'incident-statuses-form', false) + const viewIdeaStatus = (t: IdeaStatus) => openRecord(DOCTYPES.ideaStatuses, t, setIdeaStatusForm, defaultIdeaStatusForm(), 'idea-statuses-form', true) + const editIdeaStatus = (t: IdeaStatus) => openRecord(DOCTYPES.ideaStatuses, t, setIdeaStatusForm, defaultIdeaStatusForm(), 'idea-statuses-form', false) + const viewOrderStatus = (t: OrderStatus) => openRecord(DOCTYPES.orderStatuses, t, setOrderStatusForm, defaultOrderStatusForm(), 'order-statuses-form', true) + const editOrderStatus = (t: OrderStatus) => openRecord(DOCTYPES.orderStatuses, t, setOrderStatusForm, defaultOrderStatusForm(), 'order-statuses-form', false) + const viewProcessStatus = (t: ProcessStatus) => openRecord(DOCTYPES.processStatuses, t, setProcessStatusForm, defaultProcessStatusForm(), 'process-statuses-form', true) + const editProcessStatus = (t: ProcessStatus) => openRecord(DOCTYPES.processStatuses, t, setProcessStatusForm, defaultProcessStatusForm(), 'process-statuses-form', false) + + const viewUser = async (u: any) => { + setProfileUser(u) + setPage('profile') + } + const editUser = (u: any) => { + setIsViewMode(false) + setIsSelfProfile(false) + setUserForm({ ...defaultUserForm(), ...u }) + setPage('users-form') + } + + const submitUserForm = async () => { + setFormStatus('loading') + try { + const isNew = !userForm.name + if (isNew) { + const { new_password, ...postData } = userForm + const created = await post('User', { ...postData, send_welcome_email: 0, user_type: 'System User' }) + const createdData = created?.data ?? created + const createdName = createdData?.name || userForm.email + if (new_password && createdName) { + try { await setUserPassword(createdName, new_password) } catch (err) { console.error('Password set error:', err) } + } + if (userForm.custom_category === 'Admin' && userForm.custom_group_company) { + const gc = groupCompanies.find(g => g.name === userForm.custom_group_company) + if (gc && !gc.owner_user) { + try { await updateDoc('SB Group Company', gc.name, { owner_user: createdName }) } catch (err) { console.error('GC owner sync error:', err) } + } + } + setUserForm({ ...defaultUserForm(), ...userForm, ...(created?.data ?? created) }) + setIsViewMode(true) + setFormStatus('success') + loadAll() + setTimeout(() => setFormStatus('idle'), 1500) + } else { + const { name, email, creation, full_name, new_password, ...updateData } = userForm + await updateDoc('User', name, updateData) + if (new_password) { + try { await setUserPassword(name, new_password) } catch (err) { console.error('Password set error:', err) } + } + if (userForm.custom_category === 'Admin' && userForm.custom_group_company) { + const gc = groupCompanies.find(g => g.name === userForm.custom_group_company) + if (gc && !gc.owner_user) { + try { await updateDoc('SB Group Company', gc.name, { owner_user: name }) } catch (err) { console.error('GC owner sync error:', err) } + } + } + setFormStatus('success') + loadAll() + setTimeout(() => setFormStatus('idle'), 1500) + } + } catch (e) { + console.error('User submit error:', e) + setFormStatus(e instanceof Error ? e.message : 'Error saving. Please try again.') + } + } + + const viewOwnProfile = () => { + const me = sbUsers.find(u => u.name === user.name || u.username === user.name) + setProfileUser(me || { name: user.name, full_name: user.full_name, email: user.name, enabled: 1 }) + setPage('profile') + } + + const navigateToEntity = (targetPage: Page, name?: string) => { + if (name) { + const entityDoctype: Record = { + 'processes-form': DOCTYPES.processes, + 'orders-form': DOCTYPES.orders, + 'incidents-form': DOCTYPES.incidents, + 'ideas-form': DOCTYPES.ideas, + } + const doctype = entityDoctype[targetPage] + if (doctype) { + setIsViewMode(true) + setLoading(true) + const loadDoc = targetPage === 'processes-form' + ? fetchProcess(name).then(doc => ({ ...(doc as any), progress_indicators: doc.progress_indicators || [], cycle_logs: (doc as any).cycle_logs || [] })) + : getResource(doctype, name) + loadDoc.then(doc => { + if (targetPage === 'processes-form') setProcessForm(doc as any) + else if (targetPage === 'orders-form') setOrderForm({ ...defaultOrderForm(), ...doc }) + else if (targetPage === 'incidents-form') setIncForm({ ...defaultIncForm(), ...doc }) + else if (targetPage === 'ideas-form') setIdeaForm({ ...defaultIdeaForm(), ...doc }) + setPage(targetPage) + setLoading(false) + }).catch(() => { setPage(targetPage); setLoading(false) }) + return + } + } + setPage(targetPage) + } + + // ── Form page routing ───────────────────────────────────────────────────── + + const renderWithModal = (content: React.ReactNode) => ( + <> + {content} + setDeleteModalState(prev => ({ ...prev, isOpen: false }))} + onConfirmSoftDelete={handleConfirmSoftDelete} + onConfirmPermanentDelete={handleConfirmPermanentDelete} + /> + + ) + + if (page === 'incidents-form') return renderWithModal( + setIsViewMode(false) : undefined} + onDelete={canModifyPage('incidents-form') ? (name: string) => deleteRecord(DOCTYPES.incidents, name, incForm.title || incForm.number) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.incidents, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.incidents, name, incForm.title || incForm.number) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + userRole={role} + delegatedFunctions={delegatedFunctions} + delegatedOrgs={delegatedOrgs} + onSubmit={() => submitForm(DOCTYPES.incidents, incForm, () => setIncForm(defaultIncForm()), 'incidents', + created => setIncForm({ ...defaultIncForm(), ...created }))} + onBack={() => { setPage('incidents'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + onRefresh={async () => { + if (incForm.name) { + const doc = await getResource(DOCTYPES.incidents, incForm.name).catch(() => null) + if (doc) setIncForm({ ...defaultIncForm(), ...doc }) + } + get(DOCTYPES.incidents).then(inc => setIncidents(inc || [])).catch(() => {}) + }} + /> + ) + + if (page === 'ideas-form') return renderWithModal( + setIsViewMode(false) : undefined} + onDelete={canModifyPage('ideas-form') ? (name: string) => deleteRecord(DOCTYPES.ideas, name, ideaForm.title || ideaForm.number) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.ideas, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.ideas, name, ideaForm.title || ideaForm.number) : undefined} + onNavigateToOrder={(orderName: string) => openRecord(DOCTYPES.orders, { name: orderName } as any, setOrderForm, defaultOrderForm(), 'orders-form', true)} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + userRole={role as 'Manager' | 'Auditor' | 'Employee'} + delegatedFunctions={delegatedFunctions} + delegatedOrgs={delegatedOrgs} + onSubmit={() => submitForm(DOCTYPES.ideas, ideaForm, () => setIdeaForm(defaultIdeaForm()), 'ideas', + created => setIdeaForm({ ...defaultIdeaForm(), ...created }))} + onBack={() => { setPage('ideas'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + onRefresh={async () => { + if (ideaForm.name) { + const doc = await getResource(DOCTYPES.ideas, ideaForm.name).catch(() => null) + if (doc) setIdeaForm({ ...defaultIdeaForm(), ...doc }) + } + get(DOCTYPES.ideas).then(idea => setIdeas(idea || [])).catch(() => {}) + }} + /> + ) + + if (page === 'orders-form') return renderWithModal( + setIsViewMode(false) : undefined} + onDelete={canModifyPage('orders-form') ? (name: string) => deleteRecord(DOCTYPES.orders, name, orderForm.title || orderForm.number) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.orders, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.orders, name, orderForm.title || orderForm.number) : undefined} + onNavigateToIdea={(ideaName: string) => openRecord(DOCTYPES.ideas, { name: ideaName } as any, setIdeaForm, defaultIdeaForm(), 'ideas-form', true)} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + currentUser={user.name} + userRole={role} + onSubmit={() => submitForm(DOCTYPES.orders, orderForm, () => setOrderForm(defaultOrderForm()), 'orders', + created => setOrderForm({ ...defaultOrderForm(), ...created }))} + onBack={() => { setPage('orders'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + onRefresh={async () => { + if (orderForm.name) { + const doc = await getResource(DOCTYPES.orders, orderForm.name).catch(() => null) + if (doc) setOrderForm({ ...defaultOrderForm(), ...doc }) + } + get(DOCTYPES.orders).then(ord => setOrders(ord || [])).catch(() => {}) + }} + /> + ) + + if (page === 'processes-form') return renderWithModal( + setIsViewMode(false) : undefined} + onDelete={canModifyPage('processes-form') ? (name: string) => deleteRecord(DOCTYPES.processes, name, processForm.title || processForm.number) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.processes, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.processes, name, processForm.title || processForm.number) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + userRole={user.role} + onRefreshProcess={async () => { + if (processForm.name) { + try { + const doc = await fetchProcess(processForm.name) + setProcessForm({ ...(doc as any), progress_indicators: doc.progress_indicators || [], cycle_logs: (doc as any).cycle_logs || [] }) + } catch { /* keep stale */ } + } + get(DOCTYPES.processes).then(proc => setProcesses(proc || [])).catch(() => {}) + }} + onSubmit={() => submitForm(DOCTYPES.processes, processForm, () => setProcessForm(defaultProcessForm()), 'processes', + created => setProcessForm({ ...defaultProcessForm(), ...(created as any), progress_indicators: (created as any).progress_indicators || [], cycle_logs: (created as any).cycle_logs || [] }))} + onBack={() => { setPage('processes'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'organizations-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.organizations, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.organizations, name, orgForm.organization_name) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.organizations, orgForm, () => setOrgForm(defaultOrgForm()), 'organizations', + created => { setOrgForm({ ...defaultOrgForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('organizations'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'managements-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.managements, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.managements, name, mgmtForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.managements, mgmtForm, () => setMgmtForm(defaultMgmtForm()), 'managements', + created => { setMgmtForm({ ...defaultMgmtForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('managements'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'departments-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.departments, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.departments, name, deptForm.department_name) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.departments, deptForm, () => setDeptForm(defaultDeptForm()), 'departments', + created => { setDeptForm({ ...defaultDeptForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('departments'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'branches-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.branches, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.branches, name, branchForm.branch) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.branches, branchForm, () => setBranchForm(defaultBranchForm()), 'branches', + created => { setBranchForm({ ...defaultBranchForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('branches'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'cycles-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.cycles, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.cycles, name, cycleForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.cycles, cycleForm, () => setCycleForm(defaultCycleForm()), 'cycles', + created => { setCycleForm({ ...defaultCycleForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('cycles'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'kpi-input-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.kpiInput, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.kpiInput, name, kpiForm.period) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.kpiInput, kpiForm, () => setKpiForm(defaultKpiForm()), 'kpi-input', + created => { setKpiForm({ ...defaultKpiForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('kpi-input'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'incident-types-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.incidentTypes, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.incidentTypes, name, incidentTypeForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.incidentTypes, incidentTypeForm, () => setIncidentTypeForm(defaultIncidentTypeForm()), 'incident-types', + created => { setIncidentTypeForm({ ...defaultIncidentTypeForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('incident-types'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'idea-types-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.ideaTypes, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.ideaTypes, name, ideaTypeForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.ideaTypes, ideaTypeForm, () => setIdeaTypeForm(defaultIdeaTypeForm()), 'idea-types', + created => { setIdeaTypeForm({ ...defaultIdeaTypeForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('idea-types'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'order-types-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.orderTypes, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.orderTypes, name, orderTypeForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.orderTypes, orderTypeForm, () => setOrderTypeForm(defaultOrderTypeForm()), 'order-types', + created => { setOrderTypeForm({ ...defaultOrderTypeForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('order-types'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'process-types-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.processTypes, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.processTypes, name, processTypeForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.processTypes, processTypeForm, () => setProcessTypeForm(defaultProcessTypeForm()), 'process-types', + created => { setProcessTypeForm({ ...defaultProcessTypeForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('process-types'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'incident-statuses-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.incidentStatuses, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.incidentStatuses, name, incidentStatusForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.incidentStatuses, incidentStatusForm, () => setIncidentStatusForm(defaultIncidentStatusForm()), 'incident-statuses', + created => { setIncidentStatusForm({ ...defaultIncidentStatusForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('incident-statuses'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'idea-statuses-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.ideaStatuses, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.ideaStatuses, name, ideaStatusForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.ideaStatuses, ideaStatusForm, () => setIdeaStatusForm(defaultIdeaStatusForm()), 'idea-statuses', + created => { setIdeaStatusForm({ ...defaultIdeaStatusForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('idea-statuses'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'order-statuses-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.orderStatuses, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.orderStatuses, name, orderStatusForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.orderStatuses, orderStatusForm, () => setOrderStatusForm(defaultOrderStatusForm()), 'order-statuses', + created => { setOrderStatusForm({ ...defaultOrderStatusForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('order-statuses'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'process-statuses-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.processStatuses, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.processStatuses, name, processStatusForm.name1) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.processStatuses, processStatusForm, () => setProcessStatusForm(defaultProcessStatusForm()), 'process-statuses', + created => { setProcessStatusForm({ ...defaultProcessStatusForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('process-statuses'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'users-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' || role === 'CEO' || role === 'Business Owner' ? (name: string) => restoreRecord(DOCTYPES.users, name) : undefined} + onPermanentDelete={role === 'Admin' || role === 'CEO' || role === 'Business Owner' ? (name: string) => deleteRecord(DOCTYPES.users, name, userForm.full_name || userForm.email) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + isSelfProfile={isSelfProfile} + onSubmit={submitUserForm} + onBack={() => { setPage(isSelfProfile ? (hasDashboard ? 'dashboard' : 'inbox') : 'users'); setFormStatus('idle') }} + onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + if (page === 'delegations-form') { + const isClosedDelegation = delegationForm.status === 'Revoked' || delegationForm.status === 'Accepted' || delegationForm.status === 'Rejected' + return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.delegations, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.delegations, name, delegationForm.name) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={submitDelegationForm} + onBack={() => { setPage('delegations'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + } + + if (page === 'group-companies-form') return renderWithModal( + setIsViewMode(false) : undefined} + onRestore={role === 'Admin' ? (name: string) => restoreRecord(DOCTYPES.groupCompanies, name) : undefined} + onPermanentDelete={role === 'Admin' ? (name: string) => deleteRecord(DOCTYPES.groupCompanies, name, groupCoForm.company_name) : undefined} + darkMode={darkMode} notifCount={notifCount} onToggleDark={onToggleDark} onCountChange={setNotifCount} onViewProfile={viewOwnProfile} + onSubmit={() => submitForm(DOCTYPES.groupCompanies, groupCoForm, () => setGroupCoForm(defaultGroupCoForm()), 'group-companies', + created => { setGroupCoForm({ ...defaultGroupCoForm(), ...created }); setIsViewMode(true) })} + onBack={() => { setPage('group-companies'); setFormStatus('idle') }} onNavigate={setPage} onLogout={handleLogout} user={user} + /> + ) + + const pageTitle: Record = { + ...t.pages as Record, + 'group-companies': 'Group Companies', + 'group-companies-form': 'Group Company Form', + } + + const newButtonMap: Record void }> = { + 'group-companies': { color: colors.purple, formPage: 'group-companies-form', reset: () => setGroupCoForm(defaultGroupCoForm()) }, + incidents: { color: colors.accent, formPage: 'incidents-form', reset: () => { + const org = myOrg || selectedOrg + setIncForm({ ...defaultIncForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + ideas: { color: colors.teal, formPage: 'ideas-form', reset: () => { + const org = myOrg || selectedOrg + setIdeaForm({ ...defaultIdeaForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + orders: { color: colors.purple, formPage: 'orders-form', reset: () => { + const org = myOrg || selectedOrg + setOrderForm({ ...defaultOrderForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + processes: { color: colors.blue, formPage: 'processes-form', reset: () => { + const restrictedOrgs = delegatedOrgs.create_processes + const defaultOrg = myOrg || ((role === 'Manager' && restrictedOrgs && restrictedOrgs.length === 1) ? restrictedOrgs[0] : selectedOrg) + setProcessForm({ ...defaultProcessForm(), organization: defaultOrg, group_company: myGroupCo || getOrgGroupCo(defaultOrg) }) + } }, + organizations: { color: colors.accent, formPage: 'organizations-form', reset: () => setOrgForm({ ...defaultOrgForm(), group_company: myGroupCo }) }, + managements: { color: colors.blue, formPage: 'managements-form', reset: () => { + const org = myOrg || selectedOrg + setMgmtForm({ ...defaultMgmtForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + departments: { color: colors.teal, formPage: 'departments-form', reset: () => { + const org = myOrg || selectedOrg + setDeptForm({ ...defaultDeptForm(), custom_organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + branches: { color: colors.purple, formPage: 'branches-form', reset: () => { + const org = myOrg || selectedOrg + setBranchForm({ ...defaultBranchForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + cycles: { color: colors.teal, formPage: 'cycles-form', reset: () => { + const org = myOrg || selectedOrg + setCycleForm({ ...defaultCycleForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + 'kpi-input': { color: colors.blue, formPage: 'kpi-input-form', reset: () => { + const org = myOrg || selectedOrg + setKpiForm({ ...defaultKpiForm(), organization: org, group_company: myGroupCo || getOrgGroupCo(org) }) + } }, + 'incident-types': { color: colors.red, formPage: 'incident-types-form', reset: () => setIncidentTypeForm(defaultIncidentTypeForm()) }, + 'idea-types': { color: colors.orange, formPage: 'idea-types-form', reset: () => setIdeaTypeForm(defaultIdeaTypeForm()) }, + 'order-types': { color: colors.green, formPage: 'order-types-form', reset: () => setOrderTypeForm(defaultOrderTypeForm()) }, + 'process-types': { color: colors.accent, formPage: 'process-types-form', reset: () => setProcessTypeForm(defaultProcessTypeForm()) }, + 'incident-statuses': { color: colors.red, formPage: 'incident-statuses-form', reset: () => setIncidentStatusForm(defaultIncidentStatusForm()) }, + 'idea-statuses': { color: colors.teal, formPage: 'idea-statuses-form', reset: () => setIdeaStatusForm(defaultIdeaStatusForm()) }, + 'order-statuses': { color: colors.purple, formPage: 'order-statuses-form', reset: () => setOrderStatusForm(defaultOrderStatusForm()) }, + 'process-statuses': { color: colors.blue, formPage: 'process-statuses-form', reset: () => setProcessStatusForm(defaultProcessStatusForm()) }, + users: { color: colors.blue, formPage: 'users-form', reset: () => { setUserForm({ ...defaultUserForm(), custom_group_company: myGroupCo }); setIsSelfProfile(false) } }, + delegations: { color: colors.purple, formPage: 'delegations-form', reset: () => setDelegationForm(defaultDelegationForm()) }, + } + + // Doctype-specific "+ New X" label per page, reusing the existing translated form titles + // where they exist so this stays in sync with Arabic/English toggling; the 8 Type/Status + // lookup pages don't have a translated form title so they fall back to a plain English one. + const newItemLabelMap: Record = { + 'group-companies': 'New Group Company', + incidents: t.formTitle.newIncident, ideas: t.formTitle.newIdea, orders: t.formTitle.newOrder, + processes: t.formTitle.newProcess, organizations: t.formTitle.newOrg, managements: t.formTitle.newMgmt, + departments: t.formTitle.newDept, branches: t.formTitle.newBranch, users: t.formTitle.newUser, + cycles: t.formTitle.newCycle, 'kpi-input': t.formTitle.newKpi, delegations: t.formTitle.newDelegation, + 'incident-types': 'New Incident Type', 'idea-types': 'New Idea Type', 'order-types': 'New Order Type', 'process-types': 'New Process Type', + 'incident-statuses': 'New Incident Status', 'idea-statuses': 'New Idea Status', 'order-statuses': 'New Order Status', 'process-statuses': 'New Process Status', + } + + // Raw (unfiltered) data array backing each exportable list page — feeds the Export modal. + const exportRowsMap: Record = { + incidents, ideas, orders, processes, organizations, managements, departments, branches, cycles, + 'kpi-input': kpiInputs, 'incident-types': incidentTypes, 'idea-types': ideaTypes, 'order-types': orderTypes, 'process-types': processTypes, + 'incident-statuses': incidentStatuses, 'idea-statuses': ideaStatuses, 'order-statuses': orderStatuses, 'process-statuses': processStatuses, + users: sbUsers, delegations, + } + const exportConfig = LIST_EXPORT_CONFIGS[page] + const exportRows = exportRowsMap[page] + + const canModify = canModifyPage(page) + const newBtn = canModify ? newButtonMap[page] : null + + return ( +
+ {/* Sidebar */} + setPage(p)} + hasDashboard={hasDashboard} + hasReports={hasReports} + hasDelegations={hasDelegations} + role={role} + rtl={t.rtl} + navLabels={t.nav as Record} + settingsLabel={t.nav.settings} + /> + + {/* Main content */} +
+ {/* Top strip */} +
+ +
+ + {/* Content header — hidden on profile page */} + {page !== 'profile' && ( +
+
+
{pageTitle[page] || page}
+ {exportRows && ( +
+ Total: {exportRows.length} {pageTitle[page] ? pageTitle[page].toLowerCase() : 'records'} +
+ )} +
+
+ {exportConfig && exportRows && ( + + )} + {newBtn && ( + + )} +
+
+ )} + + {exportConfig && exportRows && exportOpen && ( + setExportOpen(false)} + /> + )} + + {/* Page content */} +
+ {loading &&
{t.loading}
} + + {!loading && page === 'dashboard' && hasDashboard && ( + + )} + {!loading && page === 'inbox' && } + {!loading && page === 'reports' && hasReports && } + {!loading && page === 'incidents' && deleteRecord(DOCTYPES.incidents, name) : undefined} canModify={canModify} userRole={role} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'ideas' && deleteRecord(DOCTYPES.ideas, name) : undefined} canModify={canModify} userRole={role} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'orders' && deleteRecord(DOCTYPES.orders, name) : undefined} canModify={canModify} userRole={role} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'processes' && deleteRecord(DOCTYPES.processes, name) : undefined} canModify={canModify} userRole={role} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'organizations' && deleteRecord(DOCTYPES.organizations, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'group-companies' && deleteRecord(DOCTYPES.groupCompanies, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'managements' && deleteRecord(DOCTYPES.managements, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'departments' && deleteRecord(DOCTYPES.departments, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'branches' && deleteRecord(DOCTYPES.branches, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'cycles' && deleteRecord(DOCTYPES.cycles, name) : undefined} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'kpi-input' && deleteRecord(DOCTYPES.kpiInput, name) : undefined} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'incident-types' && deleteRecord(DOCTYPES.incidentTypes, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'idea-types' && deleteRecord(DOCTYPES.ideaTypes, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'order-types' && deleteRecord(DOCTYPES.orderTypes, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'process-types' && deleteRecord(DOCTYPES.processTypes, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'incident-statuses' && deleteRecord(DOCTYPES.incidentStatuses, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'idea-statuses' && deleteRecord(DOCTYPES.ideaStatuses, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'order-statuses' && deleteRecord(DOCTYPES.orderStatuses, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'process-statuses' && deleteRecord(DOCTYPES.processStatuses, name) : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'users' && deleteRecord('User', name) : undefined} onDisable={canModify ? disableUser : undefined} canModify={canModify} onSelectionChange={(sel, pg) => { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {!loading && page === 'delegations' && hasDelegations && { setSelectedExportRows(sel); setPageExportRows(pg) }} />} + {page === 'profile' && (() => { + // Viewing your own account is always self-service-editable (incl. password), + // regardless of the generic per-page role permissions used for admins/auditors + // browsing *other* users' profiles from the Users list. + const isOwnProfile = !!profileUser && (profileUser.name === user.name || profileUser.email === user.name) + return ( + setPage(isOwnProfile ? (hasDashboard ? 'dashboard' : 'inbox') : 'users')} + onNavigate={setPage} + /> + ) + })()} +
+
+ setDeleteModalState(prev => ({ ...prev, isOpen: false }))} + onConfirmSoftDelete={handleConfirmSoftDelete} + onConfirmPermanentDelete={handleConfirmPermanentDelete} + /> +
+ ) +} + +function computeRole(roles: string[]): string { + if (roles.some(r => ['CEO', 'Ceo', 'SB CEO', 'Chairman'].includes(r))) return 'CEO' + if (roles.some(r => ['Business Owner', 'SB Business Owner', 'SB Owner', 'Owner'].includes(r))) return 'Business Owner' + if (roles.some(r => ['SB Admin', 'System Manager', 'Administrator'].includes(r))) return 'Admin' + if (roles.includes('SB Manager')) return 'Manager' + if (roles.includes('SB Auditor')) return 'Auditor' + if (roles.includes('SB X-Auditor')) return 'XAuditor' + return 'Employee' +} + +export default function Root() { + const [user, setUser] = useState(() => { + try { + const stored = localStorage.getItem('sidrah_user') + return stored ? JSON.parse(stored) : null + } catch { return null } + }) + + const [darkMode, setDarkMode] = useState(() => localStorage.getItem('sb_dark') === '1') + + const toggleDark = () => { + const next = !darkMode + setDarkMode(next) + localStorage.setItem('sb_dark', next ? '1' : '0') + document.documentElement.classList.toggle('dark', next) + } + + useEffect(() => { + document.documentElement.classList.toggle('dark', darkMode) + }, []) + + useEffect(() => { + if (!user) return + getSessionUser().then(async sessionName => { + const actualName = sessionName || user.name + const roles = await getUserRoles(actualName) + if (!roles.length) return + const role = computeRole(roles) + const myOrg = await getMyOrganization() + const needsUpdate = role !== user.role || actualName !== user.name || myOrg !== (user.custom_organization || '') + if (needsUpdate) { + const updated = { ...user, name: actualName, role, custom_organization: myOrg } + setUser(updated) + localStorage.setItem('sidrah_user', JSON.stringify(updated)) + } + }) + }, [user?.name]) + + const handleLogin = (usr: User) => { + setUser(usr) + localStorage.setItem('sidrah_user', JSON.stringify(usr)) + } + + const handleLogout = () => { + setUser(null) + localStorage.removeItem('sidrah_user') + } + + if (!user) return + return +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..c746e0b --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,612 @@ +import { API_BASE_URL } from '../constants' + +const SB_DOCTYPES = new Set([ + 'SB Incident', 'SB Idea', 'SB Order', 'SB Process', + 'SB Organization', 'SB Cycle', 'SB KPI Input', + 'Managements', 'Department', 'Branch', + 'Incident Types', 'Idea Types', 'Order Types', 'Process Types', + 'Incident Class', 'Incident Field', + 'Incident Statuses', 'Idea Statuses', 'Order Statuses', 'Process Statuses', + 'SB Group Company', 'User', +]) + +let csrfToken = '' + +export async function fetchCSRFToken(): Promise { + try { + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.get_csrf_token`, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (res.ok) { + const json = await res.json() + const token: string = json.message || '' + if (token) { + csrfToken = token + return token + } + } + } catch (e) { + console.error('CSRF fetch error:', e) + } + return csrfToken +} + +async function ensureCSRF(): Promise { + if (!csrfToken) await fetchCSRFToken() + return csrfToken +} + +function extractFrappeErrorMessage(j: any, fallback: string): string { + try { + if (j?._server_messages) { + const messages = JSON.parse(j._server_messages) + if (messages.length) { + const first = JSON.parse(messages[0]) + if (first?.message) return first.message.replace(/<[^>]+>/g, '').trim() + } + } + } catch { /* fall through to fallback */ } + let msg = j?.message || j?.exception || j?.exc || fallback + if (typeof msg === 'string') { + if (msg.includes('frappe.exceptions.')) { + const lines = msg.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('Traceback') && !l.startsWith('File ')) + const lastLine = lines[lines.length - 1] || msg + msg = lastLine.replace(/^frappe\.exceptions\.\w+:\s*/, '').trim() + } + } + return msg || fallback +} + +export async function getResource(doctype: string, name: string, fields: string[] = []): Promise { + // SB doctypes use permission-bypassing endpoint + if (SB_DOCTYPES.has(doctype)) { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_sb_doc?doctype=${encodeURIComponent(doctype)}&name=${encodeURIComponent(name)}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) throw new Error(`API Error: ${res.status}`) + const json = await res.json() + return json.message + } + const query = fields.length ? `?fields=${encodeURIComponent(JSON.stringify(fields))}` : '' + const res = await fetch(`${API_BASE_URL}/api/v1/resource/${doctype}/${encodeURIComponent(name)}${query}`, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (!res.ok) throw new Error(`API Error: ${res.status}`) + const json = await res.json() + return json.data +} + +export async function deleteResource(doctype: string, name: string, reason: string = '', permanent: boolean = false): Promise { + const token = await ensureCSRF() + + if (SB_DOCTYPES.has(doctype)) { + const params = new URLSearchParams({ doctype, name }) + if (reason) params.append('reason', reason) + if (permanent) params.append('permanent', '1') + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.delete_sb_doc`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(extractFrappeErrorMessage(j, `Error deleting ${name}`)) } + return + } + + const params = new URLSearchParams({ doctype, name }) + if (reason) params.append('reason', reason) + if (permanent) params.append('permanent', '1') + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.soft_delete.soft_delete`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + + if (!res.ok) { + const j = await res.json().catch(() => ({})) + throw new Error(extractFrappeErrorMessage(j, `Error deleting ${name}`)) + } + + return await res.json() +} + +export async function restoreResource(doctype: string, name: string): Promise { + const token = await ensureCSRF() + + if (SB_DOCTYPES.has(doctype)) { + const params = new URLSearchParams({ doctype, name }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.restore_sb_doc`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(extractFrappeErrorMessage(j, `Error restoring ${name}`)) } + const json = await res.json() + return json.message || { success: true } + } + + const params = new URLSearchParams({ doctype, name }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.soft_delete.restore_doc`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + + if (!res.ok) { + const j = await res.json().catch(() => ({})) + throw new Error(extractFrappeErrorMessage(j, `Error restoring ${name}`)) + } + + return await res.json() +} + +export async function get(doctype: string): Promise { + if (SB_DOCTYPES.has(doctype)) { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_sb_list?doctype=${encodeURIComponent(doctype)}&limit=500` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } + } + const fields = encodeURIComponent('["*"]') + const url = `${API_BASE_URL}/api/v1/resource/${doctype}?limit_page_length=500&fields=${fields}` + try { + const res = await fetch(url, { method: 'GET', credentials: 'include', headers: { Accept: 'application/json' } }) + if (res.status === 401 || res.status === 403) return [] + if (!res.ok) return [] + const json = await res.json() + return json.data || [] + } catch (e) { + console.error('Get error:', e) + return [] + } +} + +export async function post(doctype: string, data: Record): Promise { + const token = await ensureCSRF() + + if (SB_DOCTYPES.has(doctype)) { + const params = new URLSearchParams({ doctype, data_json: JSON.stringify(data) }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.create_sb_doc`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.message || j.exc || `Error creating ${doctype}`) } + const j = await res.json() + return { data: j.message } + } + + const url = `${API_BASE_URL}/api/v1/resource/${doctype}` + try { + const res = await fetch(url, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: JSON.stringify(data) + }) + if (res.status === 401 || res.status === 403) throw new Error('Permission denied - login required') + if (!res.ok) { const json = await res.json(); throw new Error(json.message || json.exc || `API Error: ${res.status}`) } + return await res.json() + } catch (e) { console.error('Post error:', e); throw e } +} + +export async function updateDoc(doctype: string, name: string, data: Record): Promise { + const token = await ensureCSRF() + + if (SB_DOCTYPES.has(doctype)) { + const params = new URLSearchParams({ doctype, name, data_json: JSON.stringify(data) }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.update_sb_doc`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.message || j.exc || `Error updating ${doctype}`) } + const j = await res.json() + return { data: j.message } + } + + const url = `${API_BASE_URL}/api/v1/resource/${doctype}/${encodeURIComponent(name)}` + try { + const res = await fetch(url, { + method: 'PUT', + credentials: 'include', + headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: JSON.stringify(data) + }) + if (res.status === 401 || res.status === 403) throw new Error('Permission denied - login required') + if (!res.ok) { const json = await res.json(); throw new Error(json.message || 'Update Error') } + return await res.json() + } catch (e) { console.error('Update error:', e); throw e } +} + +export async function getUsers(): Promise { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_users` + try { + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (res.status === 401 || res.status === 403) return [] + if (!res.ok) return [] + const json = await res.json() + return json.message || json.data || [] + } catch (e) { + console.error('Get users error:', e) + return [] + } +} + +export async function loginApi(usr: string, pwd: string): Promise { + const form = new FormData() + form.append('usr', usr) + form.append('pwd', pwd) + const res = await fetch(`${API_BASE_URL}/api/method/login`, { + method: 'POST', + credentials: 'include', + body: form + }) + const data = await res.json() + if (data.csrf_token) { + csrfToken = data.csrf_token + } else { + await fetchCSRFToken() + } + return data +} + +export async function logoutApi(): Promise { + const token = await ensureCSRF() + try { + await fetch(`${API_BASE_URL}/api/method/logout`, { + method: 'POST', + credentials: 'include', + headers: { + 'X-Frappe-CSRF-Token': token, + Accept: 'application/json' + } + }) + } catch { + // Ignore logout errors — clear local state regardless + } + csrfToken = '' +} + +export function setCSRF(token: string): void { + csrfToken = token +} + +export function getCsrfToken(): string { + return csrfToken +} + +export async function getSessionUser(): Promise { + try { + const res = await fetch(`${API_BASE_URL}/api/method/frappe.auth.get_logged_user`, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (!res.ok) return '' + const json = await res.json() + return json.message || '' + } catch { + return '' + } +} + +export async function getSBUsers(): Promise { + const filters = JSON.stringify([['name','not in',['Administrator','Guest']]]) + const url = `${API_BASE_URL}/api/v1/resource/User?limit_page_length=500&fields=${encodeURIComponent('["*"]')}&filters=${encodeURIComponent(filters)}` + try { + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.data || [] + } catch { return [] } +} + +export async function getSBUserRoleMap(): Promise> { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_user_role_map` + try { + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return {} + const json = await res.json() + return json.message || {} + } catch { return {} } +} + +export async function setUserPassword(userName: string, newPassword: string): Promise { + const token = await ensureCSRF() + try { + const params = new URLSearchParams({ user: userName, password: newPassword }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.set_user_password`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { + await updateDoc('User', userName, { new_password: newPassword }) + } + } catch { + await updateDoc('User', userName, { new_password: newPassword }) + } +} + +export async function getNotifications(limit = 20): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_notifications?limit=${limit}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function markAllNotificationsRead(): Promise { + const token = await ensureCSRF() + try { + await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.mark_notifications_read`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token } + }) + } catch { /* non-critical */ } +} + +export async function markNotificationRead(name: string): Promise { + const token = await ensureCSRF() + try { + await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.mark_notification_read`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: new URLSearchParams({ name }).toString(), + }) + } catch { /* non-critical */ } +} + +export async function disableUserApi(name: string): Promise { + const token = await ensureCSRF() + const params = new URLSearchParams({ name }) + await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.disable_user`, { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) +} + +export async function getComments(doctype: string, docname: string): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_doc_comments?doctype=${encodeURIComponent(doctype)}&docname=${encodeURIComponent(docname)}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function postComment(doctype: string, docname: string, content: string): Promise { + const token = await ensureCSRF() + const params = new URLSearchParams({ doctype, docname, content }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.post_doc_comment`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) throw new Error('Failed to post comment') + const json = await res.json() + return json.message +} + +export async function getActivityLog(doctype: string, docname: string): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_doc_versions?doctype=${encodeURIComponent(doctype)}&docname=${encodeURIComponent(docname)}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function updateProcessDocStatus(name: string, status: string): Promise { + const token = await ensureCSRF() + const params = new URLSearchParams({ name, status }) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.update_process_doc_status`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { + const json = await res.json().catch(() => ({})) + throw new Error(json.message || 'Failed to update status') + } +} + +export async function getUserRoles(user: string): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_user_roles?user=${encodeURIComponent(user)}` + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { + return [] + } +} + +export async function getMyOrganization(): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_my_organization` + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' } + }) + if (!res.ok) return '' + const json = await res.json() + return json.message || '' + } catch { + return '' + } +} + +async function callMethod(method: string, params: Record = {}): Promise { + const token = await ensureCSRF() + const body = new URLSearchParams(params) + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.${method}`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: body.toString(), + }) + if (!res.ok) { + const json = await res.json().catch(() => ({})) + throw new Error(extractFrappeErrorMessage(json, `Error calling ${method}`)) + } + const json = await res.json() + return json.message +} + +export async function applyOrderWorkflow(name: string, action: string, comment = '', stage?: string, status?: string, cycleStatus?: string): Promise { + const params: Record = { name, action, comment } + if (stage !== undefined) params.stage = stage + if (status !== undefined) params.status = status + if (cycleStatus !== undefined) params.cycle_status = cycleStatus + return callMethod('apply_order_workflow', params) +} + +export async function applyIncidentWorkflow(name: string, action: string, fieldsJson = ''): Promise { + return callMethod('apply_incident_workflow', { name, action, fields_json: fieldsJson }) +} + +export async function applyIdeaWorkflow(name: string, action: string, fieldsJson = ''): Promise { + return callMethod('apply_idea_workflow', { name, action, fields_json: fieldsJson }) +} + +export async function getDashboardData(organization = ''): Promise { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_dashboard_data${organization ? `?organization=${encodeURIComponent(organization)}` : ''}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return { escalated_processes: [], escalated_orders: [], untriaged_incidents: [], kpi_inputs: [] } + const json = await res.json() + return json.message || {} +} + +export async function getReportData(reportName: string, filters: Record = {}): Promise { + return callMethod('get_report_data', { report_name: reportName, filters_json: JSON.stringify(filters) }) +} + +export async function getInboxItems(): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_inbox_items` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function createDelegation(manager: string, functions: string[], organization = '', remarks = '', incident_name = '', idea_name = ''): Promise { + return callMethod('create_delegation', { manager, functions_json: JSON.stringify(functions), organization, remarks, incident_name, idea_name }) +} + +export async function respondToDelegation(name: string, response: 'Accepted' | 'Rejected', remarks = ''): Promise { + return callMethod('respond_to_delegation', { name, response, remarks }) +} + +export async function revokeDelegation(name: string, remarks = ''): Promise { + return callMethod('revoke_delegation', { name, remarks }) +} + +export async function getDelegations(): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_delegations` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function getDelegation(name: string): Promise { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_delegation?name=${encodeURIComponent(name)}` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) throw new Error('Failed to load delegation') + const json = await res.json() + return json.message +} + +export async function getMyDelegatedFunctions(): Promise { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_my_delegated_functions` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +// Per-function organization scope: null = unrestricted (all orgs), else the specific delegated org list +export async function getMyDelegatedOrganizations(): Promise> { + try { + const url = `${API_BASE_URL}/api/method/sidrah_business.api.get_my_delegated_organizations` + const res = await fetch(url, { credentials: 'include', headers: { Accept: 'application/json' } }) + if (!res.ok) return {} + const json = await res.json() + return json.message || {} + } catch { return {} } +} + +export async function getGroupCompanies(): Promise { + try { + const token = await ensureCSRF() + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.get_group_companies`, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + }) + if (!res.ok) return [] + const json = await res.json() + return json.message || [] + } catch { return [] } +} + +export async function saveGroupCompanyApi(data: any): Promise { + const token = await ensureCSRF() + const params = new URLSearchParams() + for (const k in data) { + if (data[k] !== undefined && data[k] !== null) { + params.append(k, String(data[k])) + } + } + const res = await fetch(`${API_BASE_URL}/api/method/sidrah_business.api.save_group_company`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Frappe-CSRF-Token': token }, + body: params.toString(), + }) + if (!res.ok) { + const j = await res.json().catch(() => ({})) + throw new Error(extractFrappeErrorMessage(j, 'Error saving Group Company')) + } + const json = await res.json() + return json.message +} diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..5187b26 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ActivityLog.tsx b/frontend/src/components/ActivityLog.tsx new file mode 100644 index 0000000..d0b26c4 --- /dev/null +++ b/frontend/src/components/ActivityLog.tsx @@ -0,0 +1,227 @@ +import { useState, useEffect, useCallback } from 'react' +import { getActivityLog } from '../api' +import { useT } from '../i18n' + +interface RawEntry { + name: string + owner: string + creation: string + data: string +} + +interface Change { field: string; oldVal: string; newVal: string } + +interface ParsedEntry { + name: string + owner: string + creation: string + changes: Change[] + added: number + removed: number + rowChanged: number +} + +interface Props { + doctype: string + docname: string + docCreation?: string + docOwner?: string +} + +function timeAgo(dateStr: string): string { + if (!dateStr) return '' + const diff = (Date.now() - new Date(dateStr).getTime()) / 1000 + if (diff < 60) return `${Math.floor(diff)}s ago` + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d ago` + return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +} + +function fmtField(f: string): string { + return (f || '').replace(/^custom_/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) +} + +function fmtVal(v: unknown): string { + if (v === null || v === undefined || v === '') return '(empty)' + if (typeof v === 'object') return JSON.stringify(v) + return String(v) +} + +function userLabel(owner: string): string { + return (owner || 'Unknown').replace(/@.*/, '').replace(/[._]/g, ' ') +} + +function userInitials(owner: string): string { + return userLabel(owner).split(' ').filter(Boolean).slice(0, 2).map(w => w[0].toUpperCase()).join('') +} + +function parseEntry(entry: RawEntry): ParsedEntry { + let data: any = {} + try { data = JSON.parse(entry.data || '{}') } catch { /* ignore */ } + const changes: Change[] = (data.changed || []).map((c: any[]) => ({ + field: c[0] || '', + oldVal: fmtVal(c[1]), + newVal: fmtVal(c[2]), + })) + return { + name: entry.name, + owner: entry.owner, + creation: entry.creation, + changes, + added: (data.added || []).length, + removed: (data.removed || []).length, + rowChanged: (data.row_changed || []).length, + } +} + +export default function ActivityLog({ doctype, docname, docCreation, docOwner }: Props) { + const t = useT() + const [entries, setEntries] = useState([]) + const [loading, setLoading] = useState(false) + const [collapsed, setCollapsed] = useState(true) + const [showAll, setShowAll] = useState(false) + const INITIAL_SHOW = 5 + + const load = useCallback(async () => { + setLoading(true) + const data = await getActivityLog(doctype, docname) + setEntries(data.map(parseEntry)) + setLoading(false) + }, [doctype, docname]) + + useEffect(() => { + if (!collapsed) load() + }, [collapsed, load]) + + const visible = showAll ? entries : entries.slice(0, INITIAL_SHOW) + const hidden = entries.length - INITIAL_SHOW + + const dotStyle = (color: string, isFirst = false): React.CSSProperties => ({ + position: 'absolute', + left: -14, + top: 14, + width: 10, + height: 10, + borderRadius: '50%', + background: color, + border: '2px solid white', + boxShadow: isFirst ? `0 0 0 2px ${color}44` : 'none', + }) + + return ( +
+ {/* Header */} +
setCollapsed(c => !c)} + style={{ padding: '12px 20px', borderBottom: collapsed ? 'none' : '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', userSelect: 'none' }}> +
+ 📋 {t.activity.title} + {!collapsed && entries.length > 0 && ( + + {entries.length} + + )} +
+
+ {!collapsed && ( + + )} + {collapsed ? '▼' : '▲'} +
+
+ + {!collapsed && ( +
+ {loading && ( +
{t.loading}
+ )} + + {!loading && ( +
+ {/* Timeline vertical line */} +
+ +
+ {visible.map((entry, idx) => ( +
+
+
+ {/* Entry header */} +
+
+ {userInitials(entry.owner)} +
+ {userLabel(entry.owner)} + {timeAgo(entry.creation)} +
+ + {/* Changes */} + {entry.changes.length > 0 ? ( +
+ {entry.changes.map((ch, i) => ( +
+ {fmtField(ch.field)} + changed from + {ch.oldVal} + → + {ch.newVal} +
+ ))} +
+ ) : ( +
+ Document updated + {entry.added > 0 && · +{entry.added} row(s)} + {entry.removed > 0 && · -{entry.removed} row(s)} + {entry.rowChanged > 0 && · ~{entry.rowChanged} row(s) modified} +
+ )} +
+
+ ))} + + {/* Creation entry at bottom */} + {docCreation && docOwner && ( +
+
+
+
+
+ {userInitials(docOwner)} +
+ {userLabel(docOwner)} + {timeAgo(docCreation)} +
+
✓ {t.activity.created}
+
+
+ )} + + {entries.length === 0 && !docCreation && ( +
No activity recorded
+ )} +
+
+ )} + + {/* Show all / less */} + {!loading && hidden > 0 && ( +
+ +
+ )} +
+ )} +
+ ) +} diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx new file mode 100644 index 0000000..2d1fd9b --- /dev/null +++ b/frontend/src/components/AppSidebar.tsx @@ -0,0 +1,239 @@ +import { NAV_ITEMS, settingsNavForRole } from '../constants' +import { Page } from '../types' +import logoImg from '../assets/logo.png' + +interface AppSidebarProps { + collapsed: boolean + onToggleCollapsed: () => void + activePage: string + onNavigate: (page: Page) => void + hasDashboard: boolean + hasReports: boolean + hasDelegations: boolean + role: string + hideSettings?: boolean + rtl: boolean + navLabels: Record + settingsLabel: string +} + +export default function AppSidebar({ + collapsed, + onToggleCollapsed, + activePage, + onNavigate, + hasDashboard, + hasReports, + hasDelegations, + role, + hideSettings, + rtl, + navLabels, + settingsLabel, +}: AppSidebarProps) { + const visibleSettingsNav = hideSettings ? [] : settingsNavForRole(role) + + return ( +
+ {/* Logo + toggle */} +
+
+
+ Logo { + (e.currentTarget as HTMLElement).style.display = 'none' + }} + /> +
+ {!collapsed && ( +
+ + {rtl ? 'صدرة للأعمال' : 'Sidrah'} + +
+ )} +
+ +
+ + {/* Navigation list */} + +
+ ) +} diff --git a/frontend/src/components/CommentSection.tsx b/frontend/src/components/CommentSection.tsx new file mode 100644 index 0000000..e2c5751 --- /dev/null +++ b/frontend/src/components/CommentSection.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect, useCallback } from 'react' +import { getComments, postComment, deleteResource } from '../api' +import { useT } from '../i18n' + +interface Comment { + name: string + owner: string + creation: string + content: string + comment_by: string + comment_email: string +} + +interface Props { + doctype: string + docname: string + currentUserName: string +} + +function timeAgo(dateStr: string): string { + const diff = (Date.now() - new Date(dateStr).getTime()) / 1000 + if (diff < 60) return `${Math.floor(diff)}s ago` + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d ago` + return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +} + +function avatarInitials(name: string): string { + if (!name) return 'U' + const parts = name.replace(/@.*/, '').replace(/[._-]/g, ' ').split(' ').filter(Boolean) + return parts.slice(0, 2).map(p => p[0].toUpperCase()).join('') +} + +export default function CommentSection({ doctype, docname, currentUserName }: Props) { + const t = useT() + const [comments, setComments] = useState([]) + const [loading, setLoading] = useState(false) + const [text, setText] = useState('') + const [posting, setPosting] = useState(false) + const [collapsed, setCollapsed] = useState(false) + const [showAll, setShowAll] = useState(false) + const [postError, setPostError] = useState('') + const INITIAL_SHOW = 5 + + const load = useCallback(async () => { + setLoading(true) + const data = await getComments(doctype, docname) + setComments(data) + setLoading(false) + }, [doctype, docname]) + + useEffect(() => { load() }, [load]) + + const submit = async () => { + const trimmed = text.trim() + if (!trimmed || posting) return + setPosting(true) + setPostError('') + try { + await postComment(doctype, docname, trimmed) + setText('') + await load() + } catch { + setPostError('Failed to post comment. Please try again.') + } + setPosting(false) + } + + const remove = async (name: string) => { + if (!window.confirm('Delete this comment?')) return + try { + await deleteResource('Comment', name) + setComments(cs => cs.filter(c => c.name !== name)) + } catch { + alert('Failed to delete comment.') + } + } + + const visible = showAll ? comments : comments.slice(-INITIAL_SHOW) + const hidden = Math.max(0, comments.length - INITIAL_SHOW) + + return ( +
+ {/* Header */} +
setCollapsed(c => !c)} + style={{ padding: '12px 20px', borderBottom: collapsed ? 'none' : '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', userSelect: 'none', background: 'white' }}> +
+ 💬 {t.comments.title} + {comments.length > 0 && ( + + {comments.length} + + )} +
+
+ + {collapsed ? '▼' : '▲'} +
+
+ + {!collapsed && ( +
+ {/* Show older comments */} + {!showAll && hidden > 0 && ( + + )} + + {loading && ( +
{t.loading}
+ )} + + {!loading && comments.length === 0 && ( +
{t.comments.noComments}
+ )} + + {/* Comment list */} + {!loading && visible.length > 0 && ( +
+ {visible.map(c => { + const displayName = c.comment_by || c.comment_email || c.owner || 'Unknown' + const isOwn = c.owner === currentUserName || c.comment_by === currentUserName + return ( +
+
+ {avatarInitials(displayName)} +
+
+
+
+ {displayName} + {timeAgo(c.creation)} +
+ {isOwn && ( + + )} +
+
+
+
+ ) + })} +
+ )} + + {/* Compose box */} +
0 ? '1px solid #f0f0f0' : 'none', paddingTop: comments.length > 0 ? 14 : 0 }}> +