From 99ab5b2ce66f309fa616f8a7e8b025d3e2d00f73 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 11 Sep 2026 12:13:25 +0200 Subject: [PATCH] Added PFM Console UI project --- PostFundManagement.ConsoleUI/.env.example | 18 + PostFundManagement.ConsoleUI/.gitignore | 8 + PostFundManagement.ConsoleUI/Dockerfile | 36 + PostFundManagement.ConsoleUI/bun.lock | 3608 +++++++++++++++++ .../docker-compose.yml | 19 + .../docker-entrypoint.sh | 4 + PostFundManagement.ConsoleUI/index.html | 18 + PostFundManagement.ConsoleUI/metadata.json | 6 + PostFundManagement.ConsoleUI/nginx.conf | 37 + PostFundManagement.ConsoleUI/package.json | 42 + PostFundManagement.ConsoleUI/src/App.tsx | 51 + .../src/auth/AuthProvider.tsx | 275 ++ PostFundManagement.ConsoleUI/src/auth/jwt.ts | 23 + .../src/components/layout/AppLayout.tsx | 279 ++ .../src/components/ui/Modal.tsx | 67 + .../src/components/ui/Toast.tsx | 79 + PostFundManagement.ConsoleUI/src/config.ts | 43 + PostFundManagement.ConsoleUI/src/index.css | 256 ++ .../src/lib/formatters.ts | 898 ++++ .../src/lib/queryClient.ts | 11 + PostFundManagement.ConsoleUI/src/main.tsx | 10 + .../src/pages/ExecutiveDashboard.tsx | 655 +++ .../src/pages/GovernancePage.tsx | 860 ++++ .../src/pages/GrantsPage.tsx | 1034 +++++ .../src/pages/LandingPage.tsx | 270 ++ .../src/pages/MonitoringPage.tsx | 848 ++++ .../src/pages/ProjectsPage.tsx | 789 ++++ .../src/pages/SettingsPage.tsx | 501 +++ .../src/services/api.ts | 535 +++ .../src/services/httpClient.ts | 28 + .../src/services/toolsService.ts | 196 + PostFundManagement.ConsoleUI/src/types.ts | 666 +++ .../src/vite-env.d.ts | 13 + PostFundManagement.ConsoleUI/tsconfig.json | 26 + PostFundManagement.ConsoleUI/vite.config.ts | 22 + 35 files changed, 12231 insertions(+) create mode 100644 PostFundManagement.ConsoleUI/.env.example create mode 100644 PostFundManagement.ConsoleUI/.gitignore create mode 100644 PostFundManagement.ConsoleUI/Dockerfile create mode 100644 PostFundManagement.ConsoleUI/bun.lock create mode 100644 PostFundManagement.ConsoleUI/docker-compose.yml create mode 100644 PostFundManagement.ConsoleUI/docker-entrypoint.sh create mode 100644 PostFundManagement.ConsoleUI/index.html create mode 100644 PostFundManagement.ConsoleUI/metadata.json create mode 100644 PostFundManagement.ConsoleUI/nginx.conf create mode 100644 PostFundManagement.ConsoleUI/package.json create mode 100644 PostFundManagement.ConsoleUI/src/App.tsx create mode 100644 PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx create mode 100644 PostFundManagement.ConsoleUI/src/auth/jwt.ts create mode 100644 PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx create mode 100644 PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx create mode 100644 PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx create mode 100644 PostFundManagement.ConsoleUI/src/config.ts create mode 100644 PostFundManagement.ConsoleUI/src/index.css create mode 100644 PostFundManagement.ConsoleUI/src/lib/formatters.ts create mode 100644 PostFundManagement.ConsoleUI/src/lib/queryClient.ts create mode 100644 PostFundManagement.ConsoleUI/src/main.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/GrantsPage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/LandingPage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/MonitoringPage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/SettingsPage.tsx create mode 100644 PostFundManagement.ConsoleUI/src/services/api.ts create mode 100644 PostFundManagement.ConsoleUI/src/services/httpClient.ts create mode 100644 PostFundManagement.ConsoleUI/src/services/toolsService.ts create mode 100644 PostFundManagement.ConsoleUI/src/types.ts create mode 100644 PostFundManagement.ConsoleUI/src/vite-env.d.ts create mode 100644 PostFundManagement.ConsoleUI/tsconfig.json create mode 100644 PostFundManagement.ConsoleUI/vite.config.ts diff --git a/PostFundManagement.ConsoleUI/.env.example b/PostFundManagement.ConsoleUI/.env.example new file mode 100644 index 0000000..ea2ec85 --- /dev/null +++ b/PostFundManagement.ConsoleUI/.env.example @@ -0,0 +1,18 @@ +# GEMINI_API_KEY: Required for Gemini AI API calls. +# AI Studio automatically injects this at runtime from user secrets. +# Users configure this via the Secrets panel in the AI Studio UI. +GEMINI_API_KEY="MY_GEMINI_API_KEY" + +# APP_URL: The URL where this applet is hosted. +# AI Studio automatically injects this at runtime with the Cloud Run service URL. +# Used for self-referential links, OAuth callbacks, and API endpoints. +APP_URL="MY_APP_URL" + +# OIDC SecuritySettings +VITE_OIDC_AUTHORITY="https://sts.security.khongisa.co.za" +VITE_OIDC_AUDIENCE="pfm-api-dev" +VITE_OIDC_CLIENT_ID="pfm-scalar-dev" +VITE_OIDC_CLIENT_SECRET="secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27" + +# PFM Backend API Base URL +VITE_API_BASE_URL="https://api.ifri.khongisa.co.za/" diff --git a/PostFundManagement.ConsoleUI/.gitignore b/PostFundManagement.ConsoleUI/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/PostFundManagement.ConsoleUI/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/PostFundManagement.ConsoleUI/Dockerfile b/PostFundManagement.ConsoleUI/Dockerfile new file mode 100644 index 0000000..cf1d6c8 --- /dev/null +++ b/PostFundManagement.ConsoleUI/Dockerfile @@ -0,0 +1,36 @@ +# Stage 1: Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy dependency definitions +COPY package*.json ./ + +# Install dependencies +RUN npm ci || npm install + +# Copy application source +COPY . . + +# Build Vite application for production +RUN npm run build + +# Stage 2: Production Nginx Server +FROM nginx:alpine + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy compiled static assets from build stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy entrypoint script for dynamic runtime environment variables +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +# Expose port 3000 +EXPOSE 3000 + +# Set entrypoint and default command +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/PostFundManagement.ConsoleUI/bun.lock b/PostFundManagement.ConsoleUI/bun.lock new file mode 100644 index 0000000..6ca99d7 --- /dev/null +++ b/PostFundManagement.ConsoleUI/bun.lock @@ -0,0 +1,3608 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "react-example", + "dependencies": { + "@google/genai": "^2.4.0", + "@hookform/resolvers": "^5.9.1", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.102.8", + "@vitejs/plugin-react": "^5.0.4", + "axios": "^1.20.0", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "oidc-client-ts": "^3.5.0", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "react-hook-form": "^7.87.0", + "react-router-dom": "^7.18.3", + "vite": "^6.2.3", + "zod": "^4.5.4" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "esbuild": "^0.25.0", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.3" + } + } + }, + "packages": { + "@babel/code-frame": [ + "@babel/code-frame@7.29.7", + "", + { + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==" + ], + + "@babel/compat-data": [ + "@babel/compat-data@7.29.7", + "", + {}, + "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==" + ], + + "@babel/core": [ + "@babel/core@7.29.7", + "", + { + "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" + } + }, + "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==" + ], + + "@babel/generator": [ + "@babel/generator@7.29.8", + "", + { + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==" + ], + + "@babel/helper-compilation-targets": [ + "@babel/helper-compilation-targets@7.29.7", + "", + { + "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" + } + }, + "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==" + ], + + "@babel/helper-globals": [ + "@babel/helper-globals@7.29.7", + "", + {}, + "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==" + ], + + "@babel/helper-module-imports": [ + "@babel/helper-module-imports@7.29.7", + "", + { + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + } + }, + "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==" + ], + + "@babel/helper-module-transforms": [ + "@babel/helper-module-transforms@7.29.7", + "", + { + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "peerDependencies": { "@babel/core": "^7.0.0" } + }, + "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==" + ], + + "@babel/helper-plugin-utils": [ + "@babel/helper-plugin-utils@7.29.7", + "", + {}, + "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==" + ], + + "@babel/helper-string-parser": [ + "@babel/helper-string-parser@7.29.7", + "", + {}, + "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" + ], + + "@babel/helper-validator-identifier": [ + "@babel/helper-validator-identifier@7.29.7", + "", + {}, + "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + ], + + "@babel/helper-validator-option": [ + "@babel/helper-validator-option@7.29.7", + "", + {}, + "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==" + ], + + "@babel/helpers": [ + "@babel/helpers@7.29.7", + "", + { + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + } + }, + "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==" + ], + + "@babel/parser": [ + "@babel/parser@7.29.8", + "", + { + "dependencies": { "@babel/types": "^7.29.8" }, + "bin": "./bin/babel-parser.js" + }, + "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==" + ], + + "@babel/plugin-transform-react-jsx-self": [ + "@babel/plugin-transform-react-jsx-self@7.29.7", + "", + { + "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, + "peerDependencies": { "@babel/core": "^7.0.0-0" } + }, + "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==" + ], + + "@babel/plugin-transform-react-jsx-source": [ + "@babel/plugin-transform-react-jsx-source@7.29.7", + "", + { + "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, + "peerDependencies": { "@babel/core": "^7.0.0-0" } + }, + "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==" + ], + + "@babel/template": [ + "@babel/template@7.29.7", + "", + { + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + } + }, + "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==" + ], + + "@babel/traverse": [ + "@babel/traverse@7.29.8", + "", + { + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + } + }, + "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==" + ], + + "@babel/types": [ + "@babel/types@7.29.8", + "", + { + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + } + }, + "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==" + ], + + "@esbuild/aix-ppc64": [ + "@esbuild/aix-ppc64@0.25.12", + "", + { + "os": "aix", + "cpu": "ppc64" + }, + "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==" + ], + + "@esbuild/android-arm": [ + "@esbuild/android-arm@0.25.12", + "", + { + "os": "android", + "cpu": "arm" + }, + "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==" + ], + + "@esbuild/android-arm64": [ + "@esbuild/android-arm64@0.25.12", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==" + ], + + "@esbuild/android-x64": [ + "@esbuild/android-x64@0.25.12", + "", + { + "os": "android", + "cpu": "x64" + }, + "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==" + ], + + "@esbuild/darwin-arm64": [ + "@esbuild/darwin-arm64@0.25.12", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==" + ], + + "@esbuild/darwin-x64": [ + "@esbuild/darwin-x64@0.25.12", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==" + ], + + "@esbuild/freebsd-arm64": [ + "@esbuild/freebsd-arm64@0.25.12", + "", + { + "os": "freebsd", + "cpu": "arm64" + }, + "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==" + ], + + "@esbuild/freebsd-x64": [ + "@esbuild/freebsd-x64@0.25.12", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==" + ], + + "@esbuild/linux-arm": [ + "@esbuild/linux-arm@0.25.12", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==" + ], + + "@esbuild/linux-arm64": [ + "@esbuild/linux-arm64@0.25.12", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==" + ], + + "@esbuild/linux-ia32": [ + "@esbuild/linux-ia32@0.25.12", + "", + { + "os": "linux", + "cpu": "ia32" + }, + "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==" + ], + + "@esbuild/linux-loong64": [ + "@esbuild/linux-loong64@0.25.12", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==" + ], + + "@esbuild/linux-mips64el": [ + "@esbuild/linux-mips64el@0.25.12", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==" + ], + + "@esbuild/linux-ppc64": [ + "@esbuild/linux-ppc64@0.25.12", + "", + { + "os": "linux", + "cpu": "ppc64" + }, + "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==" + ], + + "@esbuild/linux-riscv64": [ + "@esbuild/linux-riscv64@0.25.12", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==" + ], + + "@esbuild/linux-s390x": [ + "@esbuild/linux-s390x@0.25.12", + "", + { + "os": "linux", + "cpu": "s390x" + }, + "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==" + ], + + "@esbuild/linux-x64": [ + "@esbuild/linux-x64@0.25.12", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==" + ], + + "@esbuild/netbsd-arm64": [ + "@esbuild/netbsd-arm64@0.25.12", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==" + ], + + "@esbuild/netbsd-x64": [ + "@esbuild/netbsd-x64@0.25.12", + "", + { + "os": "none", + "cpu": "x64" + }, + "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==" + ], + + "@esbuild/openbsd-arm64": [ + "@esbuild/openbsd-arm64@0.25.12", + "", + { + "os": "openbsd", + "cpu": "arm64" + }, + "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==" + ], + + "@esbuild/openbsd-x64": [ + "@esbuild/openbsd-x64@0.25.12", + "", + { + "os": "openbsd", + "cpu": "x64" + }, + "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==" + ], + + "@esbuild/openharmony-arm64": [ + "@esbuild/openharmony-arm64@0.25.12", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==" + ], + + "@esbuild/sunos-x64": [ + "@esbuild/sunos-x64@0.25.12", + "", + { + "os": "sunos", + "cpu": "x64" + }, + "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==" + ], + + "@esbuild/win32-arm64": [ + "@esbuild/win32-arm64@0.25.12", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==" + ], + + "@esbuild/win32-ia32": [ + "@esbuild/win32-ia32@0.25.12", + "", + { + "os": "win32", + "cpu": "ia32" + }, + "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==" + ], + + "@esbuild/win32-x64": [ + "@esbuild/win32-x64@0.25.12", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==" + ], + + "@google/genai": [ + "@google/genai@2.21.0", + "", + { + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, + "optionalPeers": [ "@modelcontextprotocol/sdk" ] + }, + "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==" + ], + + "@hookform/resolvers": [ + "@hookform/resolvers@5.9.1", + "", + { + "dependencies": { "@standard-schema/utils": "^0.3.0" }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0 || ^18.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=6.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "optionalPeers": [ "@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod" ] + }, + "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==" + ], + + "@jridgewell/gen-mapping": [ + "@jridgewell/gen-mapping@0.3.13", + "", + { + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==" + ], + + "@jridgewell/remapping": [ + "@jridgewell/remapping@2.3.5", + "", + { + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==" + ], + + "@jridgewell/resolve-uri": [ + "@jridgewell/resolve-uri@3.1.2", + "", + {}, + "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + ], + + "@jridgewell/sourcemap-codec": [ + "@jridgewell/sourcemap-codec@1.6.0", + "", + {}, + "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" + ], + + "@jridgewell/trace-mapping": [ + "@jridgewell/trace-mapping@0.3.31", + "", + { + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==" + ], + + "@napi-rs/lzma-linux-x64-gnu": [ + "@napi-rs/lzma-linux-x64-gnu@1.5.1", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==" + ], + + "@protobufjs/aspromise": [ + "@protobufjs/aspromise@1.1.2", + "", + {}, + "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + ], + + "@protobufjs/base64": [ + "@protobufjs/base64@1.1.2", + "", + {}, + "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + ], + + "@protobufjs/codegen": [ + "@protobufjs/codegen@2.0.5", + "", + {}, + "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" + ], + + "@protobufjs/eventemitter": [ + "@protobufjs/eventemitter@1.1.1", + "", + {}, + "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==" + ], + + "@protobufjs/fetch": [ + "@protobufjs/fetch@1.1.1", + "", + { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, + "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==" + ], + + "@protobufjs/float": [ + "@protobufjs/float@1.0.2", + "", + {}, + "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + ], + + "@protobufjs/path": [ + "@protobufjs/path@1.1.2", + "", + {}, + "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + ], + + "@protobufjs/pool": [ + "@protobufjs/pool@1.1.0", + "", + {}, + "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + ], + + "@protobufjs/utf8": [ + "@protobufjs/utf8@1.1.2", + "", + {}, + "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==" + ], + + "@rolldown/pluginutils": [ + "@rolldown/pluginutils@1.0.0-rc.3", + "", + {}, + "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==" + ], + + "@rollup/rollup-android-arm-eabi": [ + "@rollup/rollup-android-arm-eabi@4.63.1", + "", + { + "os": "android", + "cpu": "arm" + }, + "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==" + ], + + "@rollup/rollup-android-arm64": [ + "@rollup/rollup-android-arm64@4.63.1", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==" + ], + + "@rollup/rollup-darwin-arm64": [ + "@rollup/rollup-darwin-arm64@4.63.1", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==" + ], + + "@rollup/rollup-darwin-x64": [ + "@rollup/rollup-darwin-x64@4.63.1", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==" + ], + + "@rollup/rollup-freebsd-arm64": [ + "@rollup/rollup-freebsd-arm64@4.63.1", + "", + { + "os": "freebsd", + "cpu": "arm64" + }, + "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==" + ], + + "@rollup/rollup-freebsd-x64": [ + "@rollup/rollup-freebsd-x64@4.63.1", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==" + ], + + "@rollup/rollup-linux-arm-gnueabihf": [ + "@rollup/rollup-linux-arm-gnueabihf@4.63.1", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==" + ], + + "@rollup/rollup-linux-arm-musleabihf": [ + "@rollup/rollup-linux-arm-musleabihf@4.63.1", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==" + ], + + "@rollup/rollup-linux-arm64-gnu": [ + "@rollup/rollup-linux-arm64-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==" + ], + + "@rollup/rollup-linux-arm64-musl": [ + "@rollup/rollup-linux-arm64-musl@4.63.1", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==" + ], + + "@rollup/rollup-linux-loong64-gnu": [ + "@rollup/rollup-linux-loong64-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==" + ], + + "@rollup/rollup-linux-loong64-musl": [ + "@rollup/rollup-linux-loong64-musl@4.63.1", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==" + ], + + "@rollup/rollup-linux-ppc64-gnu": [ + "@rollup/rollup-linux-ppc64-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "ppc64" + }, + "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==" + ], + + "@rollup/rollup-linux-ppc64-musl": [ + "@rollup/rollup-linux-ppc64-musl@4.63.1", + "", + { + "os": "linux", + "cpu": "ppc64" + }, + "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==" + ], + + "@rollup/rollup-linux-riscv64-gnu": [ + "@rollup/rollup-linux-riscv64-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==" + ], + + "@rollup/rollup-linux-riscv64-musl": [ + "@rollup/rollup-linux-riscv64-musl@4.63.1", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==" + ], + + "@rollup/rollup-linux-s390x-gnu": [ + "@rollup/rollup-linux-s390x-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "s390x" + }, + "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==" + ], + + "@rollup/rollup-linux-x64-gnu": [ + "@rollup/rollup-linux-x64-gnu@4.63.1", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==" + ], + + "@rollup/rollup-linux-x64-musl": [ + "@rollup/rollup-linux-x64-musl@4.63.1", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==" + ], + + "@rollup/rollup-openbsd-x64": [ + "@rollup/rollup-openbsd-x64@4.63.1", + "", + { + "os": "openbsd", + "cpu": "x64" + }, + "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==" + ], + + "@rollup/rollup-openharmony-arm64": [ + "@rollup/rollup-openharmony-arm64@4.63.1", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==" + ], + + "@rollup/rollup-win32-arm64-msvc": [ + "@rollup/rollup-win32-arm64-msvc@4.63.1", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==" + ], + + "@rollup/rollup-win32-ia32-msvc": [ + "@rollup/rollup-win32-ia32-msvc@4.63.1", + "", + { + "os": "win32", + "cpu": "ia32" + }, + "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==" + ], + + "@rollup/rollup-win32-x64-gnu": [ + "@rollup/rollup-win32-x64-gnu@4.63.1", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==" + ], + + "@rollup/rollup-win32-x64-msvc": [ + "@rollup/rollup-win32-x64-msvc@4.63.1", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==" + ], + + "@standard-schema/utils": [ + "@standard-schema/utils@0.3.0", + "", + {}, + "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + ], + + "@tailwindcss/node": [ + "@tailwindcss/node@4.3.3", + "", + { + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==" + ], + + "@tailwindcss/oxide": [ + "@tailwindcss/oxide@4.3.3", + "", + { + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==" + ], + + "@tailwindcss/oxide-android-arm64": [ + "@tailwindcss/oxide-android-arm64@4.3.3", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==" + ], + + "@tailwindcss/oxide-darwin-arm64": [ + "@tailwindcss/oxide-darwin-arm64@4.3.3", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==" + ], + + "@tailwindcss/oxide-darwin-x64": [ + "@tailwindcss/oxide-darwin-x64@4.3.3", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==" + ], + + "@tailwindcss/oxide-freebsd-x64": [ + "@tailwindcss/oxide-freebsd-x64@4.3.3", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==" + ], + + "@tailwindcss/oxide-linux-arm-gnueabihf": [ + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==" + ], + + "@tailwindcss/oxide-linux-arm64-gnu": [ + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==" + ], + + "@tailwindcss/oxide-linux-arm64-musl": [ + "@tailwindcss/oxide-linux-arm64-musl@4.3.3", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==" + ], + + "@tailwindcss/oxide-linux-x64-gnu": [ + "@tailwindcss/oxide-linux-x64-gnu@4.3.3", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==" + ], + + "@tailwindcss/oxide-linux-x64-musl": [ + "@tailwindcss/oxide-linux-x64-musl@4.3.3", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==" + ], + + "@tailwindcss/oxide-wasm32-wasi": [ + "@tailwindcss/oxide-wasm32-wasi@4.3.3", + "", + { + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "cpu": "none" + }, + "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==" + ], + + "@tailwindcss/oxide-win32-arm64-msvc": [ + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==" + ], + + "@tailwindcss/oxide-win32-x64-msvc": [ + "@tailwindcss/oxide-win32-x64-msvc@4.3.3", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==" + ], + + "@tailwindcss/vite": [ + "@tailwindcss/vite@4.3.3", + "", + { + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } + }, + "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==" + ], + + "@tanstack/query-core": [ + "@tanstack/query-core@5.102.8", + "", + {}, + "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==" + ], + + "@tanstack/react-query": [ + "@tanstack/react-query@5.102.8", + "", + { + "dependencies": { "@tanstack/query-core": "5.102.8" }, + "peerDependencies": { "react": "^18 || ^19" } + }, + "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==" + ], + + "@types/babel__core": [ + "@types/babel__core@7.20.5", + "", + { + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==" + ], + + "@types/babel__generator": [ + "@types/babel__generator@7.27.0", + "", + { "dependencies": { "@babel/types": "^7.0.0" } }, + "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==" + ], + + "@types/babel__template": [ + "@types/babel__template@7.4.4", + "", + { + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==" + ], + + "@types/babel__traverse": [ + "@types/babel__traverse@7.28.0", + "", + { "dependencies": { "@babel/types": "^7.28.2" } }, + "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==" + ], + + "@types/body-parser": [ + "@types/body-parser@1.19.6", + "", + { + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==" + ], + + "@types/connect": [ + "@types/connect@3.4.38", + "", + { "dependencies": { "@types/node": "*" } }, + "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==" + ], + + "@types/estree": [ + "@types/estree@1.0.9", + "", + {}, + "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" + ], + + "@types/express": [ + "@types/express@4.17.25", + "", + { + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==" + ], + + "@types/express-serve-static-core": [ + "@types/express-serve-static-core@4.19.9", + "", + { + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==" + ], + + "@types/http-errors": [ + "@types/http-errors@2.0.5", + "", + {}, + "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" + ], + + "@types/mime": [ + "@types/mime@1.3.5", + "", + {}, + "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + ], + + "@types/node": [ + "@types/node@22.20.1", + "", + { "dependencies": { "undici-types": "~6.21.0" } }, + "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==" + ], + + "@types/qs": [ + "@types/qs@6.15.1", + "", + {}, + "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==" + ], + + "@types/range-parser": [ + "@types/range-parser@1.2.7", + "", + {}, + "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + ], + + "@types/retry": [ + "@types/retry@0.12.0", + "", + {}, + "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + ], + + "@types/send": [ + "@types/send@1.2.1", + "", + { "dependencies": { "@types/node": "*" } }, + "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==" + ], + + "@types/serve-static": [ + "@types/serve-static@1.15.10", + "", + { + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==" + ], + + "@vitejs/plugin-react": [ + "@vitejs/plugin-react@5.2.0", + "", + { + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } + }, + "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==" + ], + + "accepts": [ + "accepts@1.3.8", + "", + { + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==" + ], + + "agent-base": [ + "agent-base@6.0.2", + "", + { "dependencies": { "debug": "4" } }, + "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==" + ], + + "array-flatten": [ + "array-flatten@1.1.1", + "", + {}, + "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + ], + + "asynckit": [ + "asynckit@0.4.0", + "", + {}, + "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + ], + + "autoprefixer": [ + "autoprefixer@10.5.5", + "", + { + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { "postcss": "^8.1.0" }, + "bin": { "autoprefixer": "bin/autoprefixer" } + }, + "sha512-uiRYvQYe/nNSzBJ7OUnd2/TZVsAdob3blml44teEpee9Cc1f4rGZFewO+JT3Wo8mgFOSzNqes4FHZn/Qz8WOuw==" + ], + + "axios": [ + "axios@1.20.0", + "", + { + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==" + ], + + "base64-js": [ + "base64-js@1.5.1", + "", + {}, + "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + ], + + "baseline-browser-mapping": [ + "baseline-browser-mapping@2.11.21", + "", + { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, + "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==" + ], + + "bignumber.js": [ + "bignumber.js@9.3.1", + "", + {}, + "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==" + ], + + "body-parser": [ + "body-parser@1.20.6", + "", + { + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + } + }, + "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==" + ], + + "browserslist": [ + "browserslist@4.28.9", + "", + { + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { "browserslist": "cli.js" } + }, + "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==" + ], + + "buffer-equal-constant-time": [ + "buffer-equal-constant-time@1.0.1", + "", + {}, + "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + ], + + "bytes": [ + "bytes@3.1.2", + "", + {}, + "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + ], + + "call-bind-apply-helpers": [ + "call-bind-apply-helpers@1.0.2", + "", + { + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==" + ], + + "call-bound": [ + "call-bound@1.0.4", + "", + { + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==" + ], + + "caniuse-lite": [ + "caniuse-lite@1.0.30001810", + "", + {}, + "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==" + ], + + "combined-stream": [ + "combined-stream@1.0.8", + "", + { "dependencies": { "delayed-stream": "~1.0.0" } }, + "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==" + ], + + "content-disposition": [ + "content-disposition@0.5.4", + "", + { "dependencies": { "safe-buffer": "5.2.1" } }, + "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==" + ], + + "content-type": [ + "content-type@1.0.5", + "", + {}, + "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + ], + + "convert-source-map": [ + "convert-source-map@2.0.0", + "", + {}, + "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + ], + + "cookie": [ + "cookie@0.7.2", + "", + {}, + "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + ], + + "cookie-signature": [ + "cookie-signature@1.0.7", + "", + {}, + "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + ], + + "data-uri-to-buffer": [ + "data-uri-to-buffer@4.0.1", + "", + {}, + "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + ], + + "debug": [ + "debug@2.6.9", + "", + { "dependencies": { "ms": "2.0.0" } }, + "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" + ], + + "delayed-stream": [ + "delayed-stream@1.0.0", + "", + {}, + "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + ], + + "depd": [ + "depd@2.0.0", + "", + {}, + "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + ], + + "destroy": [ + "destroy@1.2.0", + "", + {}, + "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + ], + + "detect-libc": [ + "detect-libc@2.1.2", + "", + {}, + "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + ], + + "dotenv": [ + "dotenv@17.4.2", + "", + {}, + "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==" + ], + + "dunder-proto": [ + "dunder-proto@1.0.1", + "", + { + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==" + ], + + "ecdsa-sig-formatter": [ + "ecdsa-sig-formatter@1.0.11", + "", + { "dependencies": { "safe-buffer": "^5.0.1" } }, + "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==" + ], + + "ee-first": [ + "ee-first@1.1.1", + "", + {}, + "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + ], + + "electron-to-chromium": [ + "electron-to-chromium@1.5.422", + "", + {}, + "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==" + ], + + "encodeurl": [ + "encodeurl@2.0.0", + "", + {}, + "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + ], + + "enhanced-resolve": [ + "enhanced-resolve@5.24.5", + "", + { + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + } + }, + "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==" + ], + + "es-define-property": [ + "es-define-property@1.0.1", + "", + {}, + "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + ], + + "es-errors": [ + "es-errors@1.3.0", + "", + {}, + "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + ], + + "es-object-atoms": [ + "es-object-atoms@1.1.2", + "", + { "dependencies": { "es-errors": "^1.3.0" } }, + "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==" + ], + + "es-set-tostringtag": [ + "es-set-tostringtag@2.1.0", + "", + { + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==" + ], + + "esbuild": [ + "esbuild@0.25.12", + "", + { + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + }, + "bin": { "esbuild": "bin/esbuild" } + }, + "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==" + ], + + "escalade": [ + "escalade@3.2.0", + "", + {}, + "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + ], + + "escape-html": [ + "escape-html@1.0.3", + "", + {}, + "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + ], + + "etag": [ + "etag@1.8.1", + "", + {}, + "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + ], + + "express": [ + "express@4.22.2", + "", + { + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==" + ], + + "extend": [ + "extend@3.0.2", + "", + {}, + "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + ], + + "fdir": [ + "fdir@6.5.0", + "", + { + "peerDependencies": { "picomatch": "^3 || ^4" }, + "optionalPeers": [ "picomatch" ] + }, + "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==" + ], + + "fetch-blob": [ + "fetch-blob@3.2.0", + "", + { + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==" + ], + + "finalhandler": [ + "finalhandler@1.3.2", + "", + { + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + } + }, + "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==" + ], + + "follow-redirects": [ + "follow-redirects@1.16.0", + "", + { + "peerDependencies": { "debug": "*" }, + "optionalPeers": [ "debug" ] + }, + "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==" + ], + + "form-data": [ + "form-data@4.0.6", + "", + { + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + } + }, + "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==" + ], + + "formdata-polyfill": [ + "formdata-polyfill@4.0.10", + "", + { "dependencies": { "fetch-blob": "^3.1.2" } }, + "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==" + ], + + "forwarded": [ + "forwarded@0.2.0", + "", + {}, + "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + ], + + "fraction.js": [ + "fraction.js@5.3.4", + "", + {}, + "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==" + ], + + "framer-motion": [ + "framer-motion@12.43.0", + "", + { + "dependencies": { + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "optionalPeers": [ "@emotion/is-prop-valid", "react", "react-dom" ] + }, + "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==" + ], + + "fresh": [ + "fresh@0.5.2", + "", + {}, + "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + ], + + "fsevents": [ + "fsevents@2.3.3", + "", + { "os": "darwin" }, + "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" + ], + + "function-bind": [ + "function-bind@1.1.2", + "", + {}, + "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + ], + + "gaxios": [ + "gaxios@7.3.1", + "", + { + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + } + }, + "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==" + ], + + "gcp-metadata": [ + "gcp-metadata@8.1.2", + "", + { + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + } + }, + "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==" + ], + + "gensync": [ + "gensync@1.0.0-beta.2", + "", + {}, + "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + ], + + "get-intrinsic": [ + "get-intrinsic@1.3.0", + "", + { + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==" + ], + + "get-proto": [ + "get-proto@1.0.1", + "", + { + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==" + ], + + "google-auth-library": [ + "google-auth-library@10.9.1", + "", + { + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + } + }, + "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==" + ], + + "google-logging-utils": [ + "google-logging-utils@1.1.3", + "", + {}, + "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==" + ], + + "gopd": [ + "gopd@1.2.0", + "", + {}, + "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + ], + + "graceful-fs": [ + "graceful-fs@4.2.11", + "", + {}, + "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + ], + + "has-symbols": [ + "has-symbols@1.1.0", + "", + {}, + "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + ], + + "has-tostringtag": [ + "has-tostringtag@1.0.2", + "", + { "dependencies": { "has-symbols": "^1.0.3" } }, + "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==" + ], + + "hasown": [ + "hasown@2.0.4", + "", + { "dependencies": { "function-bind": "^1.1.2" } }, + "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==" + ], + + "http-errors": [ + "http-errors@2.0.1", + "", + { + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==" + ], + + "https-proxy-agent": [ + "https-proxy-agent@5.0.1", + "", + { + "dependencies": { + "agent-base": "6", + "debug": "4" + } + }, + "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==" + ], + + "iconv-lite": [ + "iconv-lite@0.4.24", + "", + { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, + "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==" + ], + + "inherits": [ + "inherits@2.0.4", + "", + {}, + "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + ], + + "ipaddr.js": [ + "ipaddr.js@1.9.1", + "", + {}, + "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + ], + + "jiti": [ + "jiti@2.7.0", + "", + { "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==" + ], + + "js-tokens": [ + "js-tokens@4.0.0", + "", + {}, + "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + ], + + "jsesc": [ + "jsesc@3.1.0", + "", + { "bin": { "jsesc": "bin/jsesc" } }, + "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + ], + + "json-bigint": [ + "json-bigint@1.0.0", + "", + { "dependencies": { "bignumber.js": "^9.0.0" } }, + "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==" + ], + + "json5": [ + "json5@2.2.3", + "", + { "bin": { "json5": "lib/cli.js" } }, + "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + ], + + "jwa": [ + "jwa@2.0.1", + "", + { + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==" + ], + + "jws": [ + "jws@4.0.1", + "", + { + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==" + ], + + "jwt-decode": [ + "jwt-decode@4.0.0", + "", + {}, + "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" + ], + + "lightningcss": [ + "lightningcss@1.32.0", + "", + { + "dependencies": { "detect-libc": "^2.0.3" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==" + ], + + "lightningcss-android-arm64": [ + "lightningcss-android-arm64@1.32.0", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==" + ], + + "lightningcss-darwin-arm64": [ + "lightningcss-darwin-arm64@1.32.0", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==" + ], + + "lightningcss-darwin-x64": [ + "lightningcss-darwin-x64@1.32.0", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==" + ], + + "lightningcss-freebsd-x64": [ + "lightningcss-freebsd-x64@1.32.0", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==" + ], + + "lightningcss-linux-arm-gnueabihf": [ + "lightningcss-linux-arm-gnueabihf@1.32.0", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==" + ], + + "lightningcss-linux-arm64-gnu": [ + "lightningcss-linux-arm64-gnu@1.32.0", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==" + ], + + "lightningcss-linux-arm64-musl": [ + "lightningcss-linux-arm64-musl@1.32.0", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==" + ], + + "lightningcss-linux-x64-gnu": [ + "lightningcss-linux-x64-gnu@1.32.0", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==" + ], + + "lightningcss-linux-x64-musl": [ + "lightningcss-linux-x64-musl@1.32.0", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==" + ], + + "lightningcss-win32-arm64-msvc": [ + "lightningcss-win32-arm64-msvc@1.32.0", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==" + ], + + "lightningcss-win32-x64-msvc": [ + "lightningcss-win32-x64-msvc@1.32.0", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==" + ], + + "long": [ + "long@5.3.2", + "", + {}, + "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + ], + + "lru-cache": [ + "lru-cache@5.1.1", + "", + { "dependencies": { "yallist": "^3.0.2" } }, + "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==" + ], + + "lucide-react": [ + "lucide-react@0.546.0", + "", + { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==" + ], + + "magic-string": [ + "magic-string@0.30.21", + "", + { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==" + ], + + "math-intrinsics": [ + "math-intrinsics@1.1.0", + "", + {}, + "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + ], + + "media-typer": [ + "media-typer@0.3.0", + "", + {}, + "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + ], + + "merge-descriptors": [ + "merge-descriptors@1.0.3", + "", + {}, + "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + ], + + "methods": [ + "methods@1.1.2", + "", + {}, + "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + ], + + "mime": [ + "mime@1.6.0", + "", + { "bin": { "mime": "cli.js" } }, + "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + ], + + "mime-db": [ + "mime-db@1.52.0", + "", + {}, + "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + ], + + "mime-types": [ + "mime-types@2.1.35", + "", + { "dependencies": { "mime-db": "1.52.0" } }, + "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==" + ], + + "motion": [ + "motion@12.43.0", + "", + { + "dependencies": { + "framer-motion": "^12.43.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "optionalPeers": [ "@emotion/is-prop-valid", "react", "react-dom" ] + }, + "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==" + ], + + "motion-dom": [ + "motion-dom@12.43.0", + "", + { "dependencies": { "motion-utils": "^12.39.0" } }, + "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==" + ], + + "motion-utils": [ + "motion-utils@12.39.0", + "", + {}, + "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==" + ], + + "ms": [ + "ms@2.0.0", + "", + {}, + "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + ], + + "nanoid": [ + "nanoid@3.3.18", + "", + { "bin": { "nanoid": "bin/nanoid.cjs" } }, + "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==" + ], + + "negotiator": [ + "negotiator@0.6.3", + "", + {}, + "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + ], + + "node-domexception": [ + "node-domexception@1.0.0", + "", + {}, + "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + ], + + "node-fetch": [ + "node-fetch@3.3.2", + "", + { + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==" + ], + + "node-releases": [ + "node-releases@2.0.54", + "", + {}, + "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==" + ], + + "object-inspect": [ + "object-inspect@1.13.4", + "", + {}, + "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + ], + + "oidc-client-ts": [ + "oidc-client-ts@3.5.0", + "", + { "dependencies": { "jwt-decode": "^4.0.0" } }, + "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==" + ], + + "on-finished": [ + "on-finished@2.4.1", + "", + { "dependencies": { "ee-first": "1.1.1" } }, + "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==" + ], + + "p-retry": [ + "p-retry@4.6.2", + "", + { + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==" + ], + + "parseurl": [ + "parseurl@1.3.3", + "", + {}, + "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + ], + + "path-to-regexp": [ + "path-to-regexp@0.1.13", + "", + {}, + "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" + ], + + "picocolors": [ + "picocolors@1.1.1", + "", + {}, + "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + ], + + "picomatch": [ + "picomatch@4.0.7", + "", + {}, + "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==" + ], + + "postcss": [ + "postcss@8.5.28", + "", + { + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==" + ], + + "postcss-value-parser": [ + "postcss-value-parser@4.2.0", + "", + {}, + "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + ], + + "protobufjs": [ + "protobufjs@7.6.6", + "", + { + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + } + }, + "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==" + ], + + "proxy-addr": [ + "proxy-addr@2.0.7", + "", + { + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==" + ], + + "proxy-from-env": [ + "proxy-from-env@2.1.0", + "", + {}, + "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==" + ], + + "qs": [ + "qs@6.15.3", + "", + { + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } + }, + "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==" + ], + + "range-parser": [ + "range-parser@1.2.1", + "", + {}, + "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + ], + + "raw-body": [ + "raw-body@2.5.3", + "", + { + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + } + }, + "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==" + ], + + "react": [ + "react@19.2.8", + "", + {}, + "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==" + ], + + "react-dom": [ + "react-dom@19.2.8", + "", + { + "dependencies": { "scheduler": "^0.27.0" }, + "peerDependencies": { "react": "^19.2.8" } + }, + "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==" + ], + + "react-hook-form": [ + "react-hook-form@7.87.0", + "", + { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "sha512-zhFzWvLxNHH+8839OnZcUxgMZw88ah2jZWDWvKWgF3Tpbnd0vKL+dlcuU3nZVWESZQjd81EW8K+wU+cYfYAc0w==" + ], + + "react-refresh": [ + "react-refresh@0.18.0", + "", + {}, + "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==" + ], + + "react-router": [ + "react-router@7.18.3", + "", + { + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "optionalPeers": [ "react-dom" ] + }, + "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==" + ], + + "react-router-dom": [ + "react-router-dom@7.18.3", + "", + { + "dependencies": { "react-router": "7.18.3" }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==" + ], + + "retry": [ + "retry@0.13.1", + "", + {}, + "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + ], + + "rollup": [ + "rollup@4.63.1", + "", + { + "dependencies": { "@types/estree": "1.0.9" }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + }, + "bin": { "rollup": "dist/bin/rollup" } + }, + "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==" + ], + + "safe-buffer": [ + "safe-buffer@5.2.1", + "", + {}, + "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + ], + + "safer-buffer": [ + "safer-buffer@2.1.2", + "", + {}, + "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + ], + + "scheduler": [ + "scheduler@0.27.0", + "", + {}, + "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + ], + + "semver": [ + "semver@6.3.1", + "", + { "bin": { "semver": "bin/semver.js" } }, + "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + ], + + "send": [ + "send@0.19.2", + "", + { + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + } + }, + "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==" + ], + + "serve-static": [ + "serve-static@1.16.3", + "", + { + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + } + }, + "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==" + ], + + "set-cookie-parser": [ + "set-cookie-parser@2.7.2", + "", + {}, + "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + ], + + "setprototypeof": [ + "setprototypeof@1.2.0", + "", + {}, + "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + ], + + "side-channel": [ + "side-channel@1.1.1", + "", + { + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==" + ], + + "side-channel-list": [ + "side-channel-list@1.0.1", + "", + { + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + } + }, + "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==" + ], + + "side-channel-map": [ + "side-channel-map@1.0.1", + "", + { + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==" + ], + + "side-channel-weakmap": [ + "side-channel-weakmap@1.0.2", + "", + { + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==" + ], + + "source-map-js": [ + "source-map-js@1.2.1", + "", + {}, + "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + ], + + "statuses": [ + "statuses@2.0.2", + "", + {}, + "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + ], + + "tailwindcss": [ + "tailwindcss@4.3.3", + "", + {}, + "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==" + ], + + "tapable": [ + "tapable@2.3.3", + "", + {}, + "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" + ], + + "tinyglobby": [ + "tinyglobby@0.2.17", + "", + { + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + } + }, + "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==" + ], + + "toidentifier": [ + "toidentifier@1.0.1", + "", + {}, + "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + ], + + "tslib": [ + "tslib@2.8.1", + "", + {}, + "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + ], + + "tsx": [ + "tsx@4.23.13", + "", + { + "dependencies": { "esbuild": "~0.28.0" }, + "optionalDependencies": { "fsevents": "~2.3.3" }, + "bin": { "tsx": "dist/cli.mjs" } + }, + "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==" + ], + + "type-is": [ + "type-is@1.6.18", + "", + { + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==" + ], + + "typescript": [ + "typescript@5.8.3", + "", + { + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + } + }, + "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==" + ], + + "undici-types": [ + "undici-types@6.21.0", + "", + {}, + "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + ], + + "unpipe": [ + "unpipe@1.0.0", + "", + {}, + "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + ], + + "update-browserslist-db": [ + "update-browserslist-db@1.3.2", + "", + { + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "peerDependencies": { "browserslist": ">= 4.21.0" }, + "bin": { "update-browserslist-db": "cli.js" } + }, + "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==" + ], + + "utils-merge": [ + "utils-merge@1.0.1", + "", + {}, + "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + ], + + "vary": [ + "vary@1.1.2", + "", + {}, + "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + ], + + "vite": [ + "vite@6.4.3", + "", + { + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "optionalDependencies": { "fsevents": "~2.3.3" }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "optionalPeers": [ "@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml" ], + "bin": { "vite": "bin/vite.js" } + }, + "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==" + ], + + "web-streams-polyfill": [ + "web-streams-polyfill@3.3.3", + "", + {}, + "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" + ], + + "ws": [ + "ws@8.21.3", + "", + { + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "optionalPeers": [ "bufferutil", "utf-8-validate" ] + }, + "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==" + ], + + "yallist": [ + "yallist@3.1.1", + "", + {}, + "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + ], + + "zod": [ + "zod@4.5.4", + "", + {}, + "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==" + ], + + "@babel/core/debug": [ + "debug@4.4.3", + "", + { + "dependencies": { "ms": "^2.1.3" }, + "peerDependencies": { "supports-color": "*" }, + "optionalPeers": [ "supports-color" ] + }, + "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + ], + + "@babel/traverse/debug": [ + "debug@4.4.3", + "", + { + "dependencies": { "ms": "^2.1.3" }, + "peerDependencies": { "supports-color": "*" }, + "optionalPeers": [ "supports-color" ] + }, + "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + ], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": [ + "@emnapi/core@1.11.3", + "", + { + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + }, + "bundled": true + }, + "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==" + ], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": [ + "@emnapi/runtime@1.11.3", + "", + { + "dependencies": { "tslib": "^2.4.0" }, + "bundled": true + }, + "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==" + ], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": [ + "@emnapi/wasi-threads@1.2.3", + "", + { + "dependencies": { "tslib": "^2.4.0" }, + "bundled": true + }, + "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==" + ], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": [ + "@napi-rs/wasm-runtime@1.2.3", + "", + { + "dependencies": { "@tybys/wasm-util": "^0.10.3" }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + }, + "bundled": true + }, + "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==" + ], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": [ + "@tybys/wasm-util@0.10.3", + "", + { + "dependencies": { "tslib": "^2.4.0" }, + "bundled": true + }, + "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==" + ], + + "@tailwindcss/oxide-wasm32-wasi/tslib": [ + "tslib@2.8.1", + "", + { "bundled": true }, + "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + ], + + "@types/body-parser/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "@types/connect/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "@types/express-serve-static-core/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "@types/send/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "@types/serve-static/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "@types/serve-static/@types/send": [ + "@types/send@0.17.6", + "", + { + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==" + ], + + "agent-base/debug": [ + "debug@4.4.3", + "", + { + "dependencies": { "ms": "^2.1.3" }, + "peerDependencies": { "supports-color": "*" }, + "optionalPeers": [ "supports-color" ] + }, + "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + ], + + "gaxios/https-proxy-agent": [ + "https-proxy-agent@7.0.6", + "", + { + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==" + ], + + "https-proxy-agent/debug": [ + "debug@4.4.3", + "", + { + "dependencies": { "ms": "^2.1.3" }, + "peerDependencies": { "supports-color": "*" }, + "optionalPeers": [ "supports-color" ] + }, + "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + ], + + "protobufjs/@types/node": [ + "@types/node@26.4.1", + "", + { "dependencies": { "undici-types": "~8.3.0" } }, + "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==" + ], + + "react-router/cookie": [ + "cookie@1.1.1", + "", + {}, + "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==" + ], + + "send/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + + "tsx/esbuild": [ + "esbuild@0.28.2", + "", + { + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + }, + "bin": { "esbuild": "bin/esbuild" } + }, + "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==" + ], + + "@babel/core/debug/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + + "@babel/traverse/debug/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + + "@types/body-parser/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "@types/connect/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "@types/express-serve-static-core/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "@types/send/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "@types/serve-static/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "agent-base/debug/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + + "gaxios/https-proxy-agent/agent-base": [ + "agent-base@7.1.4", + "", + {}, + "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" + ], + + "gaxios/https-proxy-agent/debug": [ + "debug@4.4.3", + "", + { + "dependencies": { "ms": "^2.1.3" }, + "peerDependencies": { "supports-color": "*" }, + "optionalPeers": [ "supports-color" ] + }, + "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + ], + + "https-proxy-agent/debug/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + + "protobufjs/@types/node/undici-types": [ + "undici-types@8.3.0", + "", + {}, + "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + ], + + "tsx/esbuild/@esbuild/aix-ppc64": [ + "@esbuild/aix-ppc64@0.28.2", + "", + { + "os": "aix", + "cpu": "ppc64" + }, + "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==" + ], + + "tsx/esbuild/@esbuild/android-arm": [ + "@esbuild/android-arm@0.28.2", + "", + { + "os": "android", + "cpu": "arm" + }, + "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==" + ], + + "tsx/esbuild/@esbuild/android-arm64": [ + "@esbuild/android-arm64@0.28.2", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==" + ], + + "tsx/esbuild/@esbuild/android-x64": [ + "@esbuild/android-x64@0.28.2", + "", + { + "os": "android", + "cpu": "x64" + }, + "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==" + ], + + "tsx/esbuild/@esbuild/darwin-arm64": [ + "@esbuild/darwin-arm64@0.28.2", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==" + ], + + "tsx/esbuild/@esbuild/darwin-x64": [ + "@esbuild/darwin-x64@0.28.2", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==" + ], + + "tsx/esbuild/@esbuild/freebsd-arm64": [ + "@esbuild/freebsd-arm64@0.28.2", + "", + { + "os": "freebsd", + "cpu": "arm64" + }, + "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==" + ], + + "tsx/esbuild/@esbuild/freebsd-x64": [ + "@esbuild/freebsd-x64@0.28.2", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==" + ], + + "tsx/esbuild/@esbuild/linux-arm": [ + "@esbuild/linux-arm@0.28.2", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==" + ], + + "tsx/esbuild/@esbuild/linux-arm64": [ + "@esbuild/linux-arm64@0.28.2", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==" + ], + + "tsx/esbuild/@esbuild/linux-ia32": [ + "@esbuild/linux-ia32@0.28.2", + "", + { + "os": "linux", + "cpu": "ia32" + }, + "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==" + ], + + "tsx/esbuild/@esbuild/linux-loong64": [ + "@esbuild/linux-loong64@0.28.2", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==" + ], + + "tsx/esbuild/@esbuild/linux-mips64el": [ + "@esbuild/linux-mips64el@0.28.2", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==" + ], + + "tsx/esbuild/@esbuild/linux-ppc64": [ + "@esbuild/linux-ppc64@0.28.2", + "", + { + "os": "linux", + "cpu": "ppc64" + }, + "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==" + ], + + "tsx/esbuild/@esbuild/linux-riscv64": [ + "@esbuild/linux-riscv64@0.28.2", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==" + ], + + "tsx/esbuild/@esbuild/linux-s390x": [ + "@esbuild/linux-s390x@0.28.2", + "", + { + "os": "linux", + "cpu": "s390x" + }, + "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==" + ], + + "tsx/esbuild/@esbuild/linux-x64": [ + "@esbuild/linux-x64@0.28.2", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==" + ], + + "tsx/esbuild/@esbuild/netbsd-arm64": [ + "@esbuild/netbsd-arm64@0.28.2", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==" + ], + + "tsx/esbuild/@esbuild/netbsd-x64": [ + "@esbuild/netbsd-x64@0.28.2", + "", + { + "os": "none", + "cpu": "x64" + }, + "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==" + ], + + "tsx/esbuild/@esbuild/openbsd-arm64": [ + "@esbuild/openbsd-arm64@0.28.2", + "", + { + "os": "openbsd", + "cpu": "arm64" + }, + "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==" + ], + + "tsx/esbuild/@esbuild/openbsd-x64": [ + "@esbuild/openbsd-x64@0.28.2", + "", + { + "os": "openbsd", + "cpu": "x64" + }, + "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==" + ], + + "tsx/esbuild/@esbuild/openharmony-arm64": [ + "@esbuild/openharmony-arm64@0.28.2", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==" + ], + + "tsx/esbuild/@esbuild/sunos-x64": [ + "@esbuild/sunos-x64@0.28.2", + "", + { + "os": "sunos", + "cpu": "x64" + }, + "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==" + ], + + "tsx/esbuild/@esbuild/win32-arm64": [ + "@esbuild/win32-arm64@0.28.2", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==" + ], + + "tsx/esbuild/@esbuild/win32-ia32": [ + "@esbuild/win32-ia32@0.28.2", + "", + { + "os": "win32", + "cpu": "ia32" + }, + "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==" + ], + + "tsx/esbuild/@esbuild/win32-x64": [ + "@esbuild/win32-x64@0.28.2", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==" + ], + + "gaxios/https-proxy-agent/debug/ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ] + } +} diff --git a/PostFundManagement.ConsoleUI/docker-compose.yml b/PostFundManagement.ConsoleUI/docker-compose.yml new file mode 100644 index 0000000..5fa70fa --- /dev/null +++ b/PostFundManagement.ConsoleUI/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + pfm-console: + build: + context: . + dockerfile: Dockerfile + container_name: pfm-console + ports: + - "3000:3000" + environment: + # OIDC Identity Provider Configuration + - OIDC_AUTHORITY=https://sts.security.khongisa.co.za + - OIDC_AUDIENCE=pfm-api-dev + - OIDC_CLIENT_ID=pfm-scalar-dev + - OIDC_CLIENT_SECRET=secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27 + # Backend API Configuration + - API_BASE_URL=https://api.ifri.khongisa.co.za/ + restart: unless-stopped diff --git a/PostFundManagement.ConsoleUI/docker-entrypoint.sh b/PostFundManagement.ConsoleUI/docker-entrypoint.sh new file mode 100644 index 0000000..8959ebc --- /dev/null +++ b/PostFundManagement.ConsoleUI/docker-entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +exec "$@" diff --git a/PostFundManagement.ConsoleUI/index.html b/PostFundManagement.ConsoleUI/index.html new file mode 100644 index 0000000..7def44a --- /dev/null +++ b/PostFundManagement.ConsoleUI/index.html @@ -0,0 +1,18 @@ + + + + + + Post Fund Management Console + + + + + + + +
+ + + + diff --git a/PostFundManagement.ConsoleUI/metadata.json b/PostFundManagement.ConsoleUI/metadata.json new file mode 100644 index 0000000..3bd9e8b --- /dev/null +++ b/PostFundManagement.ConsoleUI/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "Post Fund Management Console", + "description": "Enterprise backoffice portal for Funders to manage portfolios, programmes, projects, grants, disbursements, and evidence workflows.", + "requestFramePermissions": [], + "majorCapabilities": [ "MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API" ] +} diff --git a/PostFundManagement.ConsoleUI/nginx.conf b/PostFundManagement.ConsoleUI/nginx.conf new file mode 100644 index 0000000..766b06d --- /dev/null +++ b/PostFundManagement.ConsoleUI/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 3000; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Gzip Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/json image/svg+xml; + + # Cache control for static assets + location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { + expires 1y; + access_log off; + add_header Cache-Control "public"; + } + + # Disable caching for appsettings.json so changes take effect immediately + location = /appsettings.json { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; + } + + # HTML5 SPA History Mode Support + location / { + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/PostFundManagement.ConsoleUI/package.json b/PostFundManagement.ConsoleUI/package.json new file mode 100644 index 0000000..44b48c1 --- /dev/null +++ b/PostFundManagement.ConsoleUI/package.json @@ -0,0 +1,42 @@ +{ + "name": "react-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3000 --host=0.0.0.0", + "build": "vite build", + "preview": "vite preview", + "clean": "rm -rf dist server.js", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@google/genai": "^2.4.0", + "@hookform/resolvers": "^5.9.1", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.102.8", + "@vitejs/plugin-react": "^5.0.4", + "axios": "^1.20.0", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "oidc-client-ts": "^3.5.0", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "react-hook-form": "^7.87.0", + "react-router-dom": "^7.18.3", + "vite": "^6.2.3", + "zod": "^4.5.4" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "esbuild": "^0.25.0", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.3", + "@types/express": "^4.17.21" + } +} diff --git a/PostFundManagement.ConsoleUI/src/App.tsx b/PostFundManagement.ConsoleUI/src/App.tsx new file mode 100644 index 0000000..740d91f --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/App.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient } from './lib/queryClient'; +import { AuthProvider, ProtectedRoute } from './auth/AuthProvider'; +import { ToastProvider } from './components/ui/Toast'; +import { AppLayout } from './components/layout/AppLayout'; +import { LandingPage } from './pages/LandingPage'; +import { ExecutiveDashboard } from './pages/ExecutiveDashboard'; +import { GovernancePage } from './pages/GovernancePage'; +import { ProjectsPage } from './pages/ProjectsPage'; +import { GrantsPage } from './pages/GrantsPage'; +import { MonitoringPage } from './pages/MonitoringPage'; +import { SettingsPage } from './pages/SettingsPage'; + +export default function App() { + return ( + + + + + + {/* Public OIDC Login / Landing Page */} + } /> + } /> + + {/* Protected Backoffice Application Shell */} + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Fallback Redirect */} + } /> + + + + + + ); +} diff --git a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx new file mode 100644 index 0000000..54fd376 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx @@ -0,0 +1,275 @@ +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { UserManager, WebStorageStateStore, User as OidcUser } from 'oidc-client-ts'; +import { Navigate, useLocation } from 'react-router-dom'; +import { getSecuritySettings, loadRuntimeConfig } from '../config'; +import { parseJwtClaims } from './jwt'; + +export interface AuthUserProfile { + id: string; + sub: string; + name: string; + email: string; + role: string; + organisationName?: string; + avatarUrl?: string; +} + +export type UserClaims = Record; + +interface AuthContextType { + user: AuthUserProfile | null; + sub: string; + claims: UserClaims; + accessToken: string | null; + isAuthenticated: boolean; + isLoading: boolean; + login: () => Promise; + logout: () => Promise; + authError: string | null; +} + +const AuthContext = createContext(undefined); + +export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token'; +export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user'; +export const LOCAL_STORAGE_SUB_KEY = 'pfm_user_sub'; +export const LOCAL_STORAGE_CLAIMS_KEY = 'pfm_user_claims'; + +export function getCurrentUserSub(): string { + if (typeof window !== 'undefined') { + return localStorage.getItem(LOCAL_STORAGE_SUB_KEY) || ''; + } + return ''; +} + +export function getCurrentUserClaims(): UserClaims { + if (typeof window !== 'undefined') { + const raw = localStorage.getItem(LOCAL_STORAGE_CLAIMS_KEY); + if (raw) { + try { + return JSON.parse(raw); + } catch { + return {}; + } + } + } + return {}; +} + +let userManagerInstance: UserManager | null = null; + +function getUserManager(): UserManager { + if (!userManagerInstance && typeof window !== 'undefined') { + const sec = getSecuritySettings(); + const origin = window.location.origin; + + userManagerInstance = new UserManager({ + authority: sec.Authority, + client_id: sec.ClientId, + client_secret: sec.ClientSecret, + redirect_uri: `${origin}/dashboard`, + post_logout_redirect_uri: `${origin}/`, + response_type: 'code', + scope: 'openid profile roles email address pfm-api-dev', + extraQueryParams: { + audience: sec.Audience, + }, + userStore: new WebStorageStateStore({ store: window.localStorage }), + automaticSilentRenew: false, + }); + } + return userManagerInstance!; +} + +export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [sub, setSub] = useState(''); + const [claims, setClaims] = useState({}); + const [accessToken, setAccessToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [authError, setAuthError] = useState(null); + + // Initialize auth state and check for OIDC callback + useEffect(() => { + const initAuth = async () => { + try { + await loadRuntimeConfig(); + + // Check for URL query code indicating OIDC redirect callback + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('code') && urlParams.has('state')) { + const mgr = getUserManager(); + const oidcUser: OidcUser = await mgr.signinCallback(); + + if (oidcUser && oidcUser.access_token) { + const token = oidcUser.access_token; + setAccessToken(token); + localStorage.setItem(LOCAL_STORAGE_TOKEN_KEY, token); + + // Merge claims from profile and decoded JWT token + const decodedTokenClaims = parseJwtClaims(token); + const mergedClaims: UserClaims = { + ...decodedTokenClaims, + ...oidcUser.profile, + }; + + const userSub = (oidcUser.profile?.sub || mergedClaims.sub || '') as string; + setSub(userSub); + setClaims(mergedClaims); + + localStorage.setItem(LOCAL_STORAGE_SUB_KEY, userSub); + localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(mergedClaims)); + + const profile: AuthUserProfile = { + id: userSub || 'user-1', + sub: userSub, + name: oidcUser.profile.name || oidcUser.profile.preferred_username || (mergedClaims.name as string) || 'Authorized User', + email: oidcUser.profile.email || (mergedClaims.email as string) || '', + role: (oidcUser.profile.role as string) || (mergedClaims.role as string) || (Array.isArray(mergedClaims.roles) ? mergedClaims.roles[0] : 'Programme Sponsor'), + organisationName: (mergedClaims.organisation as string) || (mergedClaims.org as string) || undefined, + }; + + setUser(profile); + localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); + + // Clean up code and state from URL + window.history.replaceState({}, document.title, window.location.pathname); + setIsLoading(false); + return; + } + } + + // Check local storage for persisted session + const storedToken = localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY); + const storedUser = localStorage.getItem(LOCAL_STORAGE_USER_KEY); + let storedSub = localStorage.getItem(LOCAL_STORAGE_SUB_KEY); + const storedClaimsRaw = localStorage.getItem(LOCAL_STORAGE_CLAIMS_KEY); + + if (storedToken) { + setAccessToken(storedToken); + + let parsedClaims: UserClaims = {}; + if (storedClaimsRaw) { + try { + parsedClaims = JSON.parse(storedClaimsRaw); + } catch { } + } else { + parsedClaims = parseJwtClaims(storedToken); + localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(parsedClaims)); + } + setClaims(parsedClaims); + + if (!storedSub && parsedClaims.sub) { + storedSub = String(parsedClaims.sub); + localStorage.setItem(LOCAL_STORAGE_SUB_KEY, storedSub); + } + if (storedSub) { + setSub(storedSub); + } + + if (storedUser) { + try { + setUser(JSON.parse(storedUser)); + } catch { } + } + } + } catch (err: unknown) { + console.warn('OIDC initialization notice:', err); + setAuthError(err instanceof Error ? err.message : 'Auth initialization notice'); + } finally { + setIsLoading(false); + } + }; + + initAuth(); + }, []); + + const login = async () => { + try { + setAuthError(null); + const mgr = getUserManager(); + await mgr.signinRedirect(); + } catch (err: unknown) { + console.warn('OIDC Redirect encountered:', err); + // In sandboxed preview iframe, navigation to third-party STS domain can be blocked by CSP/frame-ancestors. + if (typeof window !== 'undefined' && window.self !== window.top) { + const errorMsg = + 'Identity Provider redirect was restricted inside this preview frame. For testing with Duende STS (https://sts.security.khongisa.co.za), open the application in a new tab or Docker container.'; + setAuthError(errorMsg); + } else { + setAuthError(err instanceof Error ? err.message : 'Identity Provider redirect error'); + } + } + }; + + const logout = async () => { + try { + localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY); + localStorage.removeItem(LOCAL_STORAGE_USER_KEY); + localStorage.removeItem(LOCAL_STORAGE_SUB_KEY); + localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY); + setUser(null); + setSub(''); + setClaims({}); + setAccessToken(null); + + try { + const mgr = getUserManager(); + await mgr.signoutRedirect(); + } catch { + // Local state cleared + } + } catch (err) { + console.error('Logout error:', err); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +// Route Guard +export const ProtectedRoute: React.FC<{ children: ReactNode }> = ({ children }) => { + const { isAuthenticated, isLoading } = useAuth(); + const location = useLocation(); + + if (isLoading) { + return ( +
+
+
+

Verifying PFM Console Session...

+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +}; diff --git a/PostFundManagement.ConsoleUI/src/auth/jwt.ts b/PostFundManagement.ConsoleUI/src/auth/jwt.ts new file mode 100644 index 0000000..9c545cb --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/auth/jwt.ts @@ -0,0 +1,23 @@ +/** + * Lightweight browser-safe JWT payload decoder without external native dependencies. + */ +export function parseJwtClaims(token: string): Record { + if (!token || typeof token !== 'string') return {}; + try { + const parts = token.split('.'); + if (parts.length < 2) return {}; + const base64Url = parts[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + return JSON.parse(jsonPayload); + } catch (err) { + console.debug('Failed to decode JWT claims payload:', err); + return {}; + } +} diff --git a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..ffe3385 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx @@ -0,0 +1,279 @@ +import React, { useState } from 'react'; +import { NavLink, Outlet, useNavigate, Link } from 'react-router-dom'; +import { + LayoutDashboard, + Building2, + FolderKanban, + Coins, + ShieldAlert, + Settings, + LogOut, + Menu, + X, + ChevronRight, + ShieldCheck, + Layers, + Database, + Home, + User, +} from 'lucide-react'; +import { useAuth } from '../../auth/AuthProvider'; + +const navItems = [ + { + name: 'Executive Dashboard', + path: '/dashboard', + icon: LayoutDashboard, + }, + { + name: 'Governance & Structure', + path: '/governance', + icon: Building2, + }, + { + name: 'Projects & Milestones', + path: '/projects', + icon: FolderKanban, + }, + { + name: 'Grant & Financials', + path: '/grants', + icon: Coins, + }, + { + name: 'M&E, Evidence & Risks', + path: '/monitoring', + icon: ShieldAlert, + }, + { + name: 'Settings & Communications', + path: '/settings', + icon: Settings, + }, +]; + +export const AppLayout: React.FC = () => { + const { user, claims, sub, logout } = useAuth(); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [userMenuOpen, setUserMenuOpen] = useState(false); + const navigate = useNavigate(); + + const handleLogout = async () => { + await logout(); + }; + + // Extract name and email directly from validated token claims + const tokenUserName = + user?.name || + (claims.name as string) || + (claims.preferred_username as string) || + (claims.given_name ? `${claims.given_name} ${claims.family_name || ''}`.trim() : '') || + 'Authorized User'; + + const tokenUserEmail = + user?.email || + (claims.email as string) || + (claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) || + (claims.upn as string) || + (sub ? `sub:${sub}` : ''); + + return ( +
+ {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Top Header */} +
+
+ {/* Clickable Home Link */} + + + Home + + + + +
+ Tenant: + + + Department of Science, Innovation & Technology (DSI) + +
+ + + + + + Authenticated Session + +
+ + {/* User Name & Email derived from token, plus Identity Server signout */} +
+
+

+ {tokenUserName} +

+

+ {tokenUserEmail} +

+
+ + + + {/* Dropdown Menu */} + {userMenuOpen && ( +
setUserMenuOpen(false)} + > +
+

{tokenUserName}

+

{tokenUserEmail}

+ {sub &&

sub: {sub}

} +
+ + setUserMenuOpen(false)} + className="flex items-center gap-2 px-3 py-2 text-xs text-slate-300 hover:text-white hover:bg-slate-800/60 rounded-xl transition" + > + + Go to Home Page + + + +
+ )} +
+
+ + {/* Viewport with Landing Page ambient dot grid and glow */} +
+
+
+
+ +
+ +
+
+ + {/* Bottom Status Footer */} +
+ Project Financial Management (PFM) System + Duende STS OIDC Secure Session +
+
+
+ ); +}; diff --git a/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx new file mode 100644 index 0000000..a3bd905 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx @@ -0,0 +1,67 @@ +import React, { ReactNode, useEffect } from 'react'; +import { X } from 'lucide-react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + subtitle?: string; + children: ReactNode; + maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; +} + +export const Modal: React.FC = ({ + isOpen, + onClose, + title, + subtitle, + children, + maxWidth = 'lg', +}) => { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + if (isOpen) { + document.body.style.overflow = 'hidden'; + window.addEventListener('keydown', handleKeyDown); + } + return () => { + document.body.style.overflow = 'unset'; + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + const maxWidthClass = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + '2xl': 'max-w-2xl', + }[maxWidth]; + + return ( +
+
e.stopPropagation()} + > +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ +
+
{children}
+
+
+ ); +}; diff --git a/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx b/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx new file mode 100644 index 0000000..90fe50d --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/components/ui/Toast.tsx @@ -0,0 +1,79 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; +import { CheckCircle2, AlertCircle, Info, X } from 'lucide-react'; + +export type ToastType = 'success' | 'error' | 'info'; + +interface ToastItem { + id: string; + type: ToastType; + title: string; + message?: string; +} + +interface ToastContextType { + toast: (type: ToastType, title: string, message?: string) => void; + success: (title: string, message?: string) => void; + error: (title: string, message?: string) => void; + info: (title: string, message?: string) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + + const addToast = (type: ToastType, title: string, message?: string) => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, type, title, message }]); + setTimeout(() => removeToast(id), 5000); + }; + + const success = (title: string, message?: string) => addToast('success', title, message); + const error = (title: string, message?: string) => addToast('error', title, message); + const info = (title: string, message?: string) => addToast('info', title, message); + + return ( + + {children} +
+ {toasts.map((t) => ( +
+ {t.type === 'success' && } + {t.type === 'error' && } + {t.type === 'info' && } +
+

{t.title}

+ {t.message &&

{t.message}

} +
+ +
+ ))} +
+
+ ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; diff --git a/PostFundManagement.ConsoleUI/src/config.ts b/PostFundManagement.ConsoleUI/src/config.ts new file mode 100644 index 0000000..8167c59 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/config.ts @@ -0,0 +1,43 @@ +export interface SecuritySettings { + Authority: string; + Audience: string; + ClientId: string; + ClientSecret: string; +} + +export interface ApiSettings { + BaseUrl: string; +} + +export interface AppConfig { + SecuritySettings: SecuritySettings; + ApiSettings: ApiSettings; +} + +export const defaultSecuritySettings: SecuritySettings = { + Authority: (import.meta.env.VITE_OIDC_AUTHORITY as string) || 'https://sts.security.khongisa.co.za', + Audience: (import.meta.env.VITE_OIDC_AUDIENCE as string) || 'pfm-api-dev', + ClientId: (import.meta.env.VITE_OIDC_CLIENT_ID as string) || 'pfm-scalar-dev', + ClientSecret: (import.meta.env.VITE_OIDC_CLIENT_SECRET as string) || 'secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27', +}; + +export const defaultApiSettings: ApiSettings = { + BaseUrl: (import.meta.env.VITE_API_BASE_URL as string) || 'https://api.ifri.khongisa.co.za/', +}; + +const runtimeConfig: AppConfig = { + SecuritySettings: defaultSecuritySettings, + ApiSettings: defaultApiSettings, +}; + +export async function loadRuntimeConfig(): Promise { + return runtimeConfig; +} + +export function getSecuritySettings(): SecuritySettings { + return runtimeConfig.SecuritySettings; +} + +export function getApiBaseUrl(): string { + return runtimeConfig.ApiSettings.BaseUrl; +} diff --git a/PostFundManagement.ConsoleUI/src/index.css b/PostFundManagement.ConsoleUI/src/index.css new file mode 100644 index 0000000..d430f1f --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/index.css @@ -0,0 +1,256 @@ +@import "tailwindcss"; + +@layer base { + :root { + color-scheme: dark; + } + + body { + background-color: #0a0a0a; + color: #e5e7eb; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + } + + /* Form control color scheme */ + input, select, textarea { + color-scheme: dark; + } + + /* Custom dark scrollbar styling */ + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + ::-webkit-scrollbar-track { + background: #0a0a0a; + } + + ::-webkit-scrollbar-thumb { + background: #262626; + border-radius: 9999px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #3f3f46; + } +} + +/* High Contrast Dark Theme Adaptations for existing utility classes */ +.bg-white { + background-color: #111111 !important; +} + +.bg-slate-50, +.bg-slate-50\/50, +.bg-slate-50\/60, +.bg-slate-50\/75, +.bg-slate-50\/80, +.bg-slate-50\/90 { + background-color: #161616 !important; +} + +.bg-slate-100, +.bg-slate-200 { + background-color: #202020 !important; +} + +.border-slate-100 { + border-color: #222222 !important; +} + +.border-slate-200 { + border-color: #2d2d2d !important; +} + +.border-slate-300 { + border-color: #3d3d3d !important; +} + +.divide-slate-100 > :not([hidden]) ~ :not([hidden]), +.divide-slate-200 > :not([hidden]) ~ :not([hidden]) { + border-color: #242424 !important; +} + +/* Text contrast enhancements */ +.text-slate-900 { + color: #ffffff !important; +} + +.text-slate-800 { + color: #f8fafc !important; +} + +.text-slate-700 { + color: #f1f5f9 !important; +} + +.text-slate-600 { + color: #e2e8f0 !important; +} + +.text-slate-500 { + color: #cbd5e1 !important; +} + +.text-slate-400 { + color: #94a3b8 !important; +} + +.text-gray-900 { + color: #ffffff !important; +} + +.text-gray-800 { + color: #f8fafc !important; +} + +.text-gray-700 { + color: #f1f5f9 !important; +} + +.text-gray-600 { + color: #e2e8f0 !important; +} + +.text-gray-500 { + color: #cbd5e1 !important; +} + +.text-gray-400 { + color: #94a3b8 !important; +} + +/* High-contrast status badges and notification banners */ +.bg-emerald-50, +.bg-emerald-50\/40, +.bg-emerald-50\/60 { + background-color: rgba(16, 185, 129, 0.16) !important; +} + +.text-emerald-700, +.text-emerald-800, +.text-emerald-900 { + color: #6ee7b7 !important; +} + +.border-emerald-200, +.border-emerald-300 { + border-color: rgba(110, 231, 183, 0.35) !important; +} + +.bg-amber-50, +.bg-amber-50\/40, +.bg-amber-50\/60 { + background-color: rgba(245, 158, 11, 0.16) !important; +} + +.text-amber-700, +.text-amber-800, +.text-amber-900 { + color: #fcd34d !important; +} + +.border-amber-200, +.border-amber-300 { + border-color: rgba(252, 211, 77, 0.35) !important; +} + +.bg-rose-50, +.bg-rose-50\/40, +.bg-rose-50\/60 { + background-color: rgba(244, 63, 94, 0.16) !important; +} + +.text-rose-700, +.text-rose-800, +.text-rose-900 { + color: #fda4af !important; +} + +.border-rose-200, +.border-rose-300 { + border-color: rgba(253, 164, 175, 0.35) !important; +} + +.bg-purple-50, +.bg-indigo-50 { + background-color: rgba(139, 92, 246, 0.16) !important; +} + +.text-purple-700, +.text-purple-800, +.text-indigo-700, +.text-indigo-800 { + color: #d8b4fe !important; +} + +.border-purple-200, +.border-purple-300, +.border-indigo-200, +.border-indigo-300 { + border-color: rgba(216, 180, 254, 0.35) !important; +} + +.bg-sky-50, +.bg-blue-50 { + background-color: rgba(59, 130, 246, 0.16) !important; +} + +.text-sky-700, +.text-sky-800, +.text-blue-700, +.text-blue-800 { + color: #93c5fd !important; +} + +.border-sky-200, +.border-sky-300, +.border-blue-200, +.border-blue-300 { + border-color: rgba(147, 197, 253, 0.35) !important; +} + +/* Form inputs, textareas, and select controls */ +input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]), +select, +textarea { + background-color: #141414 !important; + color: #ffffff !important; + border-color: #383838 !important; +} + + input::placeholder, + textarea::placeholder { + color: #94a3b8 !important; + } + +/* Table header text high contrast */ +th { + color: #cbd5e1 !important; +} + +.hover\:bg-slate-50:hover, +.hover\:bg-slate-50\/80:hover, +.hover\:bg-slate-100:hover { + background-color: #1f1f1f !important; +} + +.hover\:text-slate-800:hover, +.hover\:text-slate-900:hover { + color: #ffffff !important; +} + +/* Primary actions matching the clean blue/indigo accent */ +.bg-emerald-600 { + background-color: #2563eb !important; +} + +.hover\:bg-emerald-500:hover, +.hover\:bg-emerald-700:hover { + background-color: #1d4ed8 !important; +} + +.border-emerald-600 { + border-color: #3b82f6 !important; +} diff --git a/PostFundManagement.ConsoleUI/src/lib/formatters.ts b/PostFundManagement.ConsoleUI/src/lib/formatters.ts new file mode 100644 index 0000000..91ee784 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/lib/formatters.ts @@ -0,0 +1,898 @@ +import { + ApprovalStatus, + ContractStatus, + ActivityStatus, + Priority, + OrganisationType, + IndustrySector, + ProjectType, + UnitOfMeasure, + EvidenceType, + RiskLikelihood, + RiskImpact, + NotificationPlatform, + NotificationStatus, + EmailStatuses, +} from '../types'; + +export const formatZAR = (amount: number): string => { + return new Intl.NumberFormat('en-ZA', { + style: 'currency', + currency: 'ZAR', + maximumFractionDigits: 0, + }).format(amount); +}; + +export const formatCompactZAR = (amount: number): string => { + if (amount >= 1_000_000_000) { + return `R ${(amount / 1_000_000_000).toFixed(2)}bn`; + } + if (amount >= 1_000_000) { + return `R ${(amount / 1_000_000).toFixed(1)}m`; + } + if (amount >= 1_000) { + return `R ${(amount / 1_000).toFixed(0)}k`; + } + return formatZAR(amount); +}; + +export const formatDate = (dateStr?: string | null): string => { + if (!dateStr) return '—'; + try { + const d = new Date(dateStr); + return d.toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + }); + } catch { + return dateStr; + } +}; + +// ========================================== +// Dropdown Select Options with Integer Values +// ========================================== + +export const EMAIL_STATUS_OPTIONS = [ + { value: EmailStatuses.GeneralError, label: 'General Error' }, + { value: EmailStatuses.AuthenticationError, label: 'Authentication Error' }, + { value: EmailStatuses.ProtocolError, label: 'Protocol Error' }, + { value: EmailStatuses.Connected, label: 'Connected' }, + { value: EmailStatuses.Disconnected, label: 'Disconnected' }, + { value: EmailStatuses.TooManyConnections, label: 'Too Many Connections' }, + { value: EmailStatuses.ConnectionAborted, label: 'Connection Aborted' }, + { value: EmailStatuses.Success, label: 'Success' }, +]; + +export const EMAIL_STATUSES_OPTIONS = EMAIL_STATUS_OPTIONS; + +export const NOTIFICATION_STATUS_OPTIONS = [ + { value: NotificationStatus.Pending, label: 'Pending' }, + { value: NotificationStatus.Queued, label: 'Queued' }, + { value: NotificationStatus.Processing, label: 'Processing' }, + { value: NotificationStatus.Sent, label: 'Sent' }, + { value: NotificationStatus.Delivered, label: 'Delivered' }, + { value: NotificationStatus.Read, label: 'Read' }, + { value: NotificationStatus.Failed, label: 'Failed' }, + { value: NotificationStatus.Cancelled, label: 'Cancelled' }, + { value: NotificationStatus.Bounced, label: 'Bounced' }, +]; + +export const NOTIFICATION_PLATFORM_OPTIONS = [ + { value: NotificationPlatform.Email, label: 'Email' }, + { value: NotificationPlatform.SMS, label: 'SMS' }, + { value: NotificationPlatform.WhatsApp, label: 'WhatsApp' }, + { value: NotificationPlatform.PushNotification, label: 'Push Notification' }, + { value: NotificationPlatform.InApp, label: 'In App' }, + { value: NotificationPlatform.Teams, label: 'Teams' }, + { value: NotificationPlatform.Slack, label: 'Slack' }, + { value: NotificationPlatform.Webhook, label: 'Webhook' }, + { value: NotificationPlatform.Other, label: 'Other' }, +]; + +export const EVIDENCE_TYPE_OPTIONS = [ + { value: EvidenceType.AttendanceRegister, label: 'Attendance Register' }, + { value: EvidenceType.BankStatement, label: 'Bank Statement' }, + { value: EvidenceType.CompletionCertificate, label: 'Completion Certificate' }, + { value: EvidenceType.FinancialInvoiceOrReceipt, label: 'Financial Invoice Or Receipt' }, + { value: EvidenceType.GeotaggedPhoto, label: 'Geotagged Photo' }, + { value: EvidenceType.InspectionReport, label: 'Inspection Report' }, + { value: EvidenceType.MeetingMinutes, label: 'Meeting Minutes' }, + { value: EvidenceType.PayrollOrStipendRegister, label: 'Payroll Or Stipend Register' }, + { value: EvidenceType.ProcurementDocument, label: 'Procurement Document' }, + { value: EvidenceType.TechnicalSpecificationOrDesign, label: 'Technical Specification Or Design' }, + { value: EvidenceType.VideoRecording, label: 'Video Recording' }, + { value: EvidenceType.Other, label: 'Other' }, +]; + +export const PROJECT_TYPE_OPTIONS = [ + { value: ProjectType.CapacityBuildingAndTraining, label: 'Capacity Building And Training' }, + { value: ProjectType.CapitalExpenditureAndInfrastructure, label: 'Capital Expenditure And Infrastructure' }, + { value: ProjectType.CommercializationAndScaling, label: 'Commercialization And Scaling' }, + { value: ProjectType.FeasibilityAndTechnicalAssistance, label: 'Feasibility And Technical Assistance' }, + { value: ProjectType.OperationalGrantAndSubsidy, label: 'Operational Grant And Subsidy' }, + { value: ProjectType.ProductDevelopmentAndPrototyping, label: 'Product Development And Prototyping' }, + { value: ProjectType.ResearchAndDevelopment, label: 'Research And Development' }, + { value: ProjectType.SocialImpactAndCommunityDevelopment, label: 'Social Impact And Community Development' }, + { value: ProjectType.TechnologyTransferAndAdoption, label: 'Technology Transfer And Adoption' }, + { value: ProjectType.Innovation, label: 'Innovation' }, + { value: ProjectType.Other, label: 'Other' }, +]; + +export const INDUSTRY_SECTOR_OPTIONS = [ + { value: IndustrySector.AgricultureAndAgroProcessing, label: 'Agriculture And Agro Processing' }, + { value: IndustrySector.AutomotiveAndTransportEquipment, label: 'Automotive And Transport Equipment' }, + { value: IndustrySector.ChemicalsAndPharmaceuticals, label: 'Chemicals And Pharmaceuticals' }, + { value: IndustrySector.ConstructionAndInfrastructure, label: 'Construction And Infrastructure' }, + { value: IndustrySector.CreativeIndustriesAndMedia, label: 'Creative Industries And Media' }, + { value: IndustrySector.DefenseAndAerospace, label: 'Defense And Aerospace' }, + { value: IndustrySector.EducationAndSkillsDevelopment, label: 'Education And Skills Development' }, + { value: IndustrySector.EnergyAndUtilities, label: 'Energy And Utilities' }, + { value: IndustrySector.FinancialServices, label: 'Financial Services' }, + { value: IndustrySector.HealthcareAndLifeSciences, label: 'Healthcare And Life Sciences' }, + { value: IndustrySector.InformationCommunicationTechnology, label: 'Information Communication Technology' }, + { value: IndustrySector.ManufacturingAndIndustrial, label: 'Manufacturing And Industrial' }, + { value: IndustrySector.MiningAndMetals, label: 'Mining And Metals' }, + { value: IndustrySector.RealEstateAndHousing, label: 'Real Estate And Housing' }, + { value: IndustrySector.RetailAndConsumerGoods, label: 'Retail And Consumer Goods' }, + { value: IndustrySector.TourismAndHospitality, label: 'Tourism And Hospitality' }, + { value: IndustrySector.WaterAndSanitation, label: 'Water And Sanitation' }, + { value: IndustrySector.Other, label: 'Other' }, +]; + +export const ORGANISATION_TYPE_OPTIONS = [ + { value: OrganisationType.NonGovernmentalOrganisation, label: 'Non-Governmental Organisation' }, + { value: OrganisationType.CivilSocietyOrganisation, label: 'Civil Society Organisation' }, + { value: OrganisationType.Trust, label: 'Trust' }, + { value: OrganisationType.FaithBasedOrganisation, label: 'Faith-Based Organisation' }, + { value: OrganisationType.PrivateLimitedCompany, label: 'Private Limited Company' }, + { value: OrganisationType.PublicLimitedCompany, label: 'Public Limited Company' }, + { value: OrganisationType.SoleProprietorship, label: 'Sole Proprietorship' }, + { value: OrganisationType.Partnership, label: 'Partnership' }, + { value: OrganisationType.SocialEnterprise, label: 'Social Enterprise' }, + { value: OrganisationType.NationalGovernmentMinistry, label: 'National Government Ministry' }, + { value: OrganisationType.ProvincialGovernmentEntity, label: 'Provincial Government Entity' }, + { value: OrganisationType.LocalGovernmentMunicipality, label: 'Local Government Municipality' }, + { value: OrganisationType.StateOwnedEnterprise, label: 'State-Owned Enterprise' }, + { value: OrganisationType.PublicAgency, label: 'Public Agency' }, + { value: OrganisationType.PublicUniversity, label: 'Public University' }, + { value: OrganisationType.PrivateUniversity, label: 'Private University' }, + { value: OrganisationType.ResearchInstitute, label: 'Research Institute' }, + { value: OrganisationType.TechnicalVocationalCollege, label: 'Technical Vocational College' }, + { value: OrganisationType.Cooperative, label: 'Cooperative' }, + { value: OrganisationType.MicrofinanceInstitution, label: 'Microfinance Institution' }, + { value: OrganisationType.CommercialBank, label: 'Commercial Bank' }, + { value: OrganisationType.DevelopmentFinanceInstitution, label: 'Development Finance Institution' }, + { value: OrganisationType.UnitedNationsAgency, label: 'United Nations Agency' }, + { value: OrganisationType.IntergovernmentalOrganisation, label: 'Intergovernmental Organisation' }, + { value: OrganisationType.InternationalNGO, label: 'International NGO' }, + { value: OrganisationType.Consortium, label: 'Consortium' }, + { value: OrganisationType.SpecialPurposeVehicle, label: 'Special Purpose Vehicle' }, +]; + +export const PRIORITY_OPTIONS = [ + { value: Priority.Low, label: 'Low' }, + { value: Priority.Medium, label: 'Medium' }, + { value: Priority.High, label: 'High' }, + { value: Priority.Critical, label: 'Critical' }, +]; + +export const UNIT_OF_MEASURE_OPTIONS = [ + { value: UnitOfMeasure.Count, label: 'Count' }, + { value: UnitOfMeasure.Percentage, label: 'Percentage' }, + { value: UnitOfMeasure.Currency, label: 'Currency' }, + { value: UnitOfMeasure.Area, label: 'Area' }, + { value: UnitOfMeasure.Weight, label: 'Weight' }, + { value: UnitOfMeasure.Volume, label: 'Volume' }, + { value: UnitOfMeasure.DurationDays, label: 'Duration (Days)' }, + { value: UnitOfMeasure.DurationMonths, label: 'Duration (Months)' }, + { value: UnitOfMeasure.Ratio, label: 'Ratio' }, +]; + +export const CONTRACT_STATUS_OPTIONS = [ + { value: ContractStatus.Draft, label: 'Draft' }, + { value: ContractStatus.PendingApproval, label: 'Pending Approval' }, + { value: ContractStatus.Active, label: 'Active' }, + { value: ContractStatus.Expired, label: 'Expired' }, + { value: ContractStatus.Terminated, label: 'Terminated' }, + { value: ContractStatus.Amended, label: 'Amended' }, +]; + +export const APPROVAL_STATUS_OPTIONS = [ + { value: ApprovalStatus.Pending, label: 'Pending' }, + { value: ApprovalStatus.Review, label: 'Review' }, + { value: ApprovalStatus.Approved, label: 'Approved' }, + { value: ApprovalStatus.Rejected, label: 'Rejected' }, + { value: ApprovalStatus.Removed, label: 'Removed' }, +]; + +export const ACTIVITY_STATUS_OPTIONS = [ + { value: ActivityStatus.Active, label: 'Active' }, + { value: ActivityStatus.Inactive, label: 'Inactive' }, + { value: ActivityStatus.Disabled, label: 'Disabled' }, +]; + +export const RISK_LIKELIHOOD_OPTIONS = [ + { value: RiskLikelihood.Rare, label: 'Rare' }, + { value: RiskLikelihood.Unlikely, label: 'Unlikely' }, + { value: RiskLikelihood.Possible, label: 'Possible' }, + { value: RiskLikelihood.Likely, label: 'Likely' }, + { value: RiskLikelihood.AlmostCertain, label: 'Almost Certain' }, +]; + +export const RISK_IMPACT_OPTIONS = [ + { value: RiskImpact.Minor, label: 'Minor' }, + { value: RiskImpact.Moderate, label: 'Moderate' }, + { value: RiskImpact.Major, label: 'Major' }, + { value: RiskImpact.Critical, label: 'Critical' }, + { value: RiskImpact.Severe, label: 'Severe' }, +]; + +// ========================================== +// Status Badges & Labels +// ========================================== + +export const getApprovalStatusBadge = (status: ApprovalStatus | number) => { + const s = Number(status); + switch (s) { + case ApprovalStatus.Approved: + return { + label: 'Approved', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + dot: 'bg-emerald-500', + }; + case ApprovalStatus.Review: + return { + label: 'Review', + className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold', + dot: 'bg-amber-500', + }; + case ApprovalStatus.Rejected: + return { + label: 'Rejected', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + dot: 'bg-rose-500', + }; + case ApprovalStatus.Removed: + return { + label: 'Removed', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + dot: 'bg-slate-500', + }; + case ApprovalStatus.Pending: + default: + return { + label: 'Pending', + className: 'bg-blue-500/10 text-blue-400 border-blue-500/30 font-semibold', + dot: 'bg-blue-500', + }; + } +}; + +export const getContractStatusBadge = (status: ContractStatus | number) => { + const s = Number(status); + switch (s) { + case ContractStatus.Active: + return { + label: 'Active', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + }; + case ContractStatus.PendingApproval: + return { + label: 'Pending Approval', + className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold', + }; + case ContractStatus.Amended: + return { + label: 'Amended', + className: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/30 font-semibold', + }; + case ContractStatus.Expired: + return { + label: 'Expired', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + }; + case ContractStatus.Terminated: + return { + label: 'Terminated', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + }; + case ContractStatus.Draft: + default: + return { + label: 'Draft', + className: 'bg-slate-800/80 text-slate-300 border-slate-700/80 font-semibold', + }; + } +}; + +export const getActivityStatusBadge = (status: ActivityStatus | number) => { + const s = Number(status); + switch (s) { + case ActivityStatus.Active: + return { + label: 'Active', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + }; + case ActivityStatus.Inactive: + return { + label: 'Inactive', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + }; + case ActivityStatus.Disabled: + default: + return { + label: 'Disabled', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + }; + } +}; + +export const getPriorityBadge = (priority: Priority | number) => { + const p = Number(priority); + switch (p) { + case Priority.Critical: + return { label: 'Critical', className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold' }; + case Priority.High: + return { label: 'High', className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold' }; + case Priority.Medium: + return { label: 'Medium', className: 'bg-blue-500/10 text-blue-400 border-blue-500/30 font-semibold' }; + case Priority.Low: + default: + return { label: 'Low', className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold' }; + } +}; + +export const getOrganisationTypeLabel = (type: OrganisationType | number): string => { + const opt = ORGANISATION_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Organisation'; +}; + +export const getIndustrySectorLabel = (sector: IndustrySector | number): string => { + const opt = INDUSTRY_SECTOR_OPTIONS.find((o) => o.value === Number(sector)); + return opt ? opt.label : 'Industry Sector'; +}; + +export const getProjectTypeLabel = (type: ProjectType | number): string => { + const opt = PROJECT_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Project Type'; +}; + +export const getEvidenceTypeLabel = (type: EvidenceType | number): string => { + const opt = EVIDENCE_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Evidence'; +}; + +export const getUnitOfMeasureLabel = (uom?: UnitOfMeasure | number | null): string => { + if (uom === null || uom === undefined) return 'Qty'; + const opt = UNIT_OF_MEASURE_OPTIONS.find((o) => o.value === Number(uom)); + return opt ? opt.label : 'Qty'; +}; + +export const getNotificationPlatformLabel = (platform?: NotificationPlatform | number | null): string => { + const opt = NOTIFICATION_PLATFORM_OPTIONS.find((o) => o.value === Number(platform)); + return opt ? opt.label : 'Platform'; +}; + +export const getNotificationStatusLabel = (status?: NotificationStatus | number | null): string => { + const opt = NOTIFICATION_STATUS_OPTIONS.find((o) => o.value === Number(status)); + return opt ? opt.label : 'Status'; +}; + +export const getEmailStatusLabel = (status?: EmailStatuses | number | null): string => { + const opt = EMAIL_STATUS_OPTIONS.find((o) => o.value === Number(status)); + return opt ? opt.label : 'Status'; +}; + +export const getEmailStatusBadge = (status?: EmailStatuses | number | null) => { + const s = Number(status); + switch (s) { + case EmailStatuses.Connected: + case EmailStatuses.Success: + return { label: getEmailStatusLabel(s), className: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; + case EmailStatuses.Disconnected: + return { label: getEmailStatusLabel(s), className: 'bg-slate-100 text-slate-700 border-slate-300 font-semibold' }; + default: + return { label: getEmailStatusLabel(s), className: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } +}; + +export const getNotificationStatusBadge = (status?: NotificationStatus | number | null) => { + const s = Number(status); + switch (s) { + case NotificationStatus.Delivered: + case NotificationStatus.Read: + return { label: getNotificationStatusLabel(s), className: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; + case NotificationStatus.Sent: + case NotificationStatus.Queued: + case NotificationStatus.Processing: + return { label: getNotificationStatusLabel(s), className: 'bg-sky-50 text-sky-800 border-sky-300 font-semibold' }; + case NotificationStatus.Pending: + return { label: getNotificationStatusLabel(s), className: 'bg-amber-50 text-amber-900 border-amber-300 font-semibold' }; + case NotificationStatus.Failed: + case NotificationStatus.Cancelled: + case NotificationStatus.Bounced: + default: + return { label: getNotificationStatusLabel(s), className: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } +}; + +export const getRiskScoreColor = (likelihood: RiskLikelihood | number, impact: RiskImpact | number) => { + const score = Number(likelihood) * Number(impact); + if (score >= 15) { + return { level: 'High', color: 'bg-rose-600 text-white', badge: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } + if (score >= 8) { + return { level: 'Medium', color: 'bg-amber-500 text-white', badge: 'bg-amber-50 text-amber-900 border-amber-300 font-semibold' }; + } + return { level: 'Low', color: 'bg-emerald-600 text-white', badge: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; +}; + +// ========================================== +// User to Email Resolver (UI shows only Email) +// ========================================== + +const USER_EMAIL_CACHE: Record = {}; + +export const registerUserEmail = (userId: string, email: string) => { + if (userId && email) { + USER_EMAIL_CACHE[userId] = email; + } +}; + +export const getUserEmail = (userIdOrEmail?: string | null): string => { + if (!userIdOrEmail) return '—'; + if (userIdOrEmail.includes('@')) return userIdOrEmail; + if (USER_EMAIL_CACHE[userIdOrEmail]) return USER_EMAIL_CACHE[userIdOrEmail]; + return userIdOrEmail; +}; +import { + ApprovalStatus, + ContractStatus, + ActivityStatus, + Priority, + OrganisationType, + IndustrySector, + ProjectType, + UnitOfMeasure, + EvidenceType, + RiskLikelihood, + RiskImpact, + NotificationPlatform, + NotificationStatus, + EmailStatuses, +} from '../types'; + +export const formatZAR = (amount: number): string => { + return new Intl.NumberFormat('en-ZA', { + style: 'currency', + currency: 'ZAR', + maximumFractionDigits: 0, + }).format(amount); +}; + +export const formatCompactZAR = (amount: number): string => { + if (amount >= 1_000_000_000) { + return `R ${(amount / 1_000_000_000).toFixed(2)}bn`; + } + if (amount >= 1_000_000) { + return `R ${(amount / 1_000_000).toFixed(1)}m`; + } + if (amount >= 1_000) { + return `R ${(amount / 1_000).toFixed(0)}k`; + } + return formatZAR(amount); +}; + +export const formatDate = (dateStr?: string | null): string => { + if (!dateStr) return '—'; + try { + const d = new Date(dateStr); + return d.toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + }); + } catch { + return dateStr; + } +}; + +// ========================================== +// Dropdown Select Options with Integer Values +// ========================================== + +export const EMAIL_STATUS_OPTIONS = [ + { value: EmailStatuses.GeneralError, label: 'General Error' }, + { value: EmailStatuses.AuthenticationError, label: 'Authentication Error' }, + { value: EmailStatuses.ProtocolError, label: 'Protocol Error' }, + { value: EmailStatuses.Connected, label: 'Connected' }, + { value: EmailStatuses.Disconnected, label: 'Disconnected' }, + { value: EmailStatuses.TooManyConnections, label: 'Too Many Connections' }, + { value: EmailStatuses.ConnectionAborted, label: 'Connection Aborted' }, + { value: EmailStatuses.Success, label: 'Success' }, +]; + +export const EMAIL_STATUSES_OPTIONS = EMAIL_STATUS_OPTIONS; + +export const NOTIFICATION_STATUS_OPTIONS = [ + { value: NotificationStatus.Pending, label: 'Pending' }, + { value: NotificationStatus.Queued, label: 'Queued' }, + { value: NotificationStatus.Processing, label: 'Processing' }, + { value: NotificationStatus.Sent, label: 'Sent' }, + { value: NotificationStatus.Delivered, label: 'Delivered' }, + { value: NotificationStatus.Read, label: 'Read' }, + { value: NotificationStatus.Failed, label: 'Failed' }, + { value: NotificationStatus.Cancelled, label: 'Cancelled' }, + { value: NotificationStatus.Bounced, label: 'Bounced' }, +]; + +export const NOTIFICATION_PLATFORM_OPTIONS = [ + { value: NotificationPlatform.Email, label: 'Email' }, + { value: NotificationPlatform.SMS, label: 'SMS' }, + { value: NotificationPlatform.WhatsApp, label: 'WhatsApp' }, + { value: NotificationPlatform.PushNotification, label: 'Push Notification' }, + { value: NotificationPlatform.InApp, label: 'In App' }, + { value: NotificationPlatform.Teams, label: 'Teams' }, + { value: NotificationPlatform.Slack, label: 'Slack' }, + { value: NotificationPlatform.Webhook, label: 'Webhook' }, + { value: NotificationPlatform.Other, label: 'Other' }, +]; + +export const EVIDENCE_TYPE_OPTIONS = [ + { value: EvidenceType.AttendanceRegister, label: 'Attendance Register' }, + { value: EvidenceType.BankStatement, label: 'Bank Statement' }, + { value: EvidenceType.CompletionCertificate, label: 'Completion Certificate' }, + { value: EvidenceType.FinancialInvoiceOrReceipt, label: 'Financial Invoice Or Receipt' }, + { value: EvidenceType.GeotaggedPhoto, label: 'Geotagged Photo' }, + { value: EvidenceType.InspectionReport, label: 'Inspection Report' }, + { value: EvidenceType.MeetingMinutes, label: 'Meeting Minutes' }, + { value: EvidenceType.PayrollOrStipendRegister, label: 'Payroll Or Stipend Register' }, + { value: EvidenceType.ProcurementDocument, label: 'Procurement Document' }, + { value: EvidenceType.TechnicalSpecificationOrDesign, label: 'Technical Specification Or Design' }, + { value: EvidenceType.VideoRecording, label: 'Video Recording' }, + { value: EvidenceType.Other, label: 'Other' }, +]; + +export const PROJECT_TYPE_OPTIONS = [ + { value: ProjectType.CapacityBuildingAndTraining, label: 'Capacity Building And Training' }, + { value: ProjectType.CapitalExpenditureAndInfrastructure, label: 'Capital Expenditure And Infrastructure' }, + { value: ProjectType.CommercializationAndScaling, label: 'Commercialization And Scaling' }, + { value: ProjectType.FeasibilityAndTechnicalAssistance, label: 'Feasibility And Technical Assistance' }, + { value: ProjectType.OperationalGrantAndSubsidy, label: 'Operational Grant And Subsidy' }, + { value: ProjectType.ProductDevelopmentAndPrototyping, label: 'Product Development And Prototyping' }, + { value: ProjectType.ResearchAndDevelopment, label: 'Research And Development' }, + { value: ProjectType.SocialImpactAndCommunityDevelopment, label: 'Social Impact And Community Development' }, + { value: ProjectType.TechnologyTransferAndAdoption, label: 'Technology Transfer And Adoption' }, + { value: ProjectType.Innovation, label: 'Innovation' }, + { value: ProjectType.Other, label: 'Other' }, +]; + +export const INDUSTRY_SECTOR_OPTIONS = [ + { value: IndustrySector.AgricultureAndAgroProcessing, label: 'Agriculture And Agro Processing' }, + { value: IndustrySector.AutomotiveAndTransportEquipment, label: 'Automotive And Transport Equipment' }, + { value: IndustrySector.ChemicalsAndPharmaceuticals, label: 'Chemicals And Pharmaceuticals' }, + { value: IndustrySector.ConstructionAndInfrastructure, label: 'Construction And Infrastructure' }, + { value: IndustrySector.CreativeIndustriesAndMedia, label: 'Creative Industries And Media' }, + { value: IndustrySector.DefenseAndAerospace, label: 'Defense And Aerospace' }, + { value: IndustrySector.EducationAndSkillsDevelopment, label: 'Education And Skills Development' }, + { value: IndustrySector.EnergyAndUtilities, label: 'Energy And Utilities' }, + { value: IndustrySector.FinancialServices, label: 'Financial Services' }, + { value: IndustrySector.HealthcareAndLifeSciences, label: 'Healthcare And Life Sciences' }, + { value: IndustrySector.InformationCommunicationTechnology, label: 'Information Communication Technology' }, + { value: IndustrySector.ManufacturingAndIndustrial, label: 'Manufacturing And Industrial' }, + { value: IndustrySector.MiningAndMetals, label: 'Mining And Metals' }, + { value: IndustrySector.RealEstateAndHousing, label: 'Real Estate And Housing' }, + { value: IndustrySector.RetailAndConsumerGoods, label: 'Retail And Consumer Goods' }, + { value: IndustrySector.TourismAndHospitality, label: 'Tourism And Hospitality' }, + { value: IndustrySector.WaterAndSanitation, label: 'Water And Sanitation' }, + { value: IndustrySector.Other, label: 'Other' }, +]; + +export const ORGANISATION_TYPE_OPTIONS = [ + { value: OrganisationType.NonGovernmentalOrganisation, label: 'Non-Governmental Organisation' }, + { value: OrganisationType.CivilSocietyOrganisation, label: 'Civil Society Organisation' }, + { value: OrganisationType.Trust, label: 'Trust' }, + { value: OrganisationType.FaithBasedOrganisation, label: 'Faith-Based Organisation' }, + { value: OrganisationType.PrivateLimitedCompany, label: 'Private Limited Company' }, + { value: OrganisationType.PublicLimitedCompany, label: 'Public Limited Company' }, + { value: OrganisationType.SoleProprietorship, label: 'Sole Proprietorship' }, + { value: OrganisationType.Partnership, label: 'Partnership' }, + { value: OrganisationType.SocialEnterprise, label: 'Social Enterprise' }, + { value: OrganisationType.NationalGovernmentMinistry, label: 'National Government Ministry' }, + { value: OrganisationType.ProvincialGovernmentEntity, label: 'Provincial Government Entity' }, + { value: OrganisationType.LocalGovernmentMunicipality, label: 'Local Government Municipality' }, + { value: OrganisationType.StateOwnedEnterprise, label: 'State-Owned Enterprise' }, + { value: OrganisationType.PublicAgency, label: 'Public Agency' }, + { value: OrganisationType.PublicUniversity, label: 'Public University' }, + { value: OrganisationType.PrivateUniversity, label: 'Private University' }, + { value: OrganisationType.ResearchInstitute, label: 'Research Institute' }, + { value: OrganisationType.TechnicalVocationalCollege, label: 'Technical Vocational College' }, + { value: OrganisationType.Cooperative, label: 'Cooperative' }, + { value: OrganisationType.MicrofinanceInstitution, label: 'Microfinance Institution' }, + { value: OrganisationType.CommercialBank, label: 'Commercial Bank' }, + { value: OrganisationType.DevelopmentFinanceInstitution, label: 'Development Finance Institution' }, + { value: OrganisationType.UnitedNationsAgency, label: 'United Nations Agency' }, + { value: OrganisationType.IntergovernmentalOrganisation, label: 'Intergovernmental Organisation' }, + { value: OrganisationType.InternationalNGO, label: 'International NGO' }, + { value: OrganisationType.Consortium, label: 'Consortium' }, + { value: OrganisationType.SpecialPurposeVehicle, label: 'Special Purpose Vehicle' }, +]; + +export const PRIORITY_OPTIONS = [ + { value: Priority.Low, label: 'Low' }, + { value: Priority.Medium, label: 'Medium' }, + { value: Priority.High, label: 'High' }, + { value: Priority.Critical, label: 'Critical' }, +]; + +export const UNIT_OF_MEASURE_OPTIONS = [ + { value: UnitOfMeasure.Count, label: 'Count' }, + { value: UnitOfMeasure.Percentage, label: 'Percentage' }, + { value: UnitOfMeasure.Currency, label: 'Currency' }, + { value: UnitOfMeasure.Area, label: 'Area' }, + { value: UnitOfMeasure.Weight, label: 'Weight' }, + { value: UnitOfMeasure.Volume, label: 'Volume' }, + { value: UnitOfMeasure.DurationDays, label: 'Duration (Days)' }, + { value: UnitOfMeasure.DurationMonths, label: 'Duration (Months)' }, + { value: UnitOfMeasure.Ratio, label: 'Ratio' }, +]; + +export const CONTRACT_STATUS_OPTIONS = [ + { value: ContractStatus.Draft, label: 'Draft' }, + { value: ContractStatus.PendingApproval, label: 'Pending Approval' }, + { value: ContractStatus.Active, label: 'Active' }, + { value: ContractStatus.Expired, label: 'Expired' }, + { value: ContractStatus.Terminated, label: 'Terminated' }, + { value: ContractStatus.Amended, label: 'Amended' }, +]; + +export const APPROVAL_STATUS_OPTIONS = [ + { value: ApprovalStatus.Pending, label: 'Pending' }, + { value: ApprovalStatus.Review, label: 'Review' }, + { value: ApprovalStatus.Approved, label: 'Approved' }, + { value: ApprovalStatus.Rejected, label: 'Rejected' }, + { value: ApprovalStatus.Removed, label: 'Removed' }, +]; + +export const ACTIVITY_STATUS_OPTIONS = [ + { value: ActivityStatus.Active, label: 'Active' }, + { value: ActivityStatus.Inactive, label: 'Inactive' }, + { value: ActivityStatus.Disabled, label: 'Disabled' }, +]; + +export const RISK_LIKELIHOOD_OPTIONS = [ + { value: RiskLikelihood.Rare, label: 'Rare' }, + { value: RiskLikelihood.Unlikely, label: 'Unlikely' }, + { value: RiskLikelihood.Possible, label: 'Possible' }, + { value: RiskLikelihood.Likely, label: 'Likely' }, + { value: RiskLikelihood.AlmostCertain, label: 'Almost Certain' }, +]; + +export const RISK_IMPACT_OPTIONS = [ + { value: RiskImpact.Minor, label: 'Minor' }, + { value: RiskImpact.Moderate, label: 'Moderate' }, + { value: RiskImpact.Major, label: 'Major' }, + { value: RiskImpact.Critical, label: 'Critical' }, + { value: RiskImpact.Severe, label: 'Severe' }, +]; + +// ========================================== +// Status Badges & Labels +// ========================================== + +export const getApprovalStatusBadge = (status: ApprovalStatus | number) => { + const s = Number(status); + switch (s) { + case ApprovalStatus.Approved: + return { + label: 'Approved', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + dot: 'bg-emerald-500', + }; + case ApprovalStatus.Review: + return { + label: 'Review', + className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold', + dot: 'bg-amber-500', + }; + case ApprovalStatus.Rejected: + return { + label: 'Rejected', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + dot: 'bg-rose-500', + }; + case ApprovalStatus.Removed: + return { + label: 'Removed', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + dot: 'bg-slate-500', + }; + case ApprovalStatus.Pending: + default: + return { + label: 'Pending', + className: 'bg-blue-500/10 text-blue-400 border-blue-500/30 font-semibold', + dot: 'bg-blue-500', + }; + } +}; + +export const getContractStatusBadge = (status: ContractStatus | number) => { + const s = Number(status); + switch (s) { + case ContractStatus.Active: + return { + label: 'Active', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + }; + case ContractStatus.PendingApproval: + return { + label: 'Pending Approval', + className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold', + }; + case ContractStatus.Amended: + return { + label: 'Amended', + className: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/30 font-semibold', + }; + case ContractStatus.Expired: + return { + label: 'Expired', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + }; + case ContractStatus.Terminated: + return { + label: 'Terminated', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + }; + case ContractStatus.Draft: + default: + return { + label: 'Draft', + className: 'bg-slate-800/80 text-slate-300 border-slate-700/80 font-semibold', + }; + } +}; + +export const getActivityStatusBadge = (status: ActivityStatus | number) => { + const s = Number(status); + switch (s) { + case ActivityStatus.Active: + return { + label: 'Active', + className: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 font-semibold', + }; + case ActivityStatus.Inactive: + return { + label: 'Inactive', + className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold', + }; + case ActivityStatus.Disabled: + default: + return { + label: 'Disabled', + className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold', + }; + } +}; + +export const getPriorityBadge = (priority: Priority | number) => { + const p = Number(priority); + switch (p) { + case Priority.Critical: + return { label: 'Critical', className: 'bg-rose-500/10 text-rose-400 border-rose-500/30 font-semibold' }; + case Priority.High: + return { label: 'High', className: 'bg-amber-500/10 text-amber-400 border-amber-500/30 font-semibold' }; + case Priority.Medium: + return { label: 'Medium', className: 'bg-blue-500/10 text-blue-400 border-blue-500/30 font-semibold' }; + case Priority.Low: + default: + return { label: 'Low', className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 font-semibold' }; + } +}; + +export const getOrganisationTypeLabel = (type: OrganisationType | number): string => { + const opt = ORGANISATION_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Organisation'; +}; + +export const getIndustrySectorLabel = (sector: IndustrySector | number): string => { + const opt = INDUSTRY_SECTOR_OPTIONS.find((o) => o.value === Number(sector)); + return opt ? opt.label : 'Industry Sector'; +}; + +export const getProjectTypeLabel = (type: ProjectType | number): string => { + const opt = PROJECT_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Project Type'; +}; + +export const getEvidenceTypeLabel = (type: EvidenceType | number): string => { + const opt = EVIDENCE_TYPE_OPTIONS.find((o) => o.value === Number(type)); + return opt ? opt.label : 'Evidence'; +}; + +export const getUnitOfMeasureLabel = (uom?: UnitOfMeasure | number | null): string => { + if (uom === null || uom === undefined) return 'Qty'; + const opt = UNIT_OF_MEASURE_OPTIONS.find((o) => o.value === Number(uom)); + return opt ? opt.label : 'Qty'; +}; + +export const getNotificationPlatformLabel = (platform?: NotificationPlatform | number | null): string => { + const opt = NOTIFICATION_PLATFORM_OPTIONS.find((o) => o.value === Number(platform)); + return opt ? opt.label : 'Platform'; +}; + +export const getNotificationStatusLabel = (status?: NotificationStatus | number | null): string => { + const opt = NOTIFICATION_STATUS_OPTIONS.find((o) => o.value === Number(status)); + return opt ? opt.label : 'Status'; +}; + +export const getEmailStatusLabel = (status?: EmailStatuses | number | null): string => { + const opt = EMAIL_STATUS_OPTIONS.find((o) => o.value === Number(status)); + return opt ? opt.label : 'Status'; +}; + +export const getEmailStatusBadge = (status?: EmailStatuses | number | null) => { + const s = Number(status); + switch (s) { + case EmailStatuses.Connected: + case EmailStatuses.Success: + return { label: getEmailStatusLabel(s), className: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; + case EmailStatuses.Disconnected: + return { label: getEmailStatusLabel(s), className: 'bg-slate-100 text-slate-700 border-slate-300 font-semibold' }; + default: + return { label: getEmailStatusLabel(s), className: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } +}; + +export const getNotificationStatusBadge = (status?: NotificationStatus | number | null) => { + const s = Number(status); + switch (s) { + case NotificationStatus.Delivered: + case NotificationStatus.Read: + return { label: getNotificationStatusLabel(s), className: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; + case NotificationStatus.Sent: + case NotificationStatus.Queued: + case NotificationStatus.Processing: + return { label: getNotificationStatusLabel(s), className: 'bg-sky-50 text-sky-800 border-sky-300 font-semibold' }; + case NotificationStatus.Pending: + return { label: getNotificationStatusLabel(s), className: 'bg-amber-50 text-amber-900 border-amber-300 font-semibold' }; + case NotificationStatus.Failed: + case NotificationStatus.Cancelled: + case NotificationStatus.Bounced: + default: + return { label: getNotificationStatusLabel(s), className: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } +}; + +export const getRiskScoreColor = (likelihood: RiskLikelihood | number, impact: RiskImpact | number) => { + const score = Number(likelihood) * Number(impact); + if (score >= 15) { + return { level: 'High', color: 'bg-rose-600 text-white', badge: 'bg-rose-50 text-rose-800 border-rose-300 font-semibold' }; + } + if (score >= 8) { + return { level: 'Medium', color: 'bg-amber-500 text-white', badge: 'bg-amber-50 text-amber-900 border-amber-300 font-semibold' }; + } + return { level: 'Low', color: 'bg-emerald-600 text-white', badge: 'bg-emerald-50 text-emerald-800 border-emerald-300 font-semibold' }; +}; + +// ========================================== +// User to Email Resolver (UI shows only Email) +// ========================================== + +const USER_EMAIL_CACHE: Record = {}; + +export const registerUserEmail = (userId: string, email: string) => { + if (userId && email) { + USER_EMAIL_CACHE[userId] = email; + } +}; + +export const getUserEmail = (userIdOrEmail?: string | null): string => { + if (!userIdOrEmail) return '—'; + if (userIdOrEmail.includes('@')) return userIdOrEmail; + if (USER_EMAIL_CACHE[userIdOrEmail]) return USER_EMAIL_CACHE[userIdOrEmail]; + return userIdOrEmail; +}; diff --git a/PostFundManagement.ConsoleUI/src/lib/queryClient.ts b/PostFundManagement.ConsoleUI/src/lib/queryClient.ts new file mode 100644 index 0000000..f054a86 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/lib/queryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 3, // 3 minutes default cache + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}); diff --git a/PostFundManagement.ConsoleUI/src/main.tsx b/PostFundManagement.ConsoleUI/src/main.tsx new file mode 100644 index 0000000..2ad9aab --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx new file mode 100644 index 0000000..b7a69d6 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx @@ -0,0 +1,655 @@ +import React, { useState, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import { + TrendingUp, + AlertTriangle, + Users, + Layers, + Coins, + CheckCircle, + ArrowUpRight, + Filter, + RefreshCw, + Clock, + ShieldAlert, +} from 'lucide-react'; +import { + getDashboard, + getProjects, + getAllPortfolios, + getAllProgrammes, + getAwards, + getBudgets, + getDisbursements, + getAllRisks, +} from '../services/api'; +import { formatCompactZAR, formatZAR, getIndustrySectorLabel } from '../lib/formatters'; +import { ApprovalStatus } from '../types'; + +export const ExecutiveDashboard: React.FC = () => { + const [selectedProgramme, setSelectedProgramme] = useState('All'); + const [selectedSector, setSelectedSector] = useState('All'); + const [selectedPeriod, setSelectedPeriod] = useState('FY 2026/27'); + + // Real Queries from OpenAPI Endpoints + const { data: dashboard, isLoading: loadingDashboard, refetch: refetchDashboard, isFetching } = useQuery({ + queryKey: ['dashboard'], + queryFn: getDashboard, + }); + + const { data: projects = [] } = useQuery({ + queryKey: ['projects'], + queryFn: () => getProjects(), + }); + + const { data: portfolios = [] } = useQuery({ + queryKey: ['portfolios'], + queryFn: () => getAllPortfolios(), + }); + + const { data: programmes = [] } = useQuery({ + queryKey: ['programmes'], + queryFn: () => getAllProgrammes(), + }); + + const { data: awards = [] } = useQuery({ + queryKey: ['awards'], + queryFn: () => getAwards(), + }); + + const { data: budgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(), + }); + + const { data: disbursements = [] } = useQuery({ + queryKey: ['disbursements'], + queryFn: () => getDisbursements(), + }); + + const { data: risks = [] } = useQuery({ + queryKey: ['risks'], + queryFn: () => getAllRisks(), + }); + + const refetchAll = () => { + refetchDashboard(); + }; + + // Headline metrics sourced directly from live API models + const portfolioValue = useMemo(() => { + if (dashboard?.portfolioValue && dashboard.portfolioValue > 0) { + return dashboard.portfolioValue; + } + const budgetTotal = budgets.reduce((sum, b) => sum + (b.amount || 0), 0); + if (budgetTotal > 0) return budgetTotal; + const awardsTotal = awards.reduce((sum, a) => sum + (a.amount || 0), 0); + return awardsTotal; + }, [dashboard, budgets, awards]); + + const activeAwards = dashboard?.activeAwards ?? (awards.length || projects.length); + + const disbursed = useMemo(() => { + if (dashboard?.disbursed && dashboard.disbursed > 0) { + return dashboard.disbursed; + } + return disbursements + .filter((d) => d.status === ApprovalStatus.Approved || (d.status as number) === 3) + .reduce((sum, d) => sum + (d.amount || 0), 0); + }, [dashboard, disbursements]); + + const onTrackPercentage = useMemo(() => { + if (dashboard?.onTrackPercentage !== undefined && dashboard.onTrackPercentage > 0) { + return dashboard.onTrackPercentage; + } + if (projects.length === 0) return 0; + const activeOnTrack = projects.filter((p) => p.status === ApprovalStatus.Approved).length; + return Math.round((activeOnTrack / projects.length) * 100); + }, [dashboard, projects]); + + const atRiskCount = useMemo(() => { + if (dashboard?.atRiskCount !== undefined) { + return dashboard.atRiskCount; + } + return risks.filter((r) => r.likelihood * r.impact >= 12).length; + }, [dashboard, risks]); + + const beneficiariesReached = dashboard?.beneficiariesReached ?? 0; + + // Real sector breakdown calculated dynamically from actual project records + const sectorData = useMemo(() => { + if (!projects || projects.length === 0) return []; + const counts: Record = {}; + projects.forEach((p) => { + const label = getIndustrySectorLabel(p.sector); + counts[label] = (counts[label] || 0) + 1; + }); + + const colors = ['bg-emerald-500', 'bg-teal-500', 'bg-blue-500', 'bg-indigo-500', 'bg-purple-500', 'bg-amber-500']; + const totalProjects = projects.length; + + return Object.entries(counts).map(([name, count], index) => { + const percentage = Math.round((count / totalProjects) * 100); + const amount = portfolioValue > 0 ? Math.round((portfolioValue / totalProjects) * count) : 0; + return { + name, + count, + percentage, + amount, + color: colors[index % colors.length], + }; + }); + }, [projects, portfolioValue]); + + // Risk heatmap matrix computed directly from registered risk records + const riskHeatmap = useMemo(() => { + const matrix = { + highImpHighLik: risks.filter((r) => r.impact >= 4 && r.likelihood >= 4).length, + highImpMedLik: risks.filter((r) => r.impact >= 4 && r.likelihood === 3).length, + highImpLowLik: risks.filter((r) => r.impact >= 4 && r.likelihood <= 2).length, + + medImpHighLik: risks.filter((r) => r.impact === 3 && r.likelihood >= 4).length, + medImpMedLik: risks.filter((r) => r.impact === 3 && r.likelihood === 3).length, + medImpLowLik: risks.filter((r) => r.impact === 3 && r.likelihood <= 2).length, + + lowImpHighLik: risks.filter((r) => r.impact <= 2 && r.likelihood >= 4).length, + lowImpMedLik: risks.filter((r) => r.impact <= 2 && r.likelihood === 3).length, + lowImpLowLik: risks.filter((r) => r.impact <= 2 && r.likelihood <= 2).length, + + flaggedCount: risks.filter((r) => r.impact * r.likelihood >= 12).length, + }; + return matrix; + }, [risks]); + + // Quarterly disbursements distribution computed from real disbursement items + const quarterlyTrend = useMemo(() => { + if (!disbursements || disbursements.length === 0) { + return [ + { quarter: 'Q1', amount: 0, heightPct: 15 }, + { quarter: 'Q2', amount: 0, heightPct: 15 }, + { quarter: 'Q3', amount: 0, heightPct: 15 }, + { quarter: 'Q4', amount: 0, heightPct: 15 }, + ]; + } + + const qTotals: Record = { Q1: 0, Q2: 0, Q3: 0, Q4: 0 }; + disbursements.forEach((d) => { + const dateStr = d.createdAt || d.updatedAt; + if (dateStr) { + const month = new Date(dateStr).getMonth(); // 0-11 + if (month <= 2) qTotals.Q1 += d.amount || 0; + else if (month <= 5) qTotals.Q2 += d.amount || 0; + else if (month <= 8) qTotals.Q3 += d.amount || 0; + else qTotals.Q4 += d.amount || 0; + } else { + qTotals.Q1 += (d.amount || 0) * 0.25; + qTotals.Q2 += (d.amount || 0) * 0.25; + qTotals.Q3 += (d.amount || 0) * 0.25; + qTotals.Q4 += (d.amount || 0) * 0.25; + } + }); + + const maxQ = Math.max(...Object.values(qTotals), 1); + return Object.entries(qTotals).map(([quarter, amount]) => ({ + quarter, + amount, + heightPct: Math.max(15, Math.min(100, Math.round((amount / maxQ) * 100))), + })); + }, [disbursements]); + + // Attention Projects derived directly from live project data + const attentionProjects = useMemo(() => { + if (!projects || projects.length === 0) return []; + return projects + .filter((p) => p.status === ApprovalStatus.Rejected || p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review) + .slice(0, 5) + .map((p) => { + const isRejected = p.status === ApprovalStatus.Rejected; + const isPending = p.status === ApprovalStatus.Pending || p.status === ApprovalStatus.Review; + return { + id: p.id, + name: p.name, + programme: p.programmeName || `Programme ${p.programmeId}`, + status: isRejected ? 'At Risk' : isPending ? 'Pending' : 'Active', + risk: isRejected ? 'RED' : isPending ? 'AMBER' : 'GREEN', + financialVariance: isRejected ? '-15%' : '0%', + varianceType: isRejected ? 'negative' : 'positive', + }; + }); + }, [projects]); + + return ( +
+ {/* Page Title & Filter Bar */} +
+
+
+

+ Executive Portfolio Dashboard +

+ + Live STS Feed + +
+

+ Real-time portfolio overview, disbursement tracking, risk heatmap, and projects requiring governance action. +

+
+ + {/* Filters */} +
+
+ + Filter: +
+ + + + + + + + +
+
+ + {/* 6 Headline Portfolio KPI Cards */} +
+ {/* Card 1: Portfolio Value */} +
+
+ Portfolio Value +
+ +
+
+
+

{formatCompactZAR(portfolioValue)}

+

{formatZAR(portfolioValue)}

+
+
+ + {/* Card 2: Active Awards */} +
+
+ Active Awards +
+ +
+
+
+

{activeAwards}

+

+ Across {portfolios.length} portfolio{portfolios.length === 1 ? '' : 's'} +

+
+
+ + {/* Card 3: Disbursed */} +
+
+ Disbursed +
+ +
+
+
+

{formatCompactZAR(disbursed)}

+

+ {portfolioValue > 0 ? `${((disbursed / portfolioValue) * 100).toFixed(1)}% of commitments` : '0.0% of commitments'} +

+
+
+ + {/* Card 4: On Track */} +
+
+ On Track +
+ +
+
+
+

{onTrackPercentage}%

+

Milestones verified

+
+
+ + {/* Card 5: At Risk */} +
+
+ At Risk +
+ +
+
+
+

{atRiskCount}

+

Requires PM action

+
+
+ + {/* Card 6: Beneficiaries Reached */} +
+
+ Beneficiaries +
+ +
+
+
+

+ {beneficiariesReached >= 1000 ? `${(beneficiariesReached / 1000).toFixed(0)}k` : beneficiariesReached} +

+

Directly impacted

+
+
+
+ + {/* Visual Models Section (Sector, Heatmap, Impact Trend) */} +
+ {/* Panel 1: Funding by Sector */} +
+
+
+

Funding by Sector

+ + API Calculated + +
+ + {/* Circular Graphic Simulation */} +
+
+
+ {sectorData.length} + + {sectorData.length === 1 ? 'Sector' : 'Sectors'} + +
+
+
+ + {/* Sector Progress Bars */} +
+ {sectorData.length === 0 ? ( +

No active project sectors assigned yet.

+ ) : ( + sectorData.map((s) => ( +
+
+ {s.name} ({s.count}) + {s.percentage}% +
+
+
+
+
+ )) + )} +
+
+ +
+ Primary Focus: {sectorData[0]?.name || 'All Sectors'} + + View Portfolios + +
+
+ + {/* Panel 2: Portfolio Risk Heatmap */} +
+
+
+

Portfolio Risk Heatmap

+ + {riskHeatmap.flaggedCount} Flagged + +
+ +

+ Matrix distribution of project risks by Likelihood (1–5) and Impact (1–5). +

+ + {/* 3x3 Heatmap Matrix with real counted risks */} +
+ {/* Row 1 - High Impact */} +
+ Moderate + {riskHeatmap.highImpLowLik} {riskHeatmap.highImpLowLik === 1 ? 'Risk' : 'Risks'} +
+
+ High + {riskHeatmap.highImpMedLik} {riskHeatmap.highImpMedLik === 1 ? 'Risk' : 'Risks'} +
+
+ Critical + {riskHeatmap.highImpHighLik} {riskHeatmap.highImpHighLik === 1 ? 'Risk' : 'Risks'} +
+ + {/* Row 2 - Medium Impact */} +
+ Low + {riskHeatmap.medImpLowLik} {riskHeatmap.medImpLowLik === 1 ? 'Risk' : 'Risks'} +
+
+ Moderate + {riskHeatmap.medImpMedLik} {riskHeatmap.medImpMedLik === 1 ? 'Risk' : 'Risks'} +
+
+ High + {riskHeatmap.medImpHighLik} {riskHeatmap.medImpHighLik === 1 ? 'Risk' : 'Risks'} +
+ + {/* Row 3 - Low Impact */} +
+ Trivial + {riskHeatmap.lowImpLowLik} {riskHeatmap.lowImpLowLik === 1 ? 'Risk' : 'Risks'} +
+
+ Low + {riskHeatmap.lowImpMedLik} {riskHeatmap.lowImpMedLik === 1 ? 'Risk' : 'Risks'} +
+
+ Moderate + {riskHeatmap.lowImpHighLik} {riskHeatmap.lowImpHighLik === 1 ? 'Risk' : 'Risks'} +
+
+
+ +
+ Total Registered Risks: {risks.length} + + Risk Register + +
+
+ + {/* Panel 3: Funding-to-Impact Trend */} +
+
+
+

Disbursement Distribution

+ + {disbursements.length} Tranche{disbursements.length === 1 ? '' : 's'} + +
+ +

+ Quarterly release distribution across active portfolios and milestone verifications. +

+ + {/* Graphic Trend Visualization from Live Disbursements */} +
+ {quarterlyTrend.map((q) => ( +
+ {q.quarter} +
+
+
+ ))} +
+ +
+
+ + Commitment Allocation +
+
+ + Actual Disbursed +
+
+
+ +
+ Total Disbursed: {formatCompactZAR(disbursed)} + + Financials + +
+
+
+ + {/* Projects Requiring Attention Table */} +
+
+
+

Projects Requiring Attention

+

+ Prioritized by risk severity, delivery delay, or financial variance threshold breach. +

+
+ + Open Project Directory → + +
+ +
+ + + + + + + + + + + + + {attentionProjects.length === 0 ? ( + + + + ) : ( + attentionProjects.map((p) => ( + + + + + + + + + )) + )} + +
ProjectProgrammeStatusRiskFinancial VarianceAction
+ No active projects currently flagged for executive attention. All projects are operating within target thresholds. +
+ {p.name} + ID: {p.id} + {p.programme} + + {p.status} + + + + {p.risk} + + + + {p.financialVariance} + + + + Review + + +
+
+
+
+ ); +}; diff --git a/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx new file mode 100644 index 0000000..7f4ad33 --- /dev/null +++ b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx @@ -0,0 +1,860 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { + Building2, + Plus, + FolderKanban, + Edit2, + Mail, + Shield, + Layers, + ChevronRight, + Search, +} from 'lucide-react'; +import { + getOrganisations, + createOrganisation, + updateOrganisation, + getAllPortfolios, + createPortfolio, + updatePortfolio, + getAllProgrammes, + createProgramme, + updateProgramme, +} from '../services/api'; +import { Modal } from '../components/ui/Modal'; +import { useToast } from '../components/ui/Toast'; +import { useAuth } from '../auth/AuthProvider'; +import { + Organisation, + Portfolio, + Programme, + OrganisationType, + ActivityStatus, + ApprovalStatus, + CreateOrganisationRequest, + CreatePortfolioRequest, + CreateProgrammeRequest, +} from '../types'; +import { + getOrganisationTypeLabel, + getActivityStatusBadge, + getApprovalStatusBadge, + formatDate, + getUserEmail, + ORGANISATION_TYPE_OPTIONS, + ACTIVITY_STATUS_OPTIONS, + APPROVAL_STATUS_OPTIONS, +} from '../lib/formatters'; + +// Validation Schemas matching API constraints +const organisationSchema = z.object({ + name: z.string().min(3, 'Name must be at least 3 characters'), + registrationNo: z.string().min(4, 'Registration number is mandatory'), + email: z.string().email('Enter a valid email address'), + type: z.coerce.number().min(1).max(27), + status: z.coerce.number().optional(), +}); + +const portfolioSchema = z.object({ + name: z.string().min(3, 'Portfolio name is required'), + description: z.string().min(5, 'Description is required'), + organisationId: z.coerce.number().min(1, 'Select a parent organisation'), + ownerId: z.string().min(1, 'Owner ID is required'), +}); + +const programmeSchema = z.object({ + name: z.string().min(3, 'Programme name is required'), + portfolioId: z.coerce.number().min(1, 'Select a parent portfolio'), + status: z.coerce.number().optional(), + ownerId: z.string().min(1, 'Owner ID is required'), +}); + +type OrganisationFormValues = z.infer; +type PortfolioFormValues = z.infer; +type ProgrammeFormValues = z.infer; + +export const GovernancePage: React.FC = () => { + const queryClient = useQueryClient(); + const { sub, user } = useAuth(); + const currentUserId = sub || user?.id || ''; + const { success, error: toastError } = useToast(); + const [activeTab, setActiveTab] = useState<'organisations' | 'portfolios' | 'programmes'>('organisations'); + const [searchTerm, setSearchTerm] = useState(''); + + // Modals & Editing State + const [isOrgModalOpen, setIsOrgModalOpen] = useState(false); + const [editingOrg, setEditingOrg] = useState(null); + + const [isPortfolioModalOpen, setIsPortfolioModalOpen] = useState(false); + const [editingPortfolio, setEditingPortfolio] = useState(null); + + const [isProgrammeModalOpen, setIsProgrammeModalOpen] = useState(false); + const [editingProgramme, setEditingProgramme] = useState(null); + + // Queries + const { data: organisations = [], isLoading: loadingOrgs } = useQuery({ + queryKey: ['organisations'], + queryFn: () => getOrganisations(), + }); + + const { data: portfolios = [], isLoading: loadingPortfolios } = useQuery({ + queryKey: ['portfolios'], + queryFn: () => getAllPortfolios(), + }); + + const { data: programmes = [], isLoading: loadingProgrammes } = useQuery({ + queryKey: ['programmes'], + queryFn: () => getAllProgrammes(), + }); + + // Forms + const orgForm = useForm({ + resolver: zodResolver(organisationSchema) as any, + defaultValues: { + name: '', + registrationNo: '', + email: '', + type: OrganisationType.Enterprise, + status: ActivityStatus.Active, + }, + }); + + const portfolioForm = useForm({ + resolver: zodResolver(portfolioSchema) as any, + defaultValues: { + name: '', + description: '', + organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, + ownerId: currentUserId, + }, + }); + + const programmeForm = useForm({ + resolver: zodResolver(programmeSchema) as any, + defaultValues: { + name: '', + portfolioId: portfolios[0]?.id ? Number(portfolios[0].id) : 201, + status: ApprovalStatus.Approved, + ownerId: currentUserId, + }, + }); + + // Mutations + const orgMutation = useMutation({ + mutationFn: (data: OrganisationFormValues) => { + if (editingOrg) { + return updateOrganisation({ + organisationId: editingOrg.id, + name: data.name, + registrationNo: data.registrationNo, + email: data.email, + type: Number(data.type), + status: data.status !== undefined ? Number(data.status) : undefined, + }); + } + return createOrganisation(data as CreateOrganisationRequest); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['organisations'] }); + success(editingOrg ? 'Organisation updated' : 'Organisation registered successfully'); + setIsOrgModalOpen(false); + setEditingOrg(null); + orgForm.reset(); + }, + onError: (err: unknown) => { + toastError('Failed to save organisation', String(err)); + }, + }); + + const portfolioMutation = useMutation({ + mutationFn: (data: PortfolioFormValues) => { + if (editingPortfolio) { + return updatePortfolio({ + portfolioId: editingPortfolio.id, + name: data.name, + description: data.description, + organisationId: Number(data.organisationId), + ownerId: data.ownerId, + }); + } + return createPortfolio(data as CreatePortfolioRequest); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['portfolios'] }); + success(editingPortfolio ? 'Portfolio updated successfully' : 'Portfolio created successfully'); + setIsPortfolioModalOpen(false); + setEditingPortfolio(null); + portfolioForm.reset(); + }, + onError: (err: unknown) => { + toastError('Failed to save portfolio', String(err)); + }, + }); + + const programmeMutation = useMutation({ + mutationFn: (data: ProgrammeFormValues) => { + if (editingProgramme) { + return updateProgramme({ + programmeId: editingProgramme.id, + name: data.name, + portfolioId: Number(data.portfolioId), + status: data.status !== undefined ? Number(data.status) : undefined, + ownerId: data.ownerId, + }); + } + return createProgramme(data as CreateProgrammeRequest); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['programmes'] }); + success(editingProgramme ? 'Programme updated successfully' : 'Programme created successfully'); + setIsProgrammeModalOpen(false); + setEditingProgramme(null); + programmeForm.reset(); + }, + onError: (err: unknown) => { + toastError('Failed to save programme', String(err)); + }, + }); + + const openCreateOrg = () => { + setEditingOrg(null); + orgForm.reset({ + name: '', + registrationNo: '', + email: '', + type: OrganisationType.Enterprise, + status: ActivityStatus.Active, + }); + setIsOrgModalOpen(true); + }; + + const openEditOrg = (org: Organisation) => { + setEditingOrg(org); + orgForm.reset({ + name: org.name || '', + registrationNo: org.registrationNo || '', + email: org.email || '', + type: org.type || OrganisationType.Enterprise, + status: org.status ?? ActivityStatus.Active, + }); + setIsOrgModalOpen(true); + }; + + const openCreatePortfolio = () => { + setEditingPortfolio(null); + portfolioForm.reset({ + name: '', + description: '', + organisationId: organisations[0]?.id ? Number(organisations[0].id) : 101, + ownerId: currentUserId, + }); + setIsPortfolioModalOpen(true); + }; + + const openEditPortfolio = (p: Portfolio) => { + setEditingPortfolio(p); + portfolioForm.reset({ + name: p.name || '', + description: p.description || '', + organisationId: Number(p.organisationId) || 101, + ownerId: p.ownedBy || currentUserId, + }); + setIsPortfolioModalOpen(true); + }; + + const openCreateProgramme = () => { + setEditingProgramme(null); + programmeForm.reset({ + name: '', + portfolioId: portfolios[0]?.id ? Number(portfolios[0].id) : 201, + status: ApprovalStatus.Approved, + ownerId: currentUserId, + }); + setIsProgrammeModalOpen(true); + }; + + const openEditProgramme = (pr: Programme) => { + setEditingProgramme(pr); + programmeForm.reset({ + name: pr.name || '', + portfolioId: Number(pr.portfolioId) || 201, + status: pr.status ?? ApprovalStatus.Approved, + ownerId: pr.ownedBy || currentUserId, + }); + setIsProgrammeModalOpen(true); + }; + + // Filtered lists + const filteredOrgs = organisations.filter( + (o) => + o.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + o.registrationNo?.toLowerCase().includes(searchTerm.toLowerCase()) || + o.email?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( +
+ {/* Header */} +
+
+
+

+ Governance & Structure +

+ + Directory + +
+

+ Maintain Master Organisation Registry, Funder Portfolios, and Strategic Programmes hierarchy. +

+
+ + {/* Tab Switcher & CTA */} +
+ {activeTab === 'organisations' && ( + + )} + {activeTab === 'portfolios' && ( + + )} + {activeTab === 'programmes' && ( + + )} +
+
+ + {/* Tabs */} +
+ + + +
+ + {/* Search Bar */} + {activeTab === 'organisations' && ( +
+ + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" + /> +
+ )} + + {/* Tab 1: Organisations Table */} + {activeTab === 'organisations' && ( +
+
+ + + + + + + + + + + + + {loadingOrgs ? ( + + + + ) : filteredOrgs.length === 0 ? ( + + + + ) : ( + filteredOrgs.map((org) => { + const statusBadge = getActivityStatusBadge(org.status); + return ( + + + + + + + + + ); + }) + )} + +
Organisation NameRegistration NoEntity TypeContact EmailStatusActions
+ Loading registered organisations... +
+ No organisations found matching query. +
+ {org.name} + ID: {org.id} + {org.registrationNo || '—'} + + {getOrganisationTypeLabel(org.type)} + + + + + {org.email} + + + + {statusBadge.label} + + + +
+
+
+ )} + + {/* Tab 2: Portfolios List */} + {activeTab === 'portfolios' && ( +
+ {loadingPortfolios ? ( +

Loading portfolios...

+ ) : ( + portfolios.map((p) => { + const org = organisations.find((o) => String(o.id) === String(p.organisationId)); + const progCount = programmes.filter((pr) => String(pr.portfolioId) === String(p.id)).length; + return ( +
+
+
+ + Portfolio #{p.id} + + {progCount} Programmes +
+

{p.name}

+

{p.description}

+
+ +
+
+ {org ? org.name : `Org #${p.organisationId}`} + {formatDate(p.createdAt)} +
+
+ + Owner: {getUserEmail(p.ownedBy)} + + +
+
+
+ ); + }) + )} +
+ )} + + {/* Tab 3: Programmes Hierarchy */} + {activeTab === 'programmes' && ( +
+
+ + + + + + + + + + + + + {loadingProgrammes ? ( + + + + ) : ( + programmes.map((pr) => { + const port = portfolios.find((p) => String(p.id) === String(pr.portfolioId)); + const statusBadge = getApprovalStatusBadge(pr.status); + return ( + + + + + + + + + ); + }) + )} + +
Programme NameParent PortfolioGovernance StatusCreation DateOwner EmailActions
+ Loading programmes... +
+ {pr.name} + ID: {pr.id} + + {port ? port.name : `Portfolio #${pr.portfolioId}`} + + + {statusBadge.label} + + {formatDate(pr.createdAt)} + {getUserEmail(pr.ownedBy)} + + +
+
+
+ )} + + {/* Modal: Register / Edit Organisation */} + setIsOrgModalOpen(false)} + title={editingOrg ? 'Edit Organisation Profile' : 'Register New Organisation'} + subtitle="Mandatory master data profile for Recipient or Funder entities (POPIA compliant)" + > +
orgMutation.mutate(data))} className="space-y-4"> +
+ + + {orgForm.formState.errors.name && ( +

{orgForm.formState.errors.name.message}

+ )} +
+ +
+
+ + + {orgForm.formState.errors.registrationNo && ( +

{orgForm.formState.errors.registrationNo.message}

+ )} +
+ +
+ + +
+
+ +
+
+ + + {orgForm.formState.errors.email && ( +

{orgForm.formState.errors.email.message}

+ )} +
+ + {editingOrg && ( +
+ + +
+ )} +
+ +
+ + +
+
+
+ + {/* Modal: Create / Edit Portfolio */} + { + setIsPortfolioModalOpen(false); + setEditingPortfolio(null); + }} + title={editingPortfolio ? 'Edit Funder Portfolio' : 'Create Funder Portfolio'} + subtitle="Strategic allocation bucket grouping multiple programmatic initiatives" + > +
portfolioMutation.mutate(data))} className="space-y-4"> +
+ + + {portfolioForm.formState.errors.name && ( +

{portfolioForm.formState.errors.name.message}

+ )} +
+ +
+ + +
+ +
+ + +
+ +
+ +