diff --git a/QUICK_START.md b/QUICK_START.md index 0ec3dd7..e2a3ac6 100644 --- a/QUICK_START.md +++ b/QUICK_START.md @@ -1,6 +1,5 @@ # Quick Start Guide - ## Start the Application ### Option 1: Use the Start Script (Recommended) diff --git a/README.md b/README.md index 18bc70e..a703c59 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,26 @@ # React + Vite -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This template provides a minimal setup to get React working in Vite with HMR and +some ESLint rules. Currently, two official plugins are available: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) + uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in + [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) + uses [SWC](https://swc.rs/) for Fast Refresh ## React Compiler -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +The React Compiler is not enabled on this template because of its impact on dev +& build performances. To add it, see +[this documentation](https://react.dev/learn/react-compiler/installation). ## Expanding the ESLint configuration -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +If you are developing a production application, we recommend using TypeScript +with type-aware lint rules enabled. Check out the +[TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) +for information on how to integrate TypeScript and +[`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/activities.md b/activities.md index aaf7296..c929575 100644 --- a/activities.md +++ b/activities.md @@ -2,81 +2,81 @@ ## GROUNDNUT Department -| # | Activity Name | Sub-Department | Unit of Measurement | -|---|---------------|----------------|---------------------| -| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag | -| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag | -| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag | -| 4 | Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack) | Loading/Unloading | Per Bag | -| 5 | Guthali dhada Pala Tulai Silai Dhaang / Loading (Kernel Heap Weighing Stitching Stack/Loading) | Loading/Unloading | Per Bag | -| 6 | Mufali Patthar Bori silai Dhaang (Groundnut Stone Bag Stitching Stack) | Loading/Unloading | Per Bag | -| 7 | Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading) | Loading/Unloading | Per Bag | -| 8 | Bardana Bandal Loading (Gunny Bundle Loading) | Loading/Unloading | Per Bag | -| 9 | Bardana Gatthi Loading/Unloading (Gunny Bale Loading/Unloading) | Loading/Unloading | Per Bag | -| 10 | Black Dana Loading/Unloading | Loading/Unloading | Per Bag | -| 11 | Pre Cleaner | Pre Cleaning | Fixed Rate-Per Person | -| 12 | Destoner | Destoner | Fixed Rate-Per Person | -| 13 | Water | Water | Fixed Rate-Per Person | -| 14 | Decordicater | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | -| 15 | Round Chalna (Round Sieving) | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | -| 16 | Cleaning | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | -| 17 | Round Chalna No.1 (Round Sieving No.1) | Round Chalna No.1 | Fixed Rate-Per Person | -| 18 | Dala - Chomu & Jaipur (Branch - Chomu & Jaipur) | Loading/Unloading | Per Bag | +| # | Activity Name | Sub-Department | Unit of Measurement | +| -- | ---------------------------------------------------------------------------------------------- | ---------------------------------------- | --------------------- | +| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag | +| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag | +| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag | +| 4 | Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack) | Loading/Unloading | Per Bag | +| 5 | Guthali dhada Pala Tulai Silai Dhaang / Loading (Kernel Heap Weighing Stitching Stack/Loading) | Loading/Unloading | Per Bag | +| 6 | Mufali Patthar Bori silai Dhaang (Groundnut Stone Bag Stitching Stack) | Loading/Unloading | Per Bag | +| 7 | Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading) | Loading/Unloading | Per Bag | +| 8 | Bardana Bandal Loading (Gunny Bundle Loading) | Loading/Unloading | Per Bag | +| 9 | Bardana Gatthi Loading/Unloading (Gunny Bale Loading/Unloading) | Loading/Unloading | Per Bag | +| 10 | Black Dana Loading/Unloading | Loading/Unloading | Per Bag | +| 11 | Pre Cleaner | Pre Cleaning | Fixed Rate-Per Person | +| 12 | Destoner | Destoner | Fixed Rate-Per Person | +| 13 | Water | Water | Fixed Rate-Per Person | +| 14 | Decordicater | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | +| 15 | Round Chalna (Round Sieving) | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | +| 16 | Cleaning | Decordicater & Cleaning and Round Chalna | Fixed Rate-Per Person | +| 17 | Round Chalna No.1 (Round Sieving No.1) | Round Chalna No.1 | Fixed Rate-Per Person | +| 18 | Dala - Chomu & Jaipur (Branch - Chomu & Jaipur) | Loading/Unloading | Per Bag | ## DANA Department -| # | Activity Name | Sub-Department | Unit of Measurement | -|---|---------------|----------------|---------------------| -| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag | -| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag | -| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag | -| 4 | Tulai Silai Dhaang Ikai No. 2 Machine ke Pass (Weighing Stitching Stack Unit No. 2 Near Machine) | Loading/Unloading | Per Bag | -| 5 | Dana Unloading/Dhaang (Grain Unloading/Stack) | Loading/Unloading | Per Bag | -| 6 | Dana Aavak Keep Katai (Grain Arrival Hopper Cutting) | Loading/Unloading | Per Bag | -| 7 | Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg (Waste Heap Filling Weighing Stitching Load/Stack 70kg) | Loading/Unloading | Per Bag | -| 8 | Kachri Dhaang se loading (Waste Loading from Stack) | Loading/Unloading | Per Bag | -| 9 | Keep Katai Khulla Katta (Hopper Cutting Open Bag) | Loading/Unloading | Per Bag | -| 10 | Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched) | Loading/Unloading | Per Bag | -| 11 | Bardana Paltai (Gunny Turning) | Loading/Unloading | Per Bag | -| 12 | Ekai No. 2 me Keep Katai Khula Bag (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Open Bag with Dragging into Tank) | Loading/Unloading | Per Bag | -| 13 | Ekai No. 2 me Keep Katai Silai Kholkar (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Opening Stitched with Dragging into Tank) | Loading/Unloading | Per Bag | -| 14 | Silai Loading Company Gadi Dala Sahit (Stitching Loading Company Vehicle with Branch) | Loading/Unloading | Per Bag | -| 15 | Kachri Bharai Silai Dhaang Chatt Par (Waste Filling Stitching Stack on Roof) | Loading/Unloading | Per Bag | -| 16 | Bardana Unloading (Gunny Unloading) | Loading/Unloading | Per Bag | -| 17 | Grading | Loading/Unloading | Per Bag | -| 18 | Destoner | Destoner | Fixed Rate-Per Person | -| 19 | Gravity | Gravity | Fixed Rate-Per Person | -| 20 | Tank | Tank | Fixed Rate-Per Person | -| 21 | Sortex | Sortex | Fixed Rate-Per Person | -| 22 | X-Ray | X-Ray | Fixed Rate-Per Person | -| 23 | Kachri (Waste) | Kachri | Fixed Rate-Per Person | -| 24 | Other Works | Other Works | Fixed Rate-Per Person | +| # | Activity Name | Sub-Department | Unit of Measurement | +| -- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------- | +| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag | +| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag | +| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag | +| 4 | Tulai Silai Dhaang Ikai No. 2 Machine ke Pass (Weighing Stitching Stack Unit No. 2 Near Machine) | Loading/Unloading | Per Bag | +| 5 | Dana Unloading/Dhaang (Grain Unloading/Stack) | Loading/Unloading | Per Bag | +| 6 | Dana Aavak Keep Katai (Grain Arrival Hopper Cutting) | Loading/Unloading | Per Bag | +| 7 | Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg (Waste Heap Filling Weighing Stitching Load/Stack 70kg) | Loading/Unloading | Per Bag | +| 8 | Kachri Dhaang se loading (Waste Loading from Stack) | Loading/Unloading | Per Bag | +| 9 | Keep Katai Khulla Katta (Hopper Cutting Open Bag) | Loading/Unloading | Per Bag | +| 10 | Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched) | Loading/Unloading | Per Bag | +| 11 | Bardana Paltai (Gunny Turning) | Loading/Unloading | Per Bag | +| 12 | Ekai No. 2 me Keep Katai Khula Bag (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Open Bag with Dragging into Tank) | Loading/Unloading | Per Bag | +| 13 | Ekai No. 2 me Keep Katai Silai Kholkar (Khichai Sahit Tank Me) (Unit No. 2 Hopper Cutting Opening Stitched with Dragging into Tank) | Loading/Unloading | Per Bag | +| 14 | Silai Loading Company Gadi Dala Sahit (Stitching Loading Company Vehicle with Branch) | Loading/Unloading | Per Bag | +| 15 | Kachri Bharai Silai Dhaang Chatt Par (Waste Filling Stitching Stack on Roof) | Loading/Unloading | Per Bag | +| 16 | Bardana Unloading (Gunny Unloading) | Loading/Unloading | Per Bag | +| 17 | Grading | Loading/Unloading | Per Bag | +| 18 | Destoner | Destoner | Fixed Rate-Per Person | +| 19 | Gravity | Gravity | Fixed Rate-Per Person | +| 20 | Tank | Tank | Fixed Rate-Per Person | +| 21 | Sortex | Sortex | Fixed Rate-Per Person | +| 22 | X-Ray | X-Ray | Fixed Rate-Per Person | +| 23 | Kachri (Waste) | Kachri | Fixed Rate-Per Person | +| 24 | Other Works | Other Works | Fixed Rate-Per Person | ## TUKDI Department -| # | Activity Name | Sub-Department | Unit of Measurement | -|---|---------------|----------------|---------------------| -| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag | -| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag | -| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag | -| 4 | Grading Chalne se Maal Bharai Tulai Silai Loading (Grading Running Material Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag | -| 5 | Chilka Bharai silai Dhaang (Husk Filling Stitching Stack) | Loading/Unloading | Per Bag | -| 6 | Keep katai Bahar Se (Hopper Cutting from Outside) | Loading/Unloading | Per Bag | -| 7 | Keep katai Andar Se (Hopper Cutting from Inside) | Loading/Unloading | Per Bag | -| 8 | Cartoon Banai Vacume Bharai Tulai Packing and Dhaang (Carton Making Vacuum Filling Weighing Packing and Stack) | Loading/Unloading | Per Bag | -| 9 | Cartoon Banai Vacume Bharai Tulai Packing and Loading (Carton Making Vacuum Filling Weighing Packing and Loading) | Loading/Unloading | Per Bag | -| 10 | Katta Paltai (Bag Turning) | Loading/Unloading | Per Bag | -| 11 | Dhada Pala Bharai Tulai Silai Dhaang (Heap Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag | -| 12 | Dhada Pala Bharai Tulai Silai Loading (Heap Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag | -| 13 | Sike Maal Ki Silai Dhaang Andar (Roasted Material Stitching Stack Inside) | Loading/Unloading | Per Bag | -| 14 | Sike Maal Ki Silai Dhaang Bahar (Roasted Material Stitching Stack Outside) | Loading/Unloading | Per Bag | -| 15 | Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside) | Loading/Unloading | Per Bag | -| 16 | Tank | Tank | Fixed Rate-Per Person | -| 17 | Grader (Machine) | Grader (Machine) | Fixed Rate-Per Person | -| 18 | Sortex | Sortex | Fixed Rate-Per Person | -| 19 | X-Ray | X-Ray | Fixed Rate-Per Person | -| 20 | Rejection | Rejection | Fixed Rate-Per Person | -| 21 | Store | Store | Fixed Rate-Per Person | -| 22 | Roster | Roster | Fixed Rate-Per Person | -| 23 | Blancher | Blancher | Fixed Rate-Per Person | -| 24 | Other Works | Other Works | Fixed Rate-Per Person | \ No newline at end of file +| # | Activity Name | Sub-Department | Unit of Measurement | +| -- | ----------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------- | +| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag | +| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag | +| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag | +| 4 | Grading Chalne se Maal Bharai Tulai Silai Loading (Grading Running Material Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag | +| 5 | Chilka Bharai silai Dhaang (Husk Filling Stitching Stack) | Loading/Unloading | Per Bag | +| 6 | Keep katai Bahar Se (Hopper Cutting from Outside) | Loading/Unloading | Per Bag | +| 7 | Keep katai Andar Se (Hopper Cutting from Inside) | Loading/Unloading | Per Bag | +| 8 | Cartoon Banai Vacume Bharai Tulai Packing and Dhaang (Carton Making Vacuum Filling Weighing Packing and Stack) | Loading/Unloading | Per Bag | +| 9 | Cartoon Banai Vacume Bharai Tulai Packing and Loading (Carton Making Vacuum Filling Weighing Packing and Loading) | Loading/Unloading | Per Bag | +| 10 | Katta Paltai (Bag Turning) | Loading/Unloading | Per Bag | +| 11 | Dhada Pala Bharai Tulai Silai Dhaang (Heap Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag | +| 12 | Dhada Pala Bharai Tulai Silai Loading (Heap Filling Weighing Stitching Loading) | Loading/Unloading | Per Bag | +| 13 | Sike Maal Ki Silai Dhaang Andar (Roasted Material Stitching Stack Inside) | Loading/Unloading | Per Bag | +| 14 | Sike Maal Ki Silai Dhaang Bahar (Roasted Material Stitching Stack Outside) | Loading/Unloading | Per Bag | +| 15 | Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside) | Loading/Unloading | Per Bag | +| 16 | Tank | Tank | Fixed Rate-Per Person | +| 17 | Grader (Machine) | Grader (Machine) | Fixed Rate-Per Person | +| 18 | Sortex | Sortex | Fixed Rate-Per Person | +| 19 | X-Ray | X-Ray | Fixed Rate-Per Person | +| 20 | Rejection | Rejection | Fixed Rate-Per Person | +| 21 | Store | Store | Fixed Rate-Per Person | +| 22 | Roster | Roster | Fixed Rate-Per Person | +| 23 | Blancher | Blancher | Fixed Rate-Per Person | +| 24 | Other Works | Other Works | Fixed Rate-Per Person | diff --git a/backend-deno/README.md b/backend-deno/README.md index c47d306..8c0b7aa 100644 --- a/backend-deno/README.md +++ b/backend-deno/README.md @@ -1,6 +1,7 @@ # Work Allocation Backend - Deno TypeScript -A secure, type-safe backend for the Work Allocation System built with Deno and TypeScript. +A secure, type-safe backend for the Work Allocation System built with Deno and +TypeScript. ## Features @@ -90,7 +91,8 @@ deno task seed - `GET /api/departments/:id` - Get department - `GET /api/departments/:id/sub-departments` - Get sub-departments - `POST /api/departments` - Create department (SuperAdmin) -- `POST /api/departments/:id/sub-departments` - Create sub-department (SuperAdmin) +- `POST /api/departments/:id/sub-departments` - Create sub-department + (SuperAdmin) ### Work Allocations @@ -122,21 +124,21 @@ deno task seed ## Environment Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | 3000 | -| `DB_HOST` | Database host | localhost | -| `DB_USER` | Database user | root | -| `DB_PASSWORD` | Database password | admin123 | -| `DB_NAME` | Database name | work_allocation | -| `DB_PORT` | Database port | 3306 | -| `JWT_SECRET` | JWT signing secret | (change in production!) | -| `JWT_EXPIRES_IN` | Token expiration | 7d | -| `BCRYPT_ROUNDS` | Password hash rounds | 12 | -| `RATE_LIMIT_WINDOW_MS` | Rate limit window | 900000 (15 min) | -| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | 100 | -| `CORS_ORIGIN` | Allowed CORS origins | | -| `NODE_ENV` | Environment | development | +| Variable | Description | Default | +| ------------------------- | ----------------------- | ----------------------- | +| `PORT` | Server port | 3000 | +| `DB_HOST` | Database host | localhost | +| `DB_USER` | Database user | root | +| `DB_PASSWORD` | Database password | admin123 | +| `DB_NAME` | Database name | work_allocation | +| `DB_PORT` | Database port | 3306 | +| `JWT_SECRET` | JWT signing secret | (change in production!) | +| `JWT_EXPIRES_IN` | Token expiration | 7d | +| `BCRYPT_ROUNDS` | Password hash rounds | 12 | +| `RATE_LIMIT_WINDOW_MS` | Rate limit window | 900000 (15 min) | +| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | 100 | +| `CORS_ORIGIN` | Allowed CORS origins | | +| `NODE_ENV` | Environment | development | ## Security Best Practices @@ -196,14 +198,14 @@ backend-deno/ ## Differences from Node.js Backend -| Feature | Node.js | Deno | -|---------|---------|------| -| Runtime | Node.js | Deno | -| Package Manager | npm | Built-in (JSR/npm) | -| TypeScript | Requires compilation | Native support | -| Security | Manual setup | Secure by default | -| Permissions | Full access | Explicit permissions | -| Framework | Express | Oak | +| Feature | Node.js | Deno | +| --------------- | -------------------- | -------------------- | +| Runtime | Node.js | Deno | +| Package Manager | npm | Built-in (JSR/npm) | +| TypeScript | Requires compilation | Native support | +| Security | Manual setup | Secure by default | +| Permissions | Full access | Explicit permissions | +| Framework | Express | Oak | ## License diff --git a/backend-deno/config/database.ts b/backend-deno/config/database.ts index bc0ef29..33e75eb 100644 --- a/backend-deno/config/database.ts +++ b/backend-deno/config/database.ts @@ -1,4 +1,4 @@ -import { createPool, Pool } from "mysql2/promise"; +import { createPool, Pool, PoolConnection } from "mysql2/promise"; import { load } from "@std/dotenv"; // Load environment variables @@ -33,14 +33,17 @@ class Database { async connect(): Promise { if (!this.pool) { this.pool = createPool(config); - + // Test connection try { const connection = await this.pool.getConnection(); console.log("✅ Database connected successfully"); connection.release(); } catch (error) { - console.error("❌ Database connection failed:", (error as Error).message); + console.error( + "❌ Database connection failed:", + (error as Error).message, + ); throw error; } } @@ -60,12 +63,39 @@ class Database { return rows as T; } - async execute(sql: string, params?: unknown[]): Promise<{ insertId: number; affectedRows: number }> { + async execute( + sql: string, + params?: unknown[], + ): Promise<{ insertId: number; affectedRows: number }> { const pool = await this.getPool(); const [result] = await pool.execute(sql, params); return result as { insertId: number; affectedRows: number }; } + // Get a connection for transaction support + async getConnection(): Promise { + const pool = await this.getPool(); + return await pool.getConnection(); + } + + // Execute within a transaction + async transaction( + callback: (connection: PoolConnection) => Promise, + ): Promise { + const connection = await this.getConnection(); + try { + await connection.beginTransaction(); + const result = await callback(connection); + await connection.commit(); + return result; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } + async close(): Promise { if (this.pool) { await this.pool.end(); diff --git a/backend-deno/config/env.ts b/backend-deno/config/env.ts index c4c4930..252c73b 100644 --- a/backend-deno/config/env.ts +++ b/backend-deno/config/env.ts @@ -5,36 +5,41 @@ await load({ export: true }); export const config = { // Server PORT: parseInt(Deno.env.get("PORT") || "3000"), - + // Database DB_HOST: Deno.env.get("DB_HOST") || "localhost", DB_USER: Deno.env.get("DB_USER") || "root", DB_PASSWORD: Deno.env.get("DB_PASSWORD") || "admin123", DB_NAME: Deno.env.get("DB_NAME") || "work_allocation", DB_PORT: parseInt(Deno.env.get("DB_PORT") || "3306"), - + // JWT - Security: Use strong secret in production - JWT_SECRET: Deno.env.get("JWT_SECRET") || "work_alloc_jwt_secret_key_change_in_production_2024", + JWT_SECRET: Deno.env.get("JWT_SECRET") || + "work_alloc_jwt_secret_key_change_in_production_2024", JWT_EXPIRES_IN: Deno.env.get("JWT_EXPIRES_IN") || "7d", - + // Security settings BCRYPT_ROUNDS: parseInt(Deno.env.get("BCRYPT_ROUNDS") || "12"), - RATE_LIMIT_WINDOW_MS: parseInt(Deno.env.get("RATE_LIMIT_WINDOW_MS") || "900000"), // 15 minutes - RATE_LIMIT_MAX_REQUESTS: parseInt(Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100"), - + RATE_LIMIT_WINDOW_MS: parseInt( + Deno.env.get("RATE_LIMIT_WINDOW_MS") || "900000", + ), // 15 minutes + RATE_LIMIT_MAX_REQUESTS: parseInt( + Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100", + ), + // CORS CORS_ORIGIN: Deno.env.get("CORS_ORIGIN") || "http://localhost:5173", - + // Environment NODE_ENV: Deno.env.get("NODE_ENV") || "development", - + isDevelopment(): boolean { return this.NODE_ENV === "development"; }, - + isProduction(): boolean { return this.NODE_ENV === "production"; - } + }, }; export default config; diff --git a/backend-deno/main.ts b/backend-deno/main.ts index 502ce83..3e136d0 100644 --- a/backend-deno/main.ts +++ b/backend-deno/main.ts @@ -1,7 +1,12 @@ import { Application, Router } from "@oak/oak"; import { config } from "./config/env.ts"; import { db } from "./config/database.ts"; -import { cors, securityHeaders, requestLogger, rateLimit } from "./middleware/security.ts"; +import { + cors, + rateLimit, + requestLogger, + securityHeaders, +} from "./middleware/security.ts"; // Import routes import authRoutes from "./routes/auth.ts"; @@ -61,14 +66,46 @@ router.get("/health", (ctx) => { // Mount API routes router.use("/api/auth", authRoutes.routes(), authRoutes.allowedMethods()); router.use("/api/users", userRoutes.routes(), userRoutes.allowedMethods()); -router.use("/api/departments", departmentRoutes.routes(), departmentRoutes.allowedMethods()); -router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocationRoutes.allowedMethods()); -router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods()); -router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods()); -router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods()); -router.use("/api/reports", reportRoutes.routes(), reportRoutes.allowedMethods()); -router.use("/api/standard-rates", standardRateRoutes.routes(), standardRateRoutes.allowedMethods()); -router.use("/api/activities", activityRoutes.routes(), activityRoutes.allowedMethods()); +router.use( + "/api/departments", + departmentRoutes.routes(), + departmentRoutes.allowedMethods(), +); +router.use( + "/api/work-allocations", + workAllocationRoutes.routes(), + workAllocationRoutes.allowedMethods(), +); +router.use( + "/api/attendance", + attendanceRoutes.routes(), + attendanceRoutes.allowedMethods(), +); +router.use( + "/api/contractor-rates", + contractorRateRoutes.routes(), + contractorRateRoutes.allowedMethods(), +); +router.use( + "/api/employee-swaps", + employeeSwapRoutes.routes(), + employeeSwapRoutes.allowedMethods(), +); +router.use( + "/api/reports", + reportRoutes.routes(), + reportRoutes.allowedMethods(), +); +router.use( + "/api/standard-rates", + standardRateRoutes.routes(), + standardRateRoutes.allowedMethods(), +); +router.use( + "/api/activities", + activityRoutes.routes(), + activityRoutes.allowedMethods(), +); // Apply routes app.use(router.routes()); diff --git a/backend-deno/middleware/auth.ts b/backend-deno/middleware/auth.ts index 9bc9329..792c517 100644 --- a/backend-deno/middleware/auth.ts +++ b/backend-deno/middleware/auth.ts @@ -1,5 +1,5 @@ import { Context, Next } from "@oak/oak"; -import { verify, create, getNumericDate } from "djwt"; +import { create, getNumericDate, verify } from "djwt"; import { config } from "../config/env.ts"; import type { JWTPayload, UserRole } from "../types/index.ts"; @@ -12,14 +12,16 @@ const cryptoKey = await crypto.subtle.importKey( keyData, { name: "HMAC", hash: "SHA-256" }, false, - ["sign", "verify"] + ["sign", "verify"], ); // Generate JWT token -export async function generateToken(payload: Omit): Promise { +export async function generateToken( + payload: Omit, +): Promise { const expiresIn = config.JWT_EXPIRES_IN; let expSeconds = 7 * 24 * 60 * 60; // Default 7 days - + if (expiresIn.endsWith("d")) { expSeconds = parseInt(expiresIn) * 24 * 60 * 60; } else if (expiresIn.endsWith("h")) { @@ -27,7 +29,7 @@ export async function generateToken(payload: Omit): P } else if (expiresIn.endsWith("m")) { expSeconds = parseInt(expiresIn) * 60; } - + const token = await create( { alg: "HS256", typ: "JWT" }, { @@ -35,9 +37,9 @@ export async function generateToken(payload: Omit): P exp: getNumericDate(expSeconds), iat: getNumericDate(0), }, - cryptoKey + cryptoKey, ); - + return token; } @@ -52,24 +54,27 @@ export async function verifyToken(token: string): Promise { } // Authentication middleware -export async function authenticateToken(ctx: Context, next: Next): Promise { +export async function authenticateToken( + ctx: Context, + next: Next, +): Promise { const authHeader = ctx.request.headers.get("Authorization"); const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; - + if (!token) { ctx.response.status = 401; ctx.response.body = { error: "Access token required" }; return; } - + const payload = await verifyToken(token); - + if (!payload) { ctx.response.status = 403; ctx.response.body = { error: "Invalid or expired token" }; return; } - + // Attach user to context state ctx.state.user = payload; await next(); @@ -79,19 +84,19 @@ export async function authenticateToken(ctx: Context, next: Next): Promise export function authorize(...roles: UserRole[]) { return async (ctx: Context, next: Next): Promise => { const user = ctx.state.user as JWTPayload | undefined; - + if (!user) { ctx.response.status = 401; ctx.response.body = { error: "Unauthorized" }; return; } - + if (!roles.includes(user.role)) { ctx.response.status = 403; ctx.response.body = { error: "Insufficient permissions" }; return; } - + await next(); }; } diff --git a/backend-deno/middleware/security.ts b/backend-deno/middleware/security.ts index 119f50d..02701c6 100644 --- a/backend-deno/middleware/security.ts +++ b/backend-deno/middleware/security.ts @@ -10,54 +10,57 @@ export async function rateLimit(ctx: Context, next: Next): Promise { const now = Date.now(); const windowMs = config.RATE_LIMIT_WINDOW_MS; const maxRequests = config.RATE_LIMIT_MAX_REQUESTS; - + const record = rateLimitStore.get(ip); - + if (!record || now > record.resetTime) { rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs }); } else { record.count++; - + if (record.count > maxRequests) { ctx.response.status = 429; - ctx.response.body = { + ctx.response.body = { error: "Too many requests", - retryAfter: Math.ceil((record.resetTime - now) / 1000) + retryAfter: Math.ceil((record.resetTime - now) / 1000), }; return; } } - + await next(); } // Security headers middleware export async function securityHeaders(ctx: Context, next: Next): Promise { await next(); - + // Prevent clickjacking ctx.response.headers.set("X-Frame-Options", "DENY"); - + // Prevent MIME type sniffing ctx.response.headers.set("X-Content-Type-Options", "nosniff"); - + // XSS protection ctx.response.headers.set("X-XSS-Protection", "1; mode=block"); - + // Referrer policy - ctx.response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); - + ctx.response.headers.set( + "Referrer-Policy", + "strict-origin-when-cross-origin", + ); + // Content Security Policy ctx.response.headers.set( "Content-Security-Policy", - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';", ); - + // Strict Transport Security (only in production with HTTPS) if (config.isProduction()) { ctx.response.headers.set( "Strict-Transport-Security", - "max-age=31536000; includeSubDomains" + "max-age=31536000; includeSubDomains", ); } } @@ -65,27 +68,35 @@ export async function securityHeaders(ctx: Context, next: Next): Promise { // CORS middleware export async function cors(ctx: Context, next: Next): Promise { const origin = ctx.request.headers.get("Origin"); - const allowedOrigins = config.CORS_ORIGIN.split(",").map(o => o.trim()); - + const allowedOrigins = config.CORS_ORIGIN.split(",").map((o) => o.trim()); + // Check if origin is allowed - if (origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))) { + if ( + origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*")) + ) { ctx.response.headers.set("Access-Control-Allow-Origin", origin); } else if (config.isDevelopment()) { // Allow all origins in development ctx.response.headers.set("Access-Control-Allow-Origin", origin || "*"); } - - ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + ctx.response.headers.set( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS", + ); + ctx.response.headers.set( + "Access-Control-Allow-Headers", + "Content-Type, Authorization", + ); ctx.response.headers.set("Access-Control-Allow-Credentials", "true"); ctx.response.headers.set("Access-Control-Max-Age", "86400"); - + // Handle preflight requests if (ctx.request.method === "OPTIONS") { ctx.response.status = 204; return; } - + await next(); } @@ -93,19 +104,21 @@ export async function cors(ctx: Context, next: Next): Promise { export async function requestLogger(ctx: Context, next: Next): Promise { const start = Date.now(); const { method, url } = ctx.request; - + await next(); - + const ms = Date.now() - start; const status = ctx.response.status; - + // Color code based on status let statusColor = "\x1b[32m"; // Green for 2xx if (status >= 400 && status < 500) statusColor = "\x1b[33m"; // Yellow for 4xx if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx - + console.log( - `${new Date().toISOString()} - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms` + `${ + new Date().toISOString() + } - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms`, ); } @@ -125,18 +138,32 @@ export function isValidEmail(email: string): boolean { } // Validate password strength -export function isStrongPassword(password: string): { valid: boolean; message?: string } { +export function isStrongPassword( + password: string, +): { valid: boolean; message?: string } { if (password.length < 8) { - return { valid: false, message: "Password must be at least 8 characters long" }; + return { + valid: false, + message: "Password must be at least 8 characters long", + }; } if (!/[A-Z]/.test(password)) { - return { valid: false, message: "Password must contain at least one uppercase letter" }; + return { + valid: false, + message: "Password must contain at least one uppercase letter", + }; } if (!/[a-z]/.test(password)) { - return { valid: false, message: "Password must contain at least one lowercase letter" }; + return { + valid: false, + message: "Password must contain at least one lowercase letter", + }; } if (!/[0-9]/.test(password)) { - return { valid: false, message: "Password must contain at least one number" }; + return { + valid: false, + message: "Password must contain at least one number", + }; } return { valid: true }; } diff --git a/backend-deno/routes/activities.ts b/backend-deno/routes/activities.ts index c9b6889..aa400ad 100644 --- a/backend-deno/routes/activities.ts +++ b/backend-deno/routes/activities.ts @@ -21,7 +21,7 @@ router.get("/", authenticateToken, async (ctx) => { const params = ctx.request.url.searchParams; const subDepartmentId = params.get("subDepartmentId"); const departmentId = params.get("departmentId"); - + let query = ` SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at, sd.name as sub_department_name, @@ -33,19 +33,19 @@ router.get("/", authenticateToken, async (ctx) => { WHERE 1=1 `; const queryParams: unknown[] = []; - + if (subDepartmentId) { query += " AND a.sub_department_id = ?"; queryParams.push(subDepartmentId); } - + if (departmentId) { query += " AND sd.department_id = ?"; queryParams.push(departmentId); } - + query += " ORDER BY d.name, sd.name, a.name"; - + const activities = await db.query(query, queryParams); ctx.response.body = activities; } catch (error) { @@ -59,7 +59,7 @@ router.get("/", authenticateToken, async (ctx) => { router.get("/:id", authenticateToken, async (ctx) => { try { const activityId = ctx.params.id; - + const activities = await db.query( `SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at, sd.name as sub_department_name, @@ -69,15 +69,15 @@ router.get("/:id", authenticateToken, async (ctx) => { JOIN sub_departments sd ON a.sub_department_id = sd.id JOIN departments d ON sd.department_id = d.id WHERE a.id = ?`, - [activityId] + [activityId], ); - + if (activities.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Activity not found" }; return; } - + ctx.response.body = activities[0]; } catch (error) { console.error("Get activity error:", error); @@ -92,55 +92,61 @@ router.post("/", authenticateToken, async (ctx) => { const user = getCurrentUser(ctx); const body = await ctx.request.body.json(); const { sub_department_id, name, unit_of_measurement } = body; - + if (!sub_department_id || !name) { ctx.response.status = 400; ctx.response.body = { error: "Sub-department ID and name are required" }; return; } - + // Get the sub-department to check department ownership const subDepts = await db.query<{ department_id: number }[]>( "SELECT department_id FROM sub_departments WHERE id = ?", - [sub_department_id] + [sub_department_id], ); - + if (subDepts.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Sub-department not found" }; return; } - + const subDeptDepartmentId = subDepts[0].department_id; - + // Check authorization - if (user.role === 'Supervisor' && user.departmentId !== subDeptDepartmentId) { + if ( + user.role === "Supervisor" && user.departmentId !== subDeptDepartmentId + ) { ctx.response.status = 403; - ctx.response.body = { error: "You can only create activities for your own department" }; + ctx.response.body = { + error: "You can only create activities for your own department", + }; return; } - - if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { + + if (user.role !== "SuperAdmin" && user.role !== "Supervisor") { ctx.response.status = 403; ctx.response.body = { error: "Unauthorized" }; return; } - + const result = await db.execute( "INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)", - [sub_department_id, name, unit_of_measurement || "Per Bag"] + [sub_department_id, name, unit_of_measurement || "Per Bag"], ); - + ctx.response.status = 201; - ctx.response.body = { - id: result.lastInsertId, - message: "Activity created successfully" + ctx.response.body = { + id: result.insertId, + message: "Activity created successfully", }; } catch (error) { const err = error as { code?: string }; if (err.code === "ER_DUP_ENTRY") { ctx.response.status = 400; - ctx.response.body = { error: "Activity already exists in this sub-department" }; + ctx.response.body = { + error: "Activity already exists in this sub-department", + }; return; } console.error("Create activity error:", error); @@ -155,12 +161,12 @@ router.put("/:id", authenticateToken, async (ctx) => { const activityId = ctx.params.id; const body = await ctx.request.body.json(); const { name, unit_of_measurement } = body; - + await db.execute( "UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?", - [name, unit_of_measurement, activityId] + [name, unit_of_measurement, activityId], ); - + ctx.response.body = { message: "Activity updated successfully" }; } catch (error) { console.error("Update activity error:", error); @@ -174,39 +180,43 @@ router.delete("/:id", authenticateToken, async (ctx) => { try { const user = getCurrentUser(ctx); const activityId = ctx.params.id; - + // Get the activity and its sub-department to check department ownership const activities = await db.query( `SELECT a.*, sd.department_id FROM activities a JOIN sub_departments sd ON a.sub_department_id = sd.id WHERE a.id = ?`, - [activityId] + [activityId], ); - + if (activities.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Activity not found" }; return; } - + const activity = activities[0] as Activity & { department_id: number }; - + // Check authorization - if (user.role === 'Supervisor' && user.departmentId !== activity.department_id) { + if ( + user.role === "Supervisor" && user.departmentId !== activity.department_id + ) { ctx.response.status = 403; - ctx.response.body = { error: "You can only delete activities from your own department" }; + ctx.response.body = { + error: "You can only delete activities from your own department", + }; return; } - - if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { + + if (user.role !== "SuperAdmin" && user.role !== "Supervisor") { ctx.response.status = 403; ctx.response.body = { error: "Unauthorized" }; return; } - + await db.execute("DELETE FROM activities WHERE id = ?", [activityId]); - + ctx.response.body = { message: "Activity deleted successfully" }; } catch (error) { console.error("Delete activity error:", error); diff --git a/backend-deno/routes/attendance.ts b/backend-deno/routes/attendance.ts index 3b95723..6f65c50 100644 --- a/backend-deno/routes/attendance.ts +++ b/backend-deno/routes/attendance.ts @@ -1,21 +1,37 @@ -import { Router } from "@oak/oak"; +import { Router, type RouterContext, type State } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; -import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; +import type { + Attendance, + AttendanceStatus, + CheckInOutRequest, + JWTPayload, + UpdateAttendanceStatusRequest, + User, +} from "../types/index.ts"; const router = new Router(); // Get all attendance records -router.get("/", authenticateToken, async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const employeeId = params.get("employeeId"); - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - const status = params.get("status"); - - let query = ` +router.get( + "/", + authenticateToken, + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const currentUser: JWTPayload = getCurrentUser(ctx); + const params: URLSearchParams = ctx.request.url.searchParams; + const employeeId: string | null = params.get("employeeId"); + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + const status: string | null = params.get("status"); + + let query = ` SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, @@ -28,53 +44,54 @@ router.get("/", authenticateToken, async (ctx) => { LEFT JOIN users c ON e.contractor_id = c.id WHERE 1=1 `; - const queryParams: unknown[] = []; - - // Role-based filtering - if (currentUser.role === "Supervisor") { - query += " AND a.supervisor_id = ?"; - queryParams.push(currentUser.id); - } else if (currentUser.role === "Employee") { - query += " AND a.employee_id = ?"; - queryParams.push(currentUser.id); + const queryParams: unknown[] = []; + + // Role-based filtering + if (currentUser.role === "Supervisor") { + query += " AND a.supervisor_id = ?"; + queryParams.push(currentUser.id); + } else if (currentUser.role === "Employee") { + query += " AND a.employee_id = ?"; + queryParams.push(currentUser.id); + } + + if (employeeId) { + query += " AND a.employee_id = ?"; + queryParams.push(employeeId); + } + + if (startDate) { + query += " AND a.work_date >= ?"; + queryParams.push(startDate); + } + + if (endDate) { + query += " AND a.work_date <= ?"; + queryParams.push(endDate); + } + + if (status) { + query += " AND a.status = ?"; + queryParams.push(status); + } + + query += " ORDER BY a.work_date DESC, a.check_in_time DESC"; + + const records = await db.query(query, queryParams); + ctx.response.body = records; + } catch (error) { + console.error("Get attendance error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - if (employeeId) { - query += " AND a.employee_id = ?"; - queryParams.push(employeeId); - } - - if (startDate) { - query += " AND a.work_date >= ?"; - queryParams.push(startDate); - } - - if (endDate) { - query += " AND a.work_date <= ?"; - queryParams.push(endDate); - } - - if (status) { - query += " AND a.status = ?"; - queryParams.push(status); - } - - query += " ORDER BY a.work_date DESC, a.check_in_time DESC"; - - const records = await db.query(query, queryParams); - ctx.response.body = records; - } catch (error) { - console.error("Get attendance error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Get attendance by ID router.get("/:id", authenticateToken, async (ctx) => { try { const attendanceId = ctx.params.id; - + const records = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, @@ -87,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => { LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, - [attendanceId] + [attendanceId], ); - + if (records.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Attendance record not found" }; return; } - + ctx.response.body = records[0]; } catch (error) { console.error("Get attendance error:", error); @@ -105,245 +122,260 @@ router.get("/:id", authenticateToken, async (ctx) => { }); // Check in employee (Supervisor or SuperAdmin) -router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as CheckInOutRequest; - const { employeeId, workDate } = body; - - if (!employeeId || !workDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Employee ID and work date required" }; - return; - } - - // Verify employee exists - let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?"; - const employeeParams: unknown[] = [employeeId, "Employee"]; - - if (currentUser.role === "Supervisor") { - employeeQuery += " AND department_id = ?"; - employeeParams.push(currentUser.departmentId); - } - - const employees = await db.query(employeeQuery, employeeParams); - - if (employees.length === 0) { - ctx.response.status = 403; - ctx.response.body = { error: "Employee not found or not in your department" }; - return; - } - - // Check if already checked in today - const existing = await db.query( - "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?", - [employeeId, workDate, "CheckedIn"] - ); - - if (existing.length > 0) { - ctx.response.status = 400; - ctx.response.body = { error: "Employee already checked in today" }; - return; - } - - const checkInTime = new Date().toISOString().slice(0, 19).replace("T", " "); - - const result = await db.execute( - "INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)", - [employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"] - ); - - const newRecord = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newRecord[0]; - } catch (error) { - console.error("Check in error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); +router.post( + "/check-in", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CheckInOutRequest; + const { employeeId, workDate } = body; -// Check out employee (Supervisor or SuperAdmin) -router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as CheckInOutRequest; - const { employeeId, workDate } = body; - - if (!employeeId || !workDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Employee ID and work date required" }; - return; - } - - // Find the check-in record - let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?"; - const params: unknown[] = [employeeId, workDate, "CheckedIn"]; - - if (currentUser.role === "Supervisor") { - query += " AND supervisor_id = ?"; - params.push(currentUser.id); - } - - const records = await db.query(query, params); - - if (records.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "No check-in record found for today" }; - return; - } - - const checkOutTime = new Date().toISOString().slice(0, 19).replace("T", " "); - - await db.execute( - "UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?", - [checkOutTime, "CheckedOut", records[0].id] - ); - - const updatedRecord = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [records[0].id] - ); - - ctx.response.body = updatedRecord[0]; - } catch (error) { - console.error("Check out error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + if (!employeeId || !workDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee ID and work date required" }; + return; + } -// Update attendance status (mark as Absent, HalfDay, Late) -router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const attendanceId = ctx.params.id; - const body = await ctx.request.body.json() as UpdateAttendanceStatusRequest; - const { status, remark } = body; - - // Validate status - const validStatuses: AttendanceStatus[] = ["CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late"]; - if (!validStatuses.includes(status)) { - ctx.response.status = 400; - ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late" }; - return; - } - - // Check if record exists - const existing = await db.query( - "SELECT * FROM attendance WHERE id = ?", - [attendanceId] - ); - - if (existing.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Attendance record not found" }; - return; - } - - // Update the status - await db.execute( - "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", - [status, remark || null, attendanceId] - ); - - const updatedRecord = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [attendanceId] - ); - - ctx.response.body = updatedRecord[0]; - } catch (error) { - console.error("Update attendance status error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + // Verify employee exists + let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?"; + const employeeParams: unknown[] = [employeeId, "Employee"]; -// Mark employee as absent (create absent record) -router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json(); - const { employeeId, workDate, remark } = body; - - if (!employeeId || !workDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Employee ID and work date required" }; - return; - } - - // Check if record already exists for this date - const existing = await db.query( - "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?", - [employeeId, workDate] - ); - - if (existing.length > 0) { - // Update existing record to Absent - await db.execute( - "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", - ["Absent", remark || "Marked absent", existing[0].id] + if (currentUser.role === "Supervisor") { + employeeQuery += " AND department_id = ?"; + employeeParams.push(currentUser.departmentId); + } + + const employees = await db.query(employeeQuery, employeeParams); + + if (employees.length === 0) { + ctx.response.status = 403; + ctx.response.body = { + error: "Employee not found or not in your department", + }; + return; + } + + // Check if already checked in today + const existing = await db.query( + "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?", + [employeeId, workDate, "CheckedIn"], ); - - const updatedRecord = await db.query( - `SELECT a.*, - e.name as employee_name, e.username as employee_username, - s.name as supervisor_name, - d.name as department_name, - c.name as contractor_name - FROM attendance a - JOIN users e ON a.employee_id = e.id - JOIN users s ON a.supervisor_id = s.id - LEFT JOIN departments d ON e.department_id = d.id - LEFT JOIN users c ON e.contractor_id = c.id - WHERE a.id = ?`, - [existing[0].id] + + if (existing.length > 0) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee already checked in today" }; + return; + } + + const checkInTime = new Date().toISOString().slice(0, 19).replace( + "T", + " ", ); - - ctx.response.body = updatedRecord[0]; - } else { - // Create new absent record + const result = await db.execute( - "INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)", - [employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent"] + "INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)", + [employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"], ); - + const newRecord = await db.query( `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newRecord[0]; + } catch (error) { + console.error("Check in error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); + +// Check out employee (Supervisor or SuperAdmin) +router.post( + "/check-out", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CheckInOutRequest; + const { employeeId, workDate } = body; + + if (!employeeId || !workDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee ID and work date required" }; + return; + } + + // Find the check-in record + let query = + "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?"; + const params: unknown[] = [employeeId, workDate, "CheckedIn"]; + + if (currentUser.role === "Supervisor") { + query += " AND supervisor_id = ?"; + params.push(currentUser.id); + } + + const records = await db.query(query, params); + + if (records.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "No check-in record found for today" }; + return; + } + + const checkOutTime = new Date().toISOString().slice(0, 19).replace( + "T", + " ", + ); + + await db.execute( + "UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?", + [checkOutTime, "CheckedOut", records[0].id], + ); + + const updatedRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [records[0].id], + ); + + ctx.response.body = updatedRecord[0]; + } catch (error) { + console.error("Check out error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); + +// Update attendance status (mark as Absent, HalfDay, Late) +router.put( + "/:id/status", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const attendanceId = ctx.params.id; + const body = await ctx.request.body + .json() as UpdateAttendanceStatusRequest; + const { status, remark } = body; + + // Validate status + const validStatuses: AttendanceStatus[] = [ + "CheckedIn", + "CheckedOut", + "Absent", + "HalfDay", + "Late", + ]; + if (!validStatuses.includes(status)) { + ctx.response.status = 400; + ctx.response.body = { + error: + "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late", + }; + return; + } + + // Check if record exists + const existing = await db.query( + "SELECT * FROM attendance WHERE id = ?", + [attendanceId], + ); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Attendance record not found" }; + return; + } + + // Update the status + await db.execute( + "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", + [status, remark || null, attendanceId], + ); + + const updatedRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [attendanceId], + ); + + ctx.response.body = updatedRecord[0]; + } catch (error) { + console.error("Update attendance status error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); + +// Mark employee as absent (create absent record) +router.post( + "/mark-absent", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json(); + const { employeeId, workDate, remark } = body; + + if (!employeeId || !workDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Employee ID and work date required" }; + return; + } + + // Check if record already exists for this date + const existing = await db.query( + "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?", + [employeeId, workDate], + ); + + if (existing.length > 0) { + // Update existing record to Absent + await db.execute( + "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", + ["Absent", remark || "Marked absent", existing[0].id], + ); + + const updatedRecord = await db.query( + `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, @@ -354,29 +386,68 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newRecord[0]; + [existing[0].id], + ); + + ctx.response.body = updatedRecord[0]; + } else { + // Create new absent record + const result = await db.execute( + "INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)", + [ + employeeId, + currentUser.id, + workDate, + "Absent", + remark || "Marked absent", + ], + ); + + const newRecord = await db.query( + `SELECT a.*, + e.name as employee_name, e.username as employee_username, + s.name as supervisor_name, + d.name as department_name, + c.name as contractor_name + FROM attendance a + JOIN users e ON a.employee_id = e.id + JOIN users s ON a.supervisor_id = s.id + LEFT JOIN departments d ON e.department_id = d.id + LEFT JOIN users c ON e.contractor_id = c.id + WHERE a.id = ?`, + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newRecord[0]; + } + } catch (error) { + console.error("Mark absent error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - } catch (error) { - console.error("Mark absent error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Get attendance summary -router.get("/summary/stats", authenticateToken, async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - const departmentId = params.get("departmentId"); - - let query = ` +router.get( + "/summary/stats", + authenticateToken, + async ( + ctx: RouterContext< + "/summary/stats", + Record, + State + >, + ) => { + try { + const currentUser: JWTPayload = getCurrentUser(ctx); + const params: URLSearchParams = ctx.request.url.searchParams; + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + const departmentId: string | null = params.get("departmentId"); + + let query: string = ` SELECT COUNT(DISTINCT a.employee_id) as total_employees, COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in, @@ -387,37 +458,38 @@ router.get("/summary/stats", authenticateToken, async (ctx) => { LEFT JOIN departments d ON e.department_id = d.id WHERE 1=1 `; - const queryParams: unknown[] = []; - - if (currentUser.role === "Supervisor") { - query += " AND a.supervisor_id = ?"; - queryParams.push(currentUser.id); + const queryParams: (number | string)[] = []; + + if (currentUser.role === "Supervisor") { + query += " AND a.supervisor_id = ?"; + queryParams.push(currentUser.id); + } + + if (startDate) { + query += " AND a.work_date >= ?"; + queryParams.push(startDate); + } + + if (endDate) { + query += " AND a.work_date <= ?"; + queryParams.push(endDate); + } + + if (departmentId) { + query += " AND e.department_id = ?"; + queryParams.push(departmentId); + } + + query += " GROUP BY d.id, d.name"; + + const summary = await db.query(query, queryParams); + ctx.response.body = summary; + } catch (error) { + console.error("Get attendance summary error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - if (startDate) { - query += " AND a.work_date >= ?"; - queryParams.push(startDate); - } - - if (endDate) { - query += " AND a.work_date <= ?"; - queryParams.push(endDate); - } - - if (departmentId) { - query += " AND e.department_id = ?"; - queryParams.push(departmentId); - } - - query += " GROUP BY d.id, d.name"; - - const summary = await db.query(query, queryParams); - ctx.response.body = summary; - } catch (error) { - console.error("Get attendance summary error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/auth.ts b/backend-deno/routes/auth.ts index 1089d23..1081761 100644 --- a/backend-deno/routes/auth.ts +++ b/backend-deno/routes/auth.ts @@ -1,16 +1,23 @@ import { Router } from "@oak/oak"; -import { hash, compare, genSalt } from "bcrypt"; +import { compare, genSalt, hash } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; -// Helper function to hash password with proper salt generation async function hashPassword(password: string): Promise { const salt = await genSalt(config.BCRYPT_ROUNDS); return await hash(password, salt); } -import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts"; -import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts"; -import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts"; +import { + authenticateToken, + generateToken, + getCurrentUser, +} from "../middleware/auth.ts"; +import { isStrongPassword, sanitizeInput } from "../middleware/security.ts"; +import type { + ChangePasswordRequest, + LoginRequest, + User, +} from "../types/index.ts"; const router = new Router(); @@ -19,41 +26,41 @@ router.post("/login", async (ctx) => { try { const body = await ctx.request.body.json() as LoginRequest; const { username, password } = body; - + // Input validation if (!username || !password) { ctx.response.status = 400; ctx.response.body = { error: "Username and password required" }; return; } - + // Sanitize input const sanitizedUsername = sanitizeInput(username); - + // Query user const users = await db.query( "SELECT * FROM users WHERE username = ? AND is_active = TRUE", - [sanitizedUsername] + [sanitizedUsername], ); - + if (users.length === 0) { // Use generic message to prevent user enumeration ctx.response.status = 401; ctx.response.body = { error: "Invalid credentials" }; return; } - + const user = users[0]; - + // Verify password const validPassword = await compare(password, user.password!); - + if (!validPassword) { ctx.response.status = 401; ctx.response.body = { error: "Invalid credentials" }; return; } - + // Generate JWT token const token = await generateToken({ id: user.id, @@ -61,10 +68,10 @@ router.post("/login", async (ctx) => { role: user.role, departmentId: user.department_id, }); - + // Return user data without password const { password: _, ...userWithoutPassword } = user; - + ctx.response.body = { token, user: userWithoutPassword, @@ -80,18 +87,18 @@ router.post("/login", async (ctx) => { router.get("/me", authenticateToken, async (ctx) => { try { const currentUser = getCurrentUser(ctx); - + const users = await db.query( "SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?", - [currentUser.id] + [currentUser.id], ); - + if (users.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "User not found" }; return; } - + ctx.response.body = users[0]; } catch (error) { console.error("Get user error:", error); @@ -106,14 +113,14 @@ router.post("/change-password", authenticateToken, async (ctx) => { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as ChangePasswordRequest; const { currentPassword, newPassword } = body; - + // Input validation if (!currentPassword || !newPassword) { ctx.response.status = 400; ctx.response.body = { error: "Current and new password required" }; return; } - + // Validate new password strength (only enforce in production or if explicitly enabled) if (config.isProduction()) { const passwordCheck = isStrongPassword(newPassword); @@ -127,37 +134,37 @@ router.post("/change-password", authenticateToken, async (ctx) => { ctx.response.body = { error: "Password must be at least 6 characters" }; return; } - + // Get current password hash const users = await db.query( "SELECT password FROM users WHERE id = ?", - [currentUser.id] + [currentUser.id], ); - + if (users.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "User not found" }; return; } - + // Verify current password const validPassword = await compare(currentPassword, users[0].password!); - + if (!validPassword) { ctx.response.status = 401; ctx.response.body = { error: "Current password is incorrect" }; return; } - + // Hash new password with configured rounds const hashedPassword = await hashPassword(newPassword); - + // Update password await db.execute( "UPDATE users SET password = ? WHERE id = ?", - [hashedPassword, currentUser.id] + [hashedPassword, currentUser.id], ); - + ctx.response.body = { message: "Password changed successfully" }; } catch (error) { console.error("Change password error:", error); diff --git a/backend-deno/routes/contractor-rates.ts b/backend-deno/routes/contractor-rates.ts index 6eedd96..4ecab6b 100644 --- a/backend-deno/routes/contractor-rates.ts +++ b/backend-deno/routes/contractor-rates.ts @@ -1,19 +1,32 @@ -import { Router } from "@oak/oak"; +import { Router, type RouterContext, type State } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; import { sanitizeInput } from "../middleware/security.ts"; -import type { ContractorRate, CreateContractorRateRequest, User } from "../types/index.ts"; +import type { + ContractorRate, + CreateContractorRateRequest, + User, +} from "../types/index.ts"; const router = new Router(); // Get contractor rates -router.get("/", authenticateToken, async (ctx) => { - try { - const params = ctx.request.url.searchParams; - const contractorId = params.get("contractorId"); - const subDepartmentId = params.get("subDepartmentId"); - - let query = ` +router.get( + "/", + authenticateToken, + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const params: URLSearchParams = ctx.request.url.searchParams; + const contractorId: string | null = params.get("contractorId"); + const subDepartmentId: string | null = params.get("subDepartmentId"); + + let query: string = ` SELECT cr.*, u.name as contractor_name, u.username as contractor_username, sd.name as sub_department_name, @@ -26,37 +39,47 @@ router.get("/", authenticateToken, async (ctx) => { LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity WHERE 1=1 `; - const queryParams: unknown[] = []; - - if (contractorId) { - query += " AND cr.contractor_id = ?"; - queryParams.push(contractorId); + const queryParams: unknown[] = []; + + if (contractorId) { + query += " AND cr.contractor_id = ?"; + queryParams.push(contractorId); + } + + if (subDepartmentId) { + query += " AND cr.sub_department_id = ?"; + queryParams.push(subDepartmentId); + } + + query += " ORDER BY cr.effective_date DESC, cr.created_at DESC"; + + const rates = await db.query(query, queryParams); + ctx.response.body = rates; + } catch (error) { + console.error("Get contractor rates error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - if (subDepartmentId) { - query += " AND cr.sub_department_id = ?"; - queryParams.push(subDepartmentId); - } - - query += " ORDER BY cr.effective_date DESC, cr.created_at DESC"; - - const rates = await db.query(query, queryParams); - ctx.response.body = rates; - } catch (error) { - console.error("Get contractor rates error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Get current rate for a contractor + sub-department combination -router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) => { - try { - const contractorId = ctx.params.contractorId; - const params = ctx.request.url.searchParams; - const subDepartmentId = params.get("subDepartmentId"); - - let query = ` +router.get( + "/contractor/:contractorId/current", + authenticateToken, + async ( + ctx: RouterContext< + "/contractor/:contractorId/current", + { contractorId: string } & Record, + State + >, + ) => { + try { + const contractorId = ctx.params.contractorId; + const params = ctx.request.url.searchParams; + const subDepartmentId = params.get("subDepartmentId"); + + let query: string = ` SELECT cr.*, u.name as contractor_name, u.username as contractor_username, sd.name as sub_department_name, @@ -67,72 +90,92 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) = LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity WHERE cr.contractor_id = ? `; - const queryParams: unknown[] = [contractorId]; - - if (subDepartmentId) { - query += " AND cr.sub_department_id = ?"; - queryParams.push(subDepartmentId); + const queryParams: unknown[] = [contractorId]; + + if (subDepartmentId) { + query += " AND cr.sub_department_id = ?"; + queryParams.push(subDepartmentId); + } + + query += " ORDER BY cr.effective_date DESC LIMIT 1"; + + const rates = await db.query(query, queryParams); + + if (rates.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "No rate found for contractor" }; + return; + } + + ctx.response.body = rates[0]; + } catch (error) { + console.error("Get current rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - query += " ORDER BY cr.effective_date DESC LIMIT 1"; - - const rates = await db.query(query, queryParams); - - if (rates.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "No rate found for contractor" }; - return; - } - - ctx.response.body = rates[0]; - } catch (error) { - console.error("Get current rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Set contractor rate (Supervisor or SuperAdmin) -router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as CreateContractorRateRequest; - const { contractorId, subDepartmentId, activity, rate, effectiveDate } = body; - - if (!contractorId || !rate || !effectiveDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Missing required fields (contractorId, rate, effectiveDate)" }; - return; - } - - // Verify contractor exists - const contractors = await db.query( - "SELECT * FROM users WHERE id = ? AND role = ?", - [contractorId, "Contractor"] - ); - - if (contractors.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Contractor not found" }; - return; - } - - // Supervisors can only set rates for contractors in their department - if (currentUser.role === "Supervisor" && contractors[0].department_id !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Contractor not in your department" }; - return; - } - - const sanitizedActivity = activity ? sanitizeInput(activity) : null; - - const result = await db.execute( - "INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)", - [contractorId, subDepartmentId || null, sanitizedActivity, rate, effectiveDate] - ); - - const newRate = await db.query( - `SELECT cr.*, +router.post( + "/", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CreateContractorRateRequest; + const { contractorId, subDepartmentId, activity, rate, effectiveDate } = + body; + + if (!contractorId || !rate || !effectiveDate) { + ctx.response.status = 400; + ctx.response.body = { + error: "Missing required fields (contractorId, rate, effectiveDate)", + }; + return; + } + + // Verify contractor exists + const contractors = await db.query( + "SELECT * FROM users WHERE id = ? AND role = ?", + [contractorId, "Contractor"], + ); + + if (contractors.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Contractor not found" }; + return; + } + + // Supervisors can only set rates for contractors in their department + if ( + currentUser.role === "Supervisor" && + contractors[0].department_id !== currentUser.departmentId + ) { + ctx.response.status = 403; + ctx.response.body = { error: "Contractor not in your department" }; + return; + } + + const sanitizedActivity = activity ? sanitizeInput(activity) : null; + + const result: { insertId: number; affectedRows: number } = await db + .execute( + "INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)", + [ + contractorId, + subDepartmentId || null, + sanitizedActivity, + rate, + effectiveDate, + ], + ); + + const newRate: ContractorRate[] = await db.query( + `SELECT cr.*, u.name as contractor_name, u.username as contractor_username, sd.name as sub_department_name, a.unit_of_measurement @@ -141,67 +184,82 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity WHERE cr.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newRate[0]; - } catch (error) { - console.error("Set contractor rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newRate[0]; + } catch (error) { + console.error("Set contractor rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Update contractor rate -router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const rateId = ctx.params.id; - const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string }; - const { rate, activity, effectiveDate } = body; - - const existing = await db.query( - "SELECT * FROM contractor_rates WHERE id = ?", - [rateId] - ); - - if (existing.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Rate not found" }; - return; - } - - const updates: string[] = []; - const params: unknown[] = []; - - if (rate !== undefined) { - updates.push("rate = ?"); - params.push(rate); - } - if (activity !== undefined) { - updates.push("activity = ?"); - params.push(sanitizeInput(activity)); - } - if (effectiveDate !== undefined) { - updates.push("effective_date = ?"); - params.push(effectiveDate); - } - - if (updates.length === 0) { - ctx.response.status = 400; - ctx.response.body = { error: "No fields to update" }; - return; - } - - params.push(rateId); - - await db.execute( - `UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`, - params - ); - - const updatedRate = await db.query( - `SELECT cr.*, +router.put( + "/:id", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async ( + ctx: RouterContext< + "/:id", + { id: string } & Record, + State + >, + ) => { + try { + const rateId = ctx.params.id; + const body = await ctx.request.body.json() as { + rate?: number; + activity?: string; + effectiveDate?: string; + }; + const { rate, activity, effectiveDate } = body; + + const existing = await db.query( + "SELECT * FROM contractor_rates WHERE id = ?", + [rateId], + ); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Rate not found" }; + return; + } + + const updates: string[] = []; + const params: unknown[] = []; + + if (rate !== undefined) { + updates.push("rate = ?"); + params.push(rate); + } + if (activity !== undefined) { + updates.push("activity = ?"); + params.push(sanitizeInput(activity)); + } + if (effectiveDate !== undefined) { + updates.push("effective_date = ?"); + params.push(effectiveDate); + } + + if (updates.length === 0) { + ctx.response.status = 400; + ctx.response.body = { error: "No fields to update" }; + return; + } + + params.push(rateId); + + await db.execute( + `UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`, + params, + ); + + const updatedRate = await db.query( + `SELECT cr.*, u.name as contractor_name, u.username as contractor_username, sd.name as sub_department_name, a.unit_of_measurement @@ -210,40 +268,52 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity WHERE cr.id = ?`, - [rateId] - ); - - ctx.response.body = updatedRate[0]; - } catch (error) { - console.error("Update contractor rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [rateId], + ); + + ctx.response.body = updatedRate[0]; + } catch (error) { + console.error("Update contractor rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Delete contractor rate -router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const rateId = ctx.params.id; - - const existing = await db.query( - "SELECT * FROM contractor_rates WHERE id = ?", - [rateId] - ); - - if (existing.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Rate not found" }; - return; +router.delete( + "/:id", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async ( + ctx: RouterContext< + "/:id", + { id: string } & Record, + State + >, + ) => { + try { + const rateId = ctx.params.id; + + const existing = await db.query( + "SELECT * FROM contractor_rates WHERE id = ?", + [rateId], + ); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Rate not found" }; + return; + } + + await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]); + ctx.response.body = { message: "Rate deleted successfully" }; + } catch (error) { + console.error("Delete contractor rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]); - ctx.response.body = { message: "Rate deleted successfully" }; - } catch (error) { - console.error("Delete contractor rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/departments.ts b/backend-deno/routes/departments.ts index b1d7245..51d86e9 100644 --- a/backend-deno/routes/departments.ts +++ b/backend-deno/routes/departments.ts @@ -1,16 +1,20 @@ -import { Router } from "@oak/oak"; +import { type Context, Router, type RouterContext } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; import { sanitizeInput } from "../middleware/security.ts"; import type { Department, SubDepartment } from "../types/index.ts"; const router = new Router(); // Get all departments -router.get("/", authenticateToken, async (ctx) => { +router.get("/", authenticateToken, async (ctx: Context) => { try { const departments = await db.query( - "SELECT * FROM departments ORDER BY name" + "SELECT * FROM departments ORDER BY name", ); ctx.response.body = departments; } catch (error) { @@ -21,21 +25,21 @@ router.get("/", authenticateToken, async (ctx) => { }); // Get department by ID -router.get("/:id", authenticateToken, async (ctx) => { +router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => { try { const deptId = ctx.params.id; - + const departments = await db.query( "SELECT * FROM departments WHERE id = ?", - [deptId] + [deptId], ); - + if (departments.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Department not found" }; return; } - + ctx.response.body = departments[0]; } catch (error) { console.error("Get department error:", error); @@ -44,70 +48,101 @@ router.get("/:id", authenticateToken, async (ctx) => { } }); +// Get all sub-departments (for reporting/filtering) +router.get( + "/sub-departments/all", + authenticateToken, + async (ctx: Context) => { + try { + const subDepartments = await db.query( + "SELECT sd.*, d.name as department_name FROM sub_departments sd LEFT JOIN departments d ON sd.department_id = d.id ORDER BY d.name, sd.name", + ); + + ctx.response.body = subDepartments; + } catch (error) { + console.error("Get all sub-departments error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); + // Get sub-departments by department ID -router.get("/:id/sub-departments", authenticateToken, async (ctx) => { - try { - const deptId = ctx.params.id; - - const subDepartments = await db.query( - "SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name", - [deptId] - ); - - ctx.response.body = subDepartments; - } catch (error) { - console.error("Get sub-departments error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); +router.get( + "/:id/sub-departments", + authenticateToken, + async (ctx: RouterContext<"/:id/sub-departments">) => { + try { + const deptId = ctx.params.id; + + const subDepartments = await db.query( + "SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name", + [deptId], + ); + + ctx.response.body = subDepartments; + } catch (error) { + console.error("Get sub-departments error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Create department (SuperAdmin only) -router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const body = await ctx.request.body.json() as { name: string }; - const { name } = body; - - if (!name) { - ctx.response.status = 400; - ctx.response.body = { error: "Department name required" }; - return; +router.post( + "/", + authenticateToken, + authorize("SuperAdmin"), + async (ctx: Context) => { + try { + const body = await ctx.request.body.json() as { name: string }; + const { name } = body; + + if (!name) { + ctx.response.status = 400; + ctx.response.body = { error: "Department name required" }; + return; + } + + const sanitizedName = sanitizeInput(name); + + const result = await db.execute( + "INSERT INTO departments (name) VALUES (?)", + [sanitizedName], + ); + + const newDepartment = await db.query( + "SELECT * FROM departments WHERE id = ?", + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newDepartment[0]; + } catch (error) { + const err = error as { code?: string }; + if (err.code === "ER_DUP_ENTRY") { + ctx.response.status = 400; + ctx.response.body = { error: "Department already exists" }; + return; + } + console.error("Create department error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const sanitizedName = sanitizeInput(name); - - const result = await db.execute( - "INSERT INTO departments (name) VALUES (?)", - [sanitizedName] - ); - - const newDepartment = await db.query( - "SELECT * FROM departments WHERE id = ?", - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newDepartment[0]; - } catch (error) { - const err = error as { code?: string }; - if (err.code === "ER_DUP_ENTRY") { - ctx.response.status = 400; - ctx.response.body = { error: "Department already exists" }; - return; - } - console.error("Create department error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Create sub-department (SuperAdmin or Supervisor for their own department) -router.post("/sub-departments", authenticateToken, async (ctx) => { +router.post("/sub-departments", authenticateToken, async (ctx: Context) => { try { const user = getCurrentUser(ctx); - const body = await ctx.request.body.json() as { department_id: number; name: string }; + const body = await ctx.request.body.json() as { + department_id: number; + name: string; + }; const { department_id, name } = body; - + if (!name || !department_id) { ctx.response.status = 400; ctx.response.body = { error: "Department ID and name are required" }; @@ -115,37 +150,41 @@ router.post("/sub-departments", authenticateToken, async (ctx) => { } // Check authorization - if (user.role === 'Supervisor' && user.departmentId !== department_id) { + if (user.role === "Supervisor" && user.departmentId !== department_id) { ctx.response.status = 403; - ctx.response.body = { error: "You can only create sub-departments for your own department" }; + ctx.response.body = { + error: "You can only create sub-departments for your own department", + }; return; } - - if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { + + if (user.role !== "SuperAdmin" && user.role !== "Supervisor") { ctx.response.status = 403; ctx.response.body = { error: "Unauthorized" }; return; } - + const sanitizedName = sanitizeInput(name); - + const result = await db.execute( "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", - [department_id, sanitizedName] + [department_id, sanitizedName], ); - + const newSubDepartment = await db.query( "SELECT * FROM sub_departments WHERE id = ?", - [result.lastInsertId] + [result.insertId], ); - + ctx.response.status = 201; ctx.response.body = newSubDepartment[0]; } catch (error) { const err = error as { code?: string }; if (err.code === "ER_DUP_ENTRY") { ctx.response.status = 400; - ctx.response.body = { error: "Sub-department already exists in this department" }; + ctx.response.body = { + error: "Sub-department already exists in this department", + }; return; } console.error("Create sub-department error:", error); @@ -155,90 +194,108 @@ router.post("/sub-departments", authenticateToken, async (ctx) => { }); // Delete sub-department (SuperAdmin or Supervisor for their own department) -router.delete("/sub-departments/:id", authenticateToken, async (ctx) => { - try { - const user = getCurrentUser(ctx); - const subDeptId = ctx.params.id; - - // Get the sub-department to check department ownership - const subDepts = await db.query( - "SELECT * FROM sub_departments WHERE id = ?", - [subDeptId] - ); - - if (subDepts.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Sub-department not found" }; - return; +router.delete( + "/sub-departments/:id", + authenticateToken, + async (ctx: RouterContext<"/sub-departments/:id">) => { + try { + const user = getCurrentUser(ctx); + const subDeptId = ctx.params.id; + + // Get the sub-department to check department ownership + const subDepts = await db.query( + "SELECT * FROM sub_departments WHERE id = ?", + [subDeptId], + ); + + if (subDepts.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Sub-department not found" }; + return; + } + + const subDept = subDepts[0]; + + // Check authorization + if ( + user.role === "Supervisor" && + user.departmentId !== subDept.department_id + ) { + ctx.response.status = 403; + ctx.response.body = { + error: "You can only delete sub-departments from your own department", + }; + return; + } + + if (user.role !== "SuperAdmin" && user.role !== "Supervisor") { + ctx.response.status = 403; + ctx.response.body = { error: "Unauthorized" }; + return; + } + + // Delete associated activities first (cascade should handle this, but being explicit) + await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [ + subDeptId, + ]); + + // Delete the sub-department + await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]); + + ctx.response.body = { message: "Sub-department deleted successfully" }; + } catch (error) { + console.error("Delete sub-department error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const subDept = subDepts[0]; - - // Check authorization - if (user.role === 'Supervisor' && user.departmentId !== subDept.department_id) { - ctx.response.status = 403; - ctx.response.body = { error: "You can only delete sub-departments from your own department" }; - return; - } - - if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { - ctx.response.status = 403; - ctx.response.body = { error: "Unauthorized" }; - return; - } - - // Delete associated activities first (cascade should handle this, but being explicit) - await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [subDeptId]); - - // Delete the sub-department - await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]); - - ctx.response.body = { message: "Sub-department deleted successfully" }; - } catch (error) { - console.error("Delete sub-department error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Legacy route for creating sub-department under specific department (SuperAdmin only) -router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const deptId = ctx.params.id; - const body = await ctx.request.body.json() as { name: string }; - const { name } = body; - - if (!name) { - ctx.response.status = 400; - ctx.response.body = { error: "Name is required" }; - return; +router.post( + "/:id/sub-departments", + authenticateToken, + authorize("SuperAdmin"), + async (ctx: RouterContext<"/:id/sub-departments">) => { + try { + const deptId: string | number = ctx.params.id; + const body = await ctx.request.body.json() as { name: string }; + const { name } = body; + + if (!name) { + ctx.response.status = 400; + ctx.response.body = { error: "Name is required" }; + return; + } + + const sanitizedName = sanitizeInput(name); + + const result = await db.execute( + "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", + [deptId, sanitizedName], + ); + + const newSubDepartment = await db.query( + "SELECT * FROM sub_departments WHERE id = ?", + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newSubDepartment[0]; + } catch (error) { + const err = error as { code?: string }; + if (err.code === "ER_DUP_ENTRY") { + ctx.response.status = 400; + ctx.response.body = { + error: "Sub-department already exists in this department", + }; + return; + } + console.error("Create sub-department error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const sanitizedName = sanitizeInput(name); - - const result = await db.execute( - "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", - [deptId, sanitizedName] - ); - - const newSubDepartment = await db.query( - "SELECT * FROM sub_departments WHERE id = ?", - [result.lastInsertId] - ); - - ctx.response.status = 201; - ctx.response.body = newSubDepartment[0]; - } catch (error) { - const err = error as { code?: string }; - if (err.code === "ER_DUP_ENTRY") { - ctx.response.status = 400; - ctx.response.body = { error: "Sub-department already exists in this department" }; - return; - } - console.error("Create sub-department error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/employee-swaps.ts b/backend-deno/routes/employee-swaps.ts index b4e4e9c..1d84fa7 100644 --- a/backend-deno/routes/employee-swaps.ts +++ b/backend-deno/routes/employee-swaps.ts @@ -1,20 +1,30 @@ -import { Router } from "@oak/oak"; +import { Router, type RouterContext, type State } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; -import type { EmployeeSwap, CreateSwapRequest, User } from "../types/index.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; +import type { CreateSwapRequest, EmployeeSwap, User } from "../types/index.ts"; const router = new Router(); // Get all employee swaps (SuperAdmin only) -router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const params = ctx.request.url.searchParams; - const status = params.get("status"); - const employeeId = params.get("employeeId"); - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - - let query = ` +router.get( + "/", + authenticateToken, + authorize("SuperAdmin"), + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const params: URLSearchParams = ctx.request.url.searchParams; + const status: string | null = params.get("status"); + const employeeId: string | null = params.get("employeeId"); + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + + let query = ` SELECT es.*, e.name as employee_name, od.name as original_department_name, @@ -31,46 +41,57 @@ router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { JOIN users sb ON es.swapped_by = sb.id WHERE 1=1 `; - const queryParams: unknown[] = []; - - if (status) { - query += " AND es.status = ?"; - queryParams.push(status); + const queryParams: unknown[] = []; + + if (status) { + query += " AND es.status = ?"; + queryParams.push(status); + } + + if (employeeId) { + query += " AND es.employee_id = ?"; + queryParams.push(employeeId); + } + + if (startDate) { + query += " AND es.swap_date >= ?"; + queryParams.push(startDate); + } + + if (endDate) { + query += " AND es.swap_date <= ?"; + queryParams.push(endDate); + } + + query += " ORDER BY es.created_at DESC"; + + const swaps = await db.query(query, queryParams); + ctx.response.body = swaps; + } catch (error) { + console.error("Get employee swaps error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - if (employeeId) { - query += " AND es.employee_id = ?"; - queryParams.push(employeeId); - } - - if (startDate) { - query += " AND es.swap_date >= ?"; - queryParams.push(startDate); - } - - if (endDate) { - query += " AND es.swap_date <= ?"; - queryParams.push(endDate); - } - - query += " ORDER BY es.created_at DESC"; - - const swaps = await db.query(query, queryParams); - ctx.response.body = swaps; - } catch (error) { - console.error("Get employee swaps error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Get swap by ID -router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const swapId = ctx.params.id; - - const swaps = await db.query( - `SELECT es.*, +router.get( + "/:id", + authenticateToken, + authorize("SuperAdmin"), + async ( + ctx: RouterContext< + "/:id", + { id: string } & Record, + State + >, + ) => { + try { + const swapId = ctx.params.id; + + const swaps = await db.query( + `SELECT es.*, e.name as employee_name, od.name as original_department_name, td.name as target_department_name, @@ -85,45 +106,49 @@ router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => { LEFT JOIN users tc ON es.target_contractor_id = tc.id JOIN users sb ON es.swapped_by = sb.id WHERE es.id = ?`, - [swapId] - ); - - if (swaps.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Swap record not found" }; - return; + [swapId], + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Swap record not found" }; + return; + } + + ctx.response.body = swaps[0]; + } catch (error) { + console.error("Get swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - ctx.response.body = swaps[0]; - } catch (error) { - console.error("Get swap error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Create new employee swap (SuperAdmin only) router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as CreateSwapRequest; - const { - employeeId, - targetDepartmentId, - targetContractorId, - swapReason, - reasonDetails, + const { + employeeId, + targetDepartmentId, + targetContractorId, + swapReason, + reasonDetails, workCompletionPercentage, - swapDate + swapDate, } = body; - + // Validate required fields if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) { ctx.response.status = 400; - ctx.response.body = { error: "Employee ID, target department, swap reason, and swap date are required" }; + ctx.response.body = { + error: + "Employee ID, target department, swap reason, and swap date are required", + }; return; } - + // Validate swap reason const validReasons = ["LeftWork", "Sick", "FinishedEarly", "Other"]; if (!validReasons.includes(swapReason)) { @@ -131,87 +156,108 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { ctx.response.body = { error: "Invalid swap reason" }; return; } - + // Get employee's current department and contractor const employees = await db.query( "SELECT * FROM users WHERE id = ? AND role = 'Employee'", - [employeeId] + [employeeId], ); - + if (employees.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Employee not found" }; return; } - + const employee = employees[0]; - + if (!employee.department_id) { ctx.response.status = 400; ctx.response.body = { error: "Employee has no current department" }; return; } - + // Check if there's already an active swap for this employee const activeSwaps = await db.query( "SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'", - [employeeId] + [employeeId], ); - + if (activeSwaps.length > 0) { ctx.response.status = 400; - ctx.response.body = { error: "Employee already has an active swap. Complete or cancel it first." }; + ctx.response.body = { + error: + "Employee already has an active swap. Complete or cancel it first.", + }; return; } - - // Create the swap record - const result = await db.execute( - `INSERT INTO employee_swaps - (employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id, - swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`, - [ - employeeId, - employee.department_id, - targetDepartmentId, - employee.contractor_id || null, - targetContractorId || null, - swapReason, - reasonDetails || null, - workCompletionPercentage || 0, - swapDate, - currentUser.id - ] - ); - - // Update the employee's department and contractor - await db.execute( - "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", - [targetDepartmentId, targetContractorId || null, employeeId] - ); - - // Fetch the created swap - const newSwap = await db.query( - `SELECT es.*, - e.name as employee_name, - od.name as original_department_name, - td.name as target_department_name, - oc.name as original_contractor_name, - tc.name as target_contractor_name, - sb.name as swapped_by_name - FROM employee_swaps es - JOIN users e ON es.employee_id = e.id - JOIN departments od ON es.original_department_id = od.id - JOIN departments td ON es.target_department_id = td.id - LEFT JOIN users oc ON es.original_contractor_id = oc.id - LEFT JOIN users tc ON es.target_contractor_id = tc.id - JOIN users sb ON es.swapped_by = sb.id - WHERE es.id = ?`, - [result.insertId] - ); - + + // Use transaction to ensure both operations succeed or fail together + const newSwap = await db.transaction(async (connection) => { + // Create the swap record + const [insertResult] = await connection.execute( + `INSERT INTO employee_swaps + (employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id, + swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`, + [ + employeeId, + employee.department_id, + targetDepartmentId, + employee.contractor_id || null, + targetContractorId || null, + swapReason, + reasonDetails || null, + workCompletionPercentage || 0, + swapDate, + currentUser.id, + ], + ); + + const swapInsertId = (insertResult as { insertId: number }).insertId; + + if (!swapInsertId) { + throw new Error("Failed to create swap record"); + } + + // Update the employee's department and contractor + const [updateResult] = await connection.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [targetDepartmentId, targetContractorId || null, employeeId], + ); + + const affectedRows = + (updateResult as { affectedRows: number }).affectedRows; + + if (affectedRows === 0) { + throw new Error("Failed to update employee department"); + } + + // Fetch the created swap + const [swapRows] = await connection.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapInsertId], + ); + + return (swapRows as EmployeeSwap[])[0]; + }); + ctx.response.status = 201; - ctx.response.body = newSwap[0]; + ctx.response.body = newSwap; } catch (error) { console.error("Create swap error:", error); ctx.response.status = 500; @@ -220,121 +266,149 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { }); // Complete a swap (return employee to original department) -router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const swapId = ctx.params.id; - - // Get the swap record - const swaps = await db.query( - "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", - [swapId] - ); - - if (swaps.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Active swap not found" }; - return; +router.put( + "/:id/complete", + authenticateToken, + authorize("SuperAdmin"), + async (ctx) => { + try { + const swapId = ctx.params.id; + + // Get the swap record + const swaps = await db.query( + "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", + [swapId], + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Active swap not found" }; + return; + } + + const swap = swaps[0]; + + // Use transaction to ensure both operations succeed or fail together + const updatedSwap = await db.transaction(async (connection) => { + // Return employee to original department and contractor + await connection.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [ + swap.original_department_id, + swap.original_contractor_id, + swap.employee_id, + ], + ); + + // Mark swap as completed + await connection.execute( + "UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?", + [swapId], + ); + + // Fetch updated swap + const [swapRows] = await connection.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapId], + ); + + return (swapRows as EmployeeSwap[])[0]; + }); + + ctx.response.body = updatedSwap; + } catch (error) { + console.error("Complete swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const swap = swaps[0]; - - // Return employee to original department and contractor - await db.execute( - "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", - [swap.original_department_id, swap.original_contractor_id, swap.employee_id] - ); - - // Mark swap as completed - await db.execute( - "UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?", - [swapId] - ); - - // Fetch updated swap - const updatedSwap = await db.query( - `SELECT es.*, - e.name as employee_name, - od.name as original_department_name, - td.name as target_department_name, - oc.name as original_contractor_name, - tc.name as target_contractor_name, - sb.name as swapped_by_name - FROM employee_swaps es - JOIN users e ON es.employee_id = e.id - JOIN departments od ON es.original_department_id = od.id - JOIN departments td ON es.target_department_id = td.id - LEFT JOIN users oc ON es.original_contractor_id = oc.id - LEFT JOIN users tc ON es.target_contractor_id = tc.id - JOIN users sb ON es.swapped_by = sb.id - WHERE es.id = ?`, - [swapId] - ); - - ctx.response.body = updatedSwap[0]; - } catch (error) { - console.error("Complete swap error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Cancel a swap (return employee to original department) -router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const swapId = ctx.params.id; - - // Get the swap record - const swaps = await db.query( - "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", - [swapId] - ); - - if (swaps.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Active swap not found" }; - return; +router.put( + "/:id/cancel", + authenticateToken, + authorize("SuperAdmin"), + async (ctx) => { + try { + const swapId = ctx.params.id; + + // Get the swap record + const swaps = await db.query( + "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", + [swapId], + ); + + if (swaps.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Active swap not found" }; + return; + } + + const swap = swaps[0]; + + // Use transaction to ensure both operations succeed or fail together + const updatedSwap = await db.transaction(async (connection) => { + // Return employee to original department and contractor + await connection.execute( + "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", + [ + swap.original_department_id, + swap.original_contractor_id, + swap.employee_id, + ], + ); + + // Mark swap as cancelled + await connection.execute( + "UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?", + [swapId], + ); + + // Fetch updated swap + const [swapRows] = await connection.query( + `SELECT es.*, + e.name as employee_name, + od.name as original_department_name, + td.name as target_department_name, + oc.name as original_contractor_name, + tc.name as target_contractor_name, + sb.name as swapped_by_name + FROM employee_swaps es + JOIN users e ON es.employee_id = e.id + JOIN departments od ON es.original_department_id = od.id + JOIN departments td ON es.target_department_id = td.id + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id + JOIN users sb ON es.swapped_by = sb.id + WHERE es.id = ?`, + [swapId], + ); + + return (swapRows as EmployeeSwap[])[0]; + }); + + ctx.response.body = updatedSwap; + } catch (error) { + console.error("Cancel swap error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const swap = swaps[0]; - - // Return employee to original department and contractor - await db.execute( - "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", - [swap.original_department_id, swap.original_contractor_id, swap.employee_id] - ); - - // Mark swap as cancelled - await db.execute( - "UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?", - [swapId] - ); - - // Fetch updated swap - const updatedSwap = await db.query( - `SELECT es.*, - e.name as employee_name, - od.name as original_department_name, - td.name as target_department_name, - oc.name as original_contractor_name, - tc.name as target_contractor_name, - sb.name as swapped_by_name - FROM employee_swaps es - JOIN users e ON es.employee_id = e.id - JOIN departments od ON es.original_department_id = od.id - JOIN departments td ON es.target_department_id = td.id - LEFT JOIN users oc ON es.original_contractor_id = oc.id - LEFT JOIN users tc ON es.target_contractor_id = tc.id - JOIN users sb ON es.swapped_by = sb.id - WHERE es.id = ?`, - [swapId] - ); - - ctx.response.body = updatedSwap[0]; - } catch (error) { - console.error("Cancel swap error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/reports.ts b/backend-deno/routes/reports.ts index b83cb11..41f4eaa 100644 --- a/backend-deno/routes/reports.ts +++ b/backend-deno/routes/reports.ts @@ -1,22 +1,30 @@ import { Router } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; -import type { WorkAllocation } from "../types/index.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; +import type { JWTPayload, WorkAllocation } from "../types/index.ts"; const router = new Router(); // Get completed work allocations for reporting (with optional filters) -router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - const departmentId = params.get("departmentId"); - const contractorId = params.get("contractorId"); - const employeeId = params.get("employeeId"); - - let query = ` +router.get( + "/completed-allocations", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser: JWTPayload = getCurrentUser(ctx); + const params: URLSearchParams = ctx.request.url.searchParams; + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + const departmentId: string | null = params.get("departmentId"); + const contractorId: string | null = params.get("contractorId"); + const employeeId: string | null = params.get("employeeId"); + + let query = ` SELECT wa.*, e.name as employee_name, e.username as employee_username, e.phone_number as employee_phone, @@ -33,95 +41,110 @@ router.get("/completed-allocations", authenticateToken, authorize("Supervisor", LEFT JOIN departments d ON e.department_id = d.id WHERE wa.status = 'Completed' `; - const queryParams: unknown[] = []; - - // Role-based filtering - Supervisors can only see their department - if (currentUser.role === "Supervisor") { - query += " AND e.department_id = ?"; - queryParams.push(currentUser.departmentId); - } - - // Date range filter - if (startDate) { - query += " AND wa.completion_date >= ?"; - queryParams.push(startDate); - } - - if (endDate) { - query += " AND wa.completion_date <= ?"; - queryParams.push(endDate); - } - - // Department filter (for SuperAdmin) - if (departmentId && currentUser.role === "SuperAdmin") { - query += " AND e.department_id = ?"; - queryParams.push(departmentId); - } - - // Contractor filter - if (contractorId) { - query += " AND wa.contractor_id = ?"; - queryParams.push(contractorId); - } - - // Employee filter - if (employeeId) { - query += " AND wa.employee_id = ?"; - queryParams.push(employeeId); - } - - query += " ORDER BY wa.completion_date DESC, wa.created_at DESC"; - - const allocations = await db.query(query, queryParams); - - // Calculate summary stats - const totalAllocations = allocations.length; - const totalAmount = allocations.reduce((sum, a) => sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0); - const totalUnits = allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0); - - ctx.response.body = { - allocations, - summary: { - totalAllocations, - totalAmount: totalAmount.toFixed(2), - totalUnits: totalUnits.toFixed(2), + const queryParams: unknown[] = []; + + // Role-based filtering - Supervisors can only see their department + if (currentUser.role === "Supervisor") { + query += " AND e.department_id = ?"; + queryParams.push(currentUser.departmentId); } - }; - } catch (error) { - console.error("Get completed allocations report error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + + // Date range filter + if (startDate) { + query += " AND wa.completion_date >= ?"; + queryParams.push(startDate); + } + + if (endDate) { + query += " AND wa.completion_date <= ?"; + queryParams.push(endDate); + } + + // Department filter (for SuperAdmin) + if (departmentId && currentUser.role === "SuperAdmin") { + query += " AND e.department_id = ?"; + queryParams.push(departmentId); + } + + // Contractor filter + if (contractorId) { + query += " AND wa.contractor_id = ?"; + queryParams.push(contractorId); + } + + // Employee filter + if (employeeId) { + query += " AND wa.employee_id = ?"; + queryParams.push(employeeId); + } + + query += " ORDER BY wa.completion_date DESC, wa.created_at DESC"; + + const allocations = await db.query(query, queryParams); + + // Calculate summary stats + const totalAllocations = allocations.length; + const totalAmount = allocations.reduce( + (sum, a) => + sum + + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || + 0), + 0, + ); + const totalUnits = allocations.reduce( + (sum, a) => sum + (parseFloat(String(a.units)) || 0), + 0, + ); + + ctx.response.body = { + allocations, + summary: { + totalAllocations, + totalAmount: totalAmount.toFixed(2), + totalUnits: totalUnits.toFixed(2), + }, + }; + } catch (error) { + console.error("Get completed allocations report error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Get summary statistics for completed work -router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - - let departmentFilter = ""; - const queryParams: unknown[] = []; - - if (currentUser.role === "Supervisor") { - departmentFilter = " AND e.department_id = ?"; - queryParams.push(currentUser.departmentId); - } - - let dateFilter = ""; - if (startDate) { - dateFilter += " AND wa.completion_date >= ?"; - queryParams.push(startDate); - } - if (endDate) { - dateFilter += " AND wa.completion_date <= ?"; - queryParams.push(endDate); - } - - // Get summary by contractor - const byContractor = await db.query(` +router.get( + "/summary", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser: JWTPayload = getCurrentUser(ctx); + const params: URLSearchParams = ctx.request.url.searchParams; + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + + let departmentFilter = ""; + const queryParams: unknown[] = []; + + if (currentUser.role === "Supervisor") { + departmentFilter = " AND e.department_id = ?"; + queryParams.push(currentUser.departmentId); + } + + let dateFilter = ""; + if (startDate) { + dateFilter += " AND wa.completion_date >= ?"; + queryParams.push(startDate); + } + if (endDate) { + dateFilter += " AND wa.completion_date <= ?"; + queryParams.push(endDate); + } + + // Get summary by contractor + const byContractor = await db.query( + ` SELECT c.id as contractor_id, c.name as contractor_name, @@ -134,10 +157,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} GROUP BY c.id, c.name ORDER BY total_amount DESC - `, queryParams); - - // Get summary by sub-department - const bySubDepartment = await db.query(` + `, + queryParams, + ); + + // Get summary by sub-department + const bySubDepartment = await db.query( + ` SELECT sd.id as sub_department_id, sd.name as sub_department_name, @@ -152,10 +178,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} GROUP BY sd.id, sd.name, d.name ORDER BY total_amount DESC - `, queryParams); - - // Get summary by activity type - const byActivity = await db.query(` + `, + queryParams, + ); + + // Get summary by activity type + const byActivity = await db.query( + ` SELECT COALESCE(wa.activity, 'Standard') as activity, COUNT(*) as total_allocations, @@ -166,18 +195,21 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} GROUP BY wa.activity ORDER BY total_amount DESC - `, queryParams); - - ctx.response.body = { - byContractor, - bySubDepartment, - byActivity, - }; - } catch (error) { - console.error("Get report summary error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + `, + queryParams, + ); + + ctx.response.body = { + byContractor, + bySubDepartment, + byActivity, + }; + } catch (error) { + console.error("Get report summary error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); export default router; diff --git a/backend-deno/routes/standard-rates.ts b/backend-deno/routes/standard-rates.ts index 1570743..d8be863 100644 --- a/backend-deno/routes/standard-rates.ts +++ b/backend-deno/routes/standard-rates.ts @@ -1,7 +1,12 @@ import { Router } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; import { sanitizeInput } from "../middleware/security.ts"; +import type { Context } from "@oak/oak"; const router = new Router(); @@ -21,14 +26,16 @@ interface StandardRate { } // Get all standard rates (default rates for comparison) -router.get("/", authenticateToken, async (ctx) => { +router.get("/", authenticateToken, async (ctx: Context) => { try { const currentUser = getCurrentUser(ctx); const params = ctx.request.url.searchParams; - const departmentId = params.get("departmentId"); - const subDepartmentId = params.get("subDepartmentId"); - const activity = params.get("activity"); - + const departmentId: string | number | null = params.get("departmentId"); + const subDepartmentId: string | number | null = params.get( + "subDepartmentId", + ); + const activity: string | null = params.get("activity"); + let query = ` SELECT sr.*, sd.name as sub_department_name, @@ -44,30 +51,30 @@ router.get("/", authenticateToken, async (ctx) => { WHERE 1=1 `; const queryParams: unknown[] = []; - + // Supervisors can only see rates for their department if (currentUser.role === "Supervisor") { query += " AND d.id = ?"; queryParams.push(currentUser.departmentId); } - + if (departmentId) { query += " AND d.id = ?"; queryParams.push(departmentId); } - + if (subDepartmentId) { query += " AND sr.sub_department_id = ?"; queryParams.push(subDepartmentId); } - + if (activity) { query += " AND sr.activity = ?"; queryParams.push(activity); } - + query += " ORDER BY sr.effective_date DESC, sr.created_at DESC"; - + const rates = await db.query(query, queryParams); ctx.response.body = rates; } catch (error) { @@ -78,15 +85,19 @@ router.get("/", authenticateToken, async (ctx) => { }); // Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date -router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => { - try { - const params = ctx.request.url.searchParams; - const departmentId = params.get("departmentId"); - const startDate = params.get("startDate"); - const endDate = params.get("endDate"); - - // Get contractor rates - let contractorQuery = ` +router.get( + "/all-rates", + authenticateToken, + authorize("SuperAdmin"), + async (ctx: Context) => { + try { + const params = ctx.request.url.searchParams; + const departmentId: string | number | null = params.get("departmentId"); + const startDate: string | null = params.get("startDate"); + const endDate: string | null = params.get("endDate"); + + // Get contractor rates + let contractorQuery = ` SELECT cr.id, 'contractor' as rate_type, @@ -108,25 +119,25 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) LEFT JOIN departments d ON sd.department_id = d.id WHERE 1=1 `; - const contractorParams: unknown[] = []; - - if (departmentId) { - contractorQuery += " AND d.id = ?"; - contractorParams.push(departmentId); - } - - if (startDate) { - contractorQuery += " AND cr.effective_date >= ?"; - contractorParams.push(startDate); - } - - if (endDate) { - contractorQuery += " AND cr.effective_date <= ?"; - contractorParams.push(endDate); - } - - // Get standard rates - let standardQuery = ` + const contractorParams: unknown[] = []; + + if (departmentId) { + contractorQuery += " AND d.id = ?"; + contractorParams.push(departmentId); + } + + if (startDate) { + contractorQuery += " AND cr.effective_date >= ?"; + contractorParams.push(startDate); + } + + if (endDate) { + contractorQuery += " AND cr.effective_date <= ?"; + contractorParams.push(endDate); + } + + // Get standard rates + let standardQuery = ` SELECT sr.id, 'standard' as rate_type, @@ -148,66 +159,77 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) LEFT JOIN users u ON sr.created_by = u.id WHERE 1=1 `; - const standardParams: unknown[] = []; - - if (departmentId) { - standardQuery += " AND d.id = ?"; - standardParams.push(departmentId); - } - - if (startDate) { - standardQuery += " AND sr.effective_date >= ?"; - standardParams.push(startDate); - } - - if (endDate) { - standardQuery += " AND sr.effective_date <= ?"; - standardParams.push(endDate); - } - - const contractorRates = await db.query(contractorQuery, contractorParams); - const standardRates = await db.query(standardQuery, standardParams); - - // Combine and sort by date - const allRates = [...contractorRates, ...standardRates].sort((a, b) => { - const dateA = new Date(a.effective_date).getTime(); - const dateB = new Date(b.effective_date).getTime(); - return dateB - dateA; // Descending order - }); - - ctx.response.body = { - allRates, - summary: { - totalContractorRates: contractorRates.length, - totalStandardRates: standardRates.length, - totalRates: allRates.length, + const standardParams: unknown[] = []; + + if (departmentId) { + standardQuery += " AND d.id = ?"; + standardParams.push(departmentId); } - }; - } catch (error) { - console.error("Get all rates error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + + if (startDate) { + standardQuery += " AND sr.effective_date >= ?"; + standardParams.push(startDate); + } + + if (endDate) { + standardQuery += " AND sr.effective_date <= ?"; + standardParams.push(endDate); + } + + const contractorRates = await db.query( + contractorQuery, + contractorParams, + ); + const standardRates = await db.query( + standardQuery, + standardParams, + ); + + // Combine and sort by date + const allRates = [...contractorRates, ...standardRates].sort((a, b) => { + const dateA = new Date(a.effective_date).getTime(); + const dateB = new Date(b.effective_date).getTime(); + return dateB - dateA; // Descending order + }); + + ctx.response.body = { + allRates, + summary: { + totalContractorRates: contractorRates.length, + totalStandardRates: standardRates.length, + totalRates: allRates.length, + }, + }; + } catch (error) { + console.error("Get all rates error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Compare contractor rates with standard rates -router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const contractorId = params.get("contractorId"); - const subDepartmentId = params.get("subDepartmentId"); - - let departmentFilter = ""; - const queryParams: unknown[] = []; - - if (currentUser.role === "Supervisor") { - departmentFilter = " AND d.id = ?"; - queryParams.push(currentUser.departmentId); - } - - // Get standard rates - let standardQuery = ` +router.get( + "/compare", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const params = ctx.request.url.searchParams; + const contractorId = params.get("contractorId"); + const subDepartmentId = params.get("subDepartmentId"); + + let departmentFilter = ""; + const queryParams: unknown[] = []; + + if (currentUser.role === "Supervisor") { + departmentFilter = " AND d.id = ?"; + queryParams.push(currentUser.departmentId); + } + + // Get standard rates + let standardQuery = ` SELECT sr.*, sd.name as sub_department_name, d.name as department_name, @@ -219,18 +241,21 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity WHERE 1=1 ${departmentFilter} `; - - if (subDepartmentId) { - standardQuery += " AND sr.sub_department_id = ?"; - queryParams.push(subDepartmentId); - } - - standardQuery += " ORDER BY sr.effective_date DESC"; - - const standardRates = await db.query(standardQuery, queryParams); - - // Get contractor rates for comparison - let contractorQuery = ` + + if (subDepartmentId) { + standardQuery += " AND sr.sub_department_id = ?"; + queryParams.push(subDepartmentId); + } + + standardQuery += " ORDER BY sr.effective_date DESC"; + + const standardRates = await db.query( + standardQuery, + queryParams, + ); + + // Get contractor rates for comparison + let contractorQuery = ` SELECT cr.*, u.name as contractor_name, sd.name as sub_department_name, @@ -244,103 +269,123 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity WHERE 1=1 `; - const contractorParams: unknown[] = []; - - if (currentUser.role === "Supervisor") { - contractorQuery += " AND d.id = ?"; - contractorParams.push(currentUser.departmentId); - } - - if (contractorId) { - contractorQuery += " AND cr.contractor_id = ?"; - contractorParams.push(contractorId); - } - - if (subDepartmentId) { - contractorQuery += " AND cr.sub_department_id = ?"; - contractorParams.push(subDepartmentId); - } - - contractorQuery += " ORDER BY cr.effective_date DESC"; - - const contractorRates = await db.query(contractorQuery, contractorParams); - - // Build comparison data - const comparisons = contractorRates.map(cr => { - // Find matching standard rate - const matchingStandard = standardRates.find(sr => - sr.sub_department_id === cr.sub_department_id && - sr.activity === cr.activity + const contractorParams: unknown[] = []; + + if (currentUser.role === "Supervisor") { + contractorQuery += " AND d.id = ?"; + contractorParams.push(currentUser.departmentId); + } + + if (contractorId) { + contractorQuery += " AND cr.contractor_id = ?"; + contractorParams.push(contractorId); + } + + if (subDepartmentId) { + contractorQuery += " AND cr.sub_department_id = ?"; + contractorParams.push(subDepartmentId); + } + + contractorQuery += " ORDER BY cr.effective_date DESC"; + + const contractorRates = await db.query( + contractorQuery, + contractorParams, ); - - const standardRate = matchingStandard?.rate || 0; - const contractorRate = cr.rate || 0; - const difference = contractorRate - standardRate; - const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null; - - return { - ...cr, - standard_rate: standardRate, - difference, - percentage_difference: percentageDiff, - is_above_standard: difference > 0, - is_below_standard: difference < 0, + + // Build comparison data + const comparisons = contractorRates.map((cr) => { + // Find matching standard rate + const matchingStandard = standardRates.find((sr) => + sr.sub_department_id === cr.sub_department_id && + sr.activity === cr.activity + ); + + const standardRate = matchingStandard?.rate || 0; + const contractorRate = cr.rate || 0; + const difference = contractorRate - standardRate; + const percentageDiff = standardRate > 0 + ? ((difference / standardRate) * 100).toFixed(2) + : null; + + return { + ...cr, + standard_rate: standardRate, + difference, + percentage_difference: percentageDiff, + is_above_standard: difference > 0, + is_below_standard: difference < 0, + }; + }); + + ctx.response.body = { + standardRates, + contractorRates, + comparisons, }; - }); - - ctx.response.body = { - standardRates, - contractorRates, - comparisons, - }; - } catch (error) { - console.error("Compare rates error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + } catch (error) { + console.error("Compare rates error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Create standard rate (Supervisor or SuperAdmin) -router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as { - subDepartmentId?: number; - activity?: string; - rate: number; - effectiveDate: string; - }; - const { subDepartmentId, activity, rate, effectiveDate } = body; - - if (!rate || !effectiveDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" }; - return; - } - - // Verify sub-department belongs to supervisor's department if supervisor - if (subDepartmentId && currentUser.role === "Supervisor") { - const subDepts = await db.query( - "SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?", - [subDepartmentId, currentUser.departmentId] - ); - - if (subDepts.length === 0) { - ctx.response.status = 403; - ctx.response.body = { error: "Sub-department not in your department" }; +router.post( + "/", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as { + subDepartmentId?: number; + activity?: string; + rate: number; + effectiveDate: string; + }; + const { subDepartmentId, activity, rate, effectiveDate } = body; + + if (!rate || !effectiveDate) { + ctx.response.status = 400; + ctx.response.body = { + error: "Missing required fields (rate, effectiveDate)", + }; return; } - } - - const sanitizedActivity = activity ? sanitizeInput(activity) : null; - - const result = await db.execute( - "INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)", - [subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id] - ); - - const newRate = await db.query( - `SELECT sr.*, + + // Verify sub-department belongs to supervisor's department if supervisor + if (subDepartmentId && currentUser.role === "Supervisor") { + const subDepts = await db.query( + "SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?", + [subDepartmentId, currentUser.departmentId], + ); + + if (subDepts.length === 0) { + ctx.response.status = 403; + ctx.response.body = { + error: "Sub-department not in your department", + }; + return; + } + } + + const sanitizedActivity = activity ? sanitizeInput(activity) : null; + + const result = await db.execute( + "INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)", + [ + subDepartmentId || null, + sanitizedActivity, + rate, + effectiveDate, + currentUser.id, + ], + ); + + const newRate = await db.query( + `SELECT sr.*, sd.name as sub_department_name, d.name as department_name, u.name as created_by_name, @@ -351,82 +396,96 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async LEFT JOIN users u ON sr.created_by = u.id LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity WHERE sr.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newRate[0]; - } catch (error) { - console.error("Create standard rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newRate[0]; + } catch (error) { + console.error("Create standard rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Update standard rate -router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const rateId = ctx.params.id; - const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string }; - const { rate, activity, effectiveDate } = body; - - // Verify rate exists and user has access - let query = ` +router.put( + "/:id", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const rateId = ctx.params.id; + const body = await ctx.request.body.json() as { + rate?: number; + activity?: string; + effectiveDate?: string; + }; + const { rate, activity, effectiveDate } = body; + + // Verify rate exists and user has access + let query = ` SELECT sr.*, d.id as department_id FROM standard_rates sr LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN departments d ON sd.department_id = d.id WHERE sr.id = ? `; - const params: unknown[] = [rateId]; - - const existing = await db.query(query, params); - - if (existing.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Standard rate not found" }; - return; - } - - // Supervisors can only update rates in their department - if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Access denied - rate not in your department" }; - return; - } - - const updates: string[] = []; - const updateParams: unknown[] = []; - - if (rate !== undefined) { - updates.push("rate = ?"); - updateParams.push(rate); - } - if (activity !== undefined) { - updates.push("activity = ?"); - updateParams.push(sanitizeInput(activity)); - } - if (effectiveDate !== undefined) { - updates.push("effective_date = ?"); - updateParams.push(effectiveDate); - } - - if (updates.length === 0) { - ctx.response.status = 400; - ctx.response.body = { error: "No fields to update" }; - return; - } - - updateParams.push(rateId); - - await db.execute( - `UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`, - updateParams - ); - - const updatedRate = await db.query( - `SELECT sr.*, + const params: unknown[] = [rateId]; + + const existing = await db.query(query, params); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Standard rate not found" }; + return; + } + + // Supervisors can only update rates in their department + if ( + currentUser.role === "Supervisor" && + existing[0].department_id !== currentUser.departmentId + ) { + ctx.response.status = 403; + ctx.response.body = { + error: "Access denied - rate not in your department", + }; + return; + } + + const updates: string[] = []; + const updateParams: unknown[] = []; + + if (rate !== undefined) { + updates.push("rate = ?"); + updateParams.push(rate); + } + if (activity !== undefined) { + updates.push("activity = ?"); + updateParams.push(sanitizeInput(activity)); + } + if (effectiveDate !== undefined) { + updates.push("effective_date = ?"); + updateParams.push(effectiveDate); + } + + if (updates.length === 0) { + ctx.response.status = 400; + ctx.response.body = { error: "No fields to update" }; + return; + } + + updateParams.push(rateId); + + await db.execute( + `UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`, + updateParams, + ); + + const updatedRate = await db.query( + `SELECT sr.*, sd.name as sub_department_name, d.name as department_name, u.name as created_by_name, @@ -437,53 +496,64 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy LEFT JOIN users u ON sr.created_by = u.id LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity WHERE sr.id = ?`, - [rateId] - ); - - ctx.response.body = updatedRate[0]; - } catch (error) { - console.error("Update standard rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [rateId], + ); + + ctx.response.body = updatedRate[0]; + } catch (error) { + console.error("Update standard rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Delete standard rate -router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const rateId = ctx.params.id; - - // Verify rate exists and user has access - const existing = await db.query( - `SELECT sr.*, d.id as department_id +router.delete( + "/:id", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx: Context) => { + try { + const currentUser = getCurrentUser(ctx); + const rateId = ctx.params.id; + + // Verify rate exists and user has access + const existing = await db.query( + `SELECT sr.*, d.id as department_id FROM standard_rates sr LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN departments d ON sd.department_id = d.id WHERE sr.id = ?`, - [rateId] - ); - - if (existing.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "Standard rate not found" }; - return; + [rateId], + ); + + if (existing.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "Standard rate not found" }; + return; + } + + // Supervisors can only delete rates in their department + if ( + currentUser.role === "Supervisor" && + existing[0].department_id !== currentUser.departmentId + ) { + ctx.response.status = 403; + ctx.response.body = { + error: "Access denied - rate not in your department", + }; + return; + } + + await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]); + ctx.response.body = { message: "Standard rate deleted successfully" }; + } catch (error) { + console.error("Delete standard rate error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - // Supervisors can only delete rates in their department - if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Access denied - rate not in your department" }; - return; - } - - await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]); - ctx.response.body = { message: "Standard rate deleted successfully" }; - } catch (error) { - console.error("Delete standard rate error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/users.ts b/backend-deno/routes/users.ts index dc8abd8..9aef0cd 100644 --- a/backend-deno/routes/users.ts +++ b/backend-deno/routes/users.ts @@ -1,5 +1,5 @@ -import { Router } from "@oak/oak"; -import { hash, genSalt } from "bcrypt"; +import { type Context, Router } from "@oak/oak"; +import { genSalt, hash } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; @@ -8,20 +8,28 @@ async function hashPassword(password: string): Promise { const salt = await genSalt(config.BCRYPT_ROUNDS); return await hash(password, salt); } -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; -import { sanitizeInput, isValidEmail } from "../middleware/security.ts"; -import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; +import { isValidEmail, sanitizeInput } from "../middleware/security.ts"; +import type { + CreateUserRequest, + UpdateUserRequest, + User, +} from "../types/index.ts"; const router = new Router(); // Get all users (with filters) -router.get("/", authenticateToken, async (ctx) => { +router.get("/", authenticateToken, async (ctx: Context) => { try { const currentUser = getCurrentUser(ctx); const params = ctx.request.url.searchParams; const role = params.get("role"); const departmentId = params.get("departmentId"); - + let query = ` SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, @@ -36,25 +44,25 @@ router.get("/", authenticateToken, async (ctx) => { WHERE 1=1 `; const queryParams: unknown[] = []; - + // Supervisors can only see users in their department if (currentUser.role === "Supervisor") { query += " AND u.department_id = ?"; queryParams.push(currentUser.departmentId); } - + if (role) { query += " AND u.role = ?"; queryParams.push(role); } - + if (departmentId) { query += " AND u.department_id = ?"; queryParams.push(departmentId); } - + query += " ORDER BY u.created_at DESC"; - + const users = await db.query(query, queryParams); ctx.response.body = users; } catch (error) { @@ -65,11 +73,11 @@ router.get("/", authenticateToken, async (ctx) => { }); // Get user by ID -router.get("/:id", authenticateToken, async (ctx) => { +router.get("/:id", authenticateToken, async (ctx: Context) => { try { const currentUser = getCurrentUser(ctx); const userId = ctx.params.id; - + const users = await db.query( `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, @@ -82,22 +90,25 @@ router.get("/:id", authenticateToken, async (ctx) => { LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN users c ON u.contractor_id = c.id WHERE u.id = ?`, - [userId] + [userId], ); - + if (users.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "User not found" }; return; } - + // Supervisors can only view users in their department - if (currentUser.role === "Supervisor" && users[0].department_id !== currentUser.departmentId) { + if ( + currentUser.role === "Supervisor" && + users[0].department_id !== currentUser.departmentId + ) { ctx.response.status = 403; ctx.response.body = { error: "Access denied" }; return; } - + ctx.response.body = users[0]; } catch (error) { console.error("Get user error:", error); @@ -107,215 +118,98 @@ router.get("/:id", authenticateToken, async (ctx) => { }); // Create user -router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as CreateUserRequest; - const { - username, name, email, password, role, departmentId, contractorId, - phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, - contractorAgreementNumber, pfNumber, esicNumber - } = body; - - // Input validation - if (!username || !name || !email || !password || !role) { - ctx.response.status = 400; - ctx.response.body = { error: "Missing required fields" }; - return; - } - - // Sanitize inputs - const sanitizedUsername = sanitizeInput(username); - const sanitizedName = sanitizeInput(name); - const sanitizedEmail = sanitizeInput(email); - - // Validate email - if (!isValidEmail(sanitizedEmail)) { - ctx.response.status = 400; - ctx.response.body = { error: "Invalid email format" }; - return; - } - - // Supervisors can only create users in their department - if (currentUser.role === "Supervisor") { - if (departmentId !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Can only create users in your department" }; - return; - } - if (role === "SuperAdmin" || role === "Supervisor") { - ctx.response.status = 403; - ctx.response.body = { error: "Cannot create admin or supervisor users" }; - return; - } - } - - // Hash password - const hashedPassword = await hashPassword(password); - - const result = await db.execute( - `INSERT INTO users (username, name, email, password, role, department_id, contractor_id, - phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, - contractor_agreement_number, pf_number, esic_number) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, - departmentId || null, contractorId || null, - phoneNumber || null, aadharNumber || null, bankAccountNumber || null, - bankName || null, bankIfsc || null, - contractorAgreementNumber || null, pfNumber || null, esicNumber || null - ] - ); - - const newUser = await db.query( - `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, - u.contractor_id, u.is_active, u.created_at, - u.phone_number, u.aadhar_number, u.bank_account_number, - u.bank_name, u.bank_ifsc, - u.contractor_agreement_number, u.pf_number, u.esic_number, - d.name as department_name, - c.name as contractor_name - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN users c ON u.contractor_id = c.id - WHERE u.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newUser[0]; - } catch (error) { - const err = error as { code?: string }; - if (err.code === "ER_DUP_ENTRY") { - ctx.response.status = 400; - ctx.response.body = { error: "Username or email already exists" }; - return; - } - console.error("Create user error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); +router.post( + "/", + authenticateToken, + authorize("SuperAdmin", "Supervisor"), + async (ctx: Context) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CreateUserRequest; + const { + username, + name, + email, + password, + role, + departmentId, + contractorId, + phoneNumber, + aadharNumber, + bankAccountNumber, + bankName, + bankIfsc, + contractorAgreementNumber, + pfNumber, + esicNumber, + } = body; -// Update user -router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const userId = ctx.params.id; - const body = await ctx.request.body.json() as UpdateUserRequest; - const { - name, email, role, departmentId, contractorId, isActive, - phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, - contractorAgreementNumber, pfNumber, esicNumber - } = body; - - // Check if user exists - const existingUsers = await db.query( - "SELECT * FROM users WHERE id = ?", - [userId] - ); - - if (existingUsers.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "User not found" }; - return; - } - - // Supervisors can only update users in their department - if (currentUser.role === "Supervisor") { - if (existingUsers[0].department_id !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Can only update users in your department" }; + // Input validation + if (!username || !name || !email || !password || !role) { + ctx.response.status = 400; + ctx.response.body = { error: "Missing required fields" }; return; } - if (role === "SuperAdmin" || role === "Supervisor") { - ctx.response.status = 403; - ctx.response.body = { error: "Cannot modify admin or supervisor roles" }; - return; - } - } - - const updates: string[] = []; - const params: unknown[] = []; - - if (name !== undefined) { - updates.push("name = ?"); - params.push(sanitizeInput(name)); - } - if (email !== undefined) { - if (!isValidEmail(email)) { + + // Sanitize inputs + const sanitizedUsername = sanitizeInput(username); + const sanitizedName = sanitizeInput(name); + const sanitizedEmail = sanitizeInput(email); + + // Validate email + if (!isValidEmail(sanitizedEmail)) { ctx.response.status = 400; ctx.response.body = { error: "Invalid email format" }; return; } - updates.push("email = ?"); - params.push(sanitizeInput(email)); - } - if (role !== undefined) { - updates.push("role = ?"); - params.push(role); - } - if (departmentId !== undefined) { - updates.push("department_id = ?"); - params.push(departmentId); - } - if (contractorId !== undefined) { - updates.push("contractor_id = ?"); - params.push(contractorId); - } - if (isActive !== undefined) { - updates.push("is_active = ?"); - params.push(isActive); - } - // New fields - if (phoneNumber !== undefined) { - updates.push("phone_number = ?"); - params.push(phoneNumber); - } - if (aadharNumber !== undefined) { - updates.push("aadhar_number = ?"); - params.push(aadharNumber); - } - if (bankAccountNumber !== undefined) { - updates.push("bank_account_number = ?"); - params.push(bankAccountNumber); - } - if (bankName !== undefined) { - updates.push("bank_name = ?"); - params.push(bankName); - } - if (bankIfsc !== undefined) { - updates.push("bank_ifsc = ?"); - params.push(bankIfsc); - } - if (contractorAgreementNumber !== undefined) { - updates.push("contractor_agreement_number = ?"); - params.push(contractorAgreementNumber); - } - if (pfNumber !== undefined) { - updates.push("pf_number = ?"); - params.push(pfNumber); - } - if (esicNumber !== undefined) { - updates.push("esic_number = ?"); - params.push(esicNumber); - } - - if (updates.length === 0) { - ctx.response.status = 400; - ctx.response.body = { error: "No fields to update" }; - return; - } - - params.push(userId); - - await db.execute( - `UPDATE users SET ${updates.join(", ")} WHERE id = ?`, - params - ); - - const updatedUser = await db.query( - `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, + + // Supervisors can only create users in their department + if (currentUser.role === "Supervisor") { + if (departmentId !== currentUser.departmentId) { + ctx.response.status = 403; + ctx.response.body = { + error: "Can only create users in your department", + }; + return; + } + if (role === "SuperAdmin" || role === "Supervisor") { + ctx.response.status = 403; + ctx.response.body = { + error: "Cannot create admin or supervisor users", + }; + return; + } + } + + // Hash password + const hashedPassword = await hashPassword(password); + + const result = await db.execute( + `INSERT INTO users (username, name, email, password, role, department_id, contractor_id, + phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, + contractor_agreement_number, pf_number, esic_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + sanitizedUsername, + sanitizedName, + sanitizedEmail, + hashedPassword, + role, + departmentId || null, + contractorId || null, + phoneNumber || null, + aadharNumber || null, + bankAccountNumber || null, + bankName || null, + bankIfsc || null, + contractorAgreementNumber || null, + pfNumber || null, + esicNumber || null, + ], + ); + + const newUser = await db.query( + `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, u.contractor_id, u.is_active, u.created_at, u.phone_number, u.aadhar_number, u.bank_account_number, u.bank_name, u.bank_ifsc, @@ -326,55 +220,232 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN users c ON u.contractor_id = c.id WHERE u.id = ?`, - [userId] - ); - - ctx.response.body = updatedUser[0]; - } catch (error) { - console.error("Update user error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newUser[0]; + } catch (error) { + const err = error as { code?: string }; + if (err.code === "ER_DUP_ENTRY") { + ctx.response.status = 400; + ctx.response.body = { error: "Username or email already exists" }; + return; + } + console.error("Create user error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); + +// Update user +router.put( + "/:id", + authenticateToken, + authorize("SuperAdmin", "Supervisor"), + async (ctx: Context) => { + try { + const currentUser = getCurrentUser(ctx); + const userId = ctx.params.id; + const body = await ctx.request.body.json() as UpdateUserRequest; + const { + name, + email, + role, + departmentId, + contractorId, + isActive, + phoneNumber, + aadharNumber, + bankAccountNumber, + bankName, + bankIfsc, + contractorAgreementNumber, + pfNumber, + esicNumber, + } = body; + + // Check if user exists + const existingUsers = await db.query( + "SELECT * FROM users WHERE id = ?", + [userId], + ); + + if (existingUsers.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "User not found" }; + return; + } + + // Supervisors can only update users in their department + if (currentUser.role === "Supervisor") { + if (existingUsers[0].department_id !== currentUser.departmentId) { + ctx.response.status = 403; + ctx.response.body = { + error: "Can only update users in your department", + }; + return; + } + if (role === "SuperAdmin" || role === "Supervisor") { + ctx.response.status = 403; + ctx.response.body = { + error: "Cannot modify admin or supervisor roles", + }; + return; + } + } + + const updates: string[] = []; + const params: unknown[] = []; + + if (name !== undefined) { + updates.push("name = ?"); + params.push(sanitizeInput(name)); + } + if (email !== undefined) { + if (!isValidEmail(email)) { + ctx.response.status = 400; + ctx.response.body = { error: "Invalid email format" }; + return; + } + updates.push("email = ?"); + params.push(sanitizeInput(email)); + } + if (role !== undefined) { + updates.push("role = ?"); + params.push(role); + } + if (departmentId !== undefined) { + updates.push("department_id = ?"); + params.push(departmentId); + } + if (contractorId !== undefined) { + updates.push("contractor_id = ?"); + params.push(contractorId); + } + if (isActive !== undefined) { + updates.push("is_active = ?"); + params.push(isActive); + } + // New fields + if (phoneNumber !== undefined) { + updates.push("phone_number = ?"); + params.push(phoneNumber); + } + if (aadharNumber !== undefined) { + updates.push("aadhar_number = ?"); + params.push(aadharNumber); + } + if (bankAccountNumber !== undefined) { + updates.push("bank_account_number = ?"); + params.push(bankAccountNumber); + } + if (bankName !== undefined) { + updates.push("bank_name = ?"); + params.push(bankName); + } + if (bankIfsc !== undefined) { + updates.push("bank_ifsc = ?"); + params.push(bankIfsc); + } + if (contractorAgreementNumber !== undefined) { + updates.push("contractor_agreement_number = ?"); + params.push(contractorAgreementNumber); + } + if (pfNumber !== undefined) { + updates.push("pf_number = ?"); + params.push(pfNumber); + } + if (esicNumber !== undefined) { + updates.push("esic_number = ?"); + params.push(esicNumber); + } + + if (updates.length === 0) { + ctx.response.status = 400; + ctx.response.body = { error: "No fields to update" }; + return; + } + + params.push(userId); + + await db.execute( + `UPDATE users SET ${updates.join(", ")} WHERE id = ?`, + params, + ); + + const updatedUser = await db.query( + `SELECT u.id, u.username, u.name, u.email, u.role, u.department_id, + u.contractor_id, u.is_active, u.created_at, + u.phone_number, u.aadhar_number, u.bank_account_number, + u.bank_name, u.bank_ifsc, + u.contractor_agreement_number, u.pf_number, u.esic_number, + d.name as department_name, + c.name as contractor_name + FROM users u + LEFT JOIN departments d ON u.department_id = d.id + LEFT JOIN users c ON u.contractor_id = c.id + WHERE u.id = ?`, + [userId], + ); + + ctx.response.body = updatedUser[0]; + } catch (error) { + console.error("Update user error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Delete user -router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const userId = ctx.params.id; - - const users = await db.query( - "SELECT * FROM users WHERE id = ?", - [userId] - ); - - if (users.length === 0) { - ctx.response.status = 404; - ctx.response.body = { error: "User not found" }; - return; - } - - // Supervisors can only delete users in their department - if (currentUser.role === "Supervisor") { - if (users[0].department_id !== currentUser.departmentId) { - ctx.response.status = 403; - ctx.response.body = { error: "Can only delete users in your department" }; +router.delete( + "/:id", + authenticateToken, + authorize("SuperAdmin", "Supervisor"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const userId = ctx.params.id; + + const users = await db.query( + "SELECT * FROM users WHERE id = ?", + [userId], + ); + + if (users.length === 0) { + ctx.response.status = 404; + ctx.response.body = { error: "User not found" }; return; } - if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") { - ctx.response.status = 403; - ctx.response.body = { error: "Cannot delete admin or supervisor users" }; - return; + + // Supervisors can only delete users in their department + if (currentUser.role === "Supervisor") { + if (users[0].department_id !== currentUser.departmentId) { + ctx.response.status = 403; + ctx.response.body = { + error: "Can only delete users in your department", + }; + return; + } + if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") { + ctx.response.status = 403; + ctx.response.body = { + error: "Cannot delete admin or supervisor users", + }; + return; + } } + + await db.execute("DELETE FROM users WHERE id = ?", [userId]); + ctx.response.body = { message: "User deleted successfully" }; + } catch (error) { + console.error("Delete user error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - await db.execute("DELETE FROM users WHERE id = ?", [userId]); - ctx.response.body = { message: "User deleted successfully" }; - } catch (error) { - console.error("Delete user error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/routes/work-allocations.ts b/backend-deno/routes/work-allocations.ts index d4cd3bd..51639d0 100644 --- a/backend-deno/routes/work-allocations.ts +++ b/backend-deno/routes/work-allocations.ts @@ -1,21 +1,35 @@ -import { Router } from "@oak/oak"; +import { Router, type RouterContext, type State } from "@oak/oak"; import { db } from "../config/database.ts"; -import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; +import { + authenticateToken, + authorize, + getCurrentUser, +} from "../middleware/auth.ts"; import { sanitizeInput } from "../middleware/security.ts"; -import type { WorkAllocation, CreateWorkAllocationRequest, ContractorRate } from "../types/index.ts"; +import type { + ContractorRate, + CreateWorkAllocationRequest, + JWTPayload, + WorkAllocation, +} from "../types/index.ts"; const router = new Router(); // Get all work allocations -router.get("/", authenticateToken, async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const params = ctx.request.url.searchParams; - const employeeId = params.get("employeeId"); - const status = params.get("status"); - const departmentId = params.get("departmentId"); - - let query = ` +router.get( + "/", + authenticateToken, + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const currentUser: JWTPayload = getCurrentUser(ctx); + const params: URLSearchParams = ctx.request.url.searchParams; + const employeeId: string | null = params.get("employeeId"); + const status: string | null = params.get("status"); + const departmentId: string | null = params.get("departmentId"); + + let query: string = ` SELECT wa.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, @@ -30,52 +44,53 @@ router.get("/", authenticateToken, async (ctx) => { LEFT JOIN departments d ON e.department_id = d.id WHERE 1=1 `; - const queryParams: unknown[] = []; - - // Role-based filtering - if (currentUser.role === "Supervisor") { - query += " AND wa.supervisor_id = ?"; - queryParams.push(currentUser.id); - } else if (currentUser.role === "Employee") { - query += " AND wa.employee_id = ?"; - queryParams.push(currentUser.id); - } else if (currentUser.role === "Contractor") { - query += " AND wa.contractor_id = ?"; - queryParams.push(currentUser.id); + const queryParams: unknown[] = []; + + // Role-based filtering + if (currentUser.role === "Supervisor") { + query += " AND wa.supervisor_id = ?"; + queryParams.push(currentUser.id); + } else if (currentUser.role === "Employee") { + query += " AND wa.employee_id = ?"; + queryParams.push(currentUser.id); + } else if (currentUser.role === "Contractor") { + query += " AND wa.contractor_id = ?"; + queryParams.push(currentUser.id); + } + + if (employeeId) { + query += " AND wa.employee_id = ?"; + queryParams.push(employeeId); + } + + if (status) { + query += " AND wa.status = ?"; + queryParams.push(status); + } + + if (departmentId) { + query += " AND e.department_id = ?"; + queryParams.push(departmentId); + } + + query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC"; + + const allocations = await db.query(query, queryParams); + ctx.response.body = allocations; + } catch (error) { + console.error("Get work allocations error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - if (employeeId) { - query += " AND wa.employee_id = ?"; - queryParams.push(employeeId); - } - - if (status) { - query += " AND wa.status = ?"; - queryParams.push(status); - } - - if (departmentId) { - query += " AND e.department_id = ?"; - queryParams.push(departmentId); - } - - query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC"; - - const allocations = await db.query(query, queryParams); - ctx.response.body = allocations; - } catch (error) { - console.error("Get work allocations error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); // Get work allocation by ID -router.get("/:id", authenticateToken, async (ctx) => { +router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => { try { - const allocationId = ctx.params.id; - - const allocations = await db.query( + const allocationId: string | undefined = ctx.params.id; + + const allocations: WorkAllocation[] = await db.query( `SELECT wa.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, @@ -89,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => { LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN departments d ON e.department_id = d.id WHERE wa.id = ?`, - [allocationId] + [allocationId], ); - + if (allocations.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Work allocation not found" }; return; } - + ctx.response.body = allocations[0]; } catch (error) { console.error("Get work allocation error:", error); @@ -107,57 +122,92 @@ router.get("/:id", authenticateToken, async (ctx) => { }); // Create work allocation (Supervisor or SuperAdmin) -router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const body = await ctx.request.body.json() as CreateWorkAllocationRequest; - const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = body; - - if (!employeeId || !contractorId || !assignedDate) { - ctx.response.status = 400; - ctx.response.body = { error: "Missing required fields" }; - return; - } - - // Verify employee exists - let employeeQuery = "SELECT * FROM users WHERE id = ?"; - const employeeParams: unknown[] = [employeeId]; - - if (currentUser.role === "Supervisor") { - employeeQuery += " AND department_id = ?"; - employeeParams.push(currentUser.departmentId); - } - - const employees = await db.query<{ id: number }[]>(employeeQuery, employeeParams); - - if (employees.length === 0) { - ctx.response.status = 403; - ctx.response.body = { error: "Employee not found or not in your department" }; - return; - } - - // Use provided rate or get contractor's current rate - let finalRate = rate; - if (!finalRate) { - const rates = await db.query( - "SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1", - [contractorId] +router.post( + "/", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async ( + ctx: RouterContext<"/", Record, State>, + ) => { + try { + const currentUser = getCurrentUser(ctx); + const body = await ctx.request.body.json() as CreateWorkAllocationRequest; + const { + employeeId, + contractorId, + subDepartmentId, + activity, + description, + assignedDate, + rate, + units, + totalAmount, + departmentId, + } = body; + + if (!employeeId || !contractorId || !assignedDate) { + ctx.response.status = 400; + ctx.response.body = { error: "Missing required fields" }; + return; + } + + // Verify employee exists + let employeeQuery = "SELECT * FROM users WHERE id = ?"; + const employeeParams: unknown[] = [employeeId]; + + if (currentUser.role === "Supervisor") { + employeeQuery += " AND department_id = ?"; + employeeParams.push(currentUser.departmentId); + } + + const employees = await db.query<{ id: number }[]>( + employeeQuery, + employeeParams, ); - finalRate = rates.length > 0 ? rates[0].rate : null; - } - - const sanitizedActivity = activity ? sanitizeInput(activity) : null; - const sanitizedDescription = description ? sanitizeInput(description) : null; - - const result = await db.execute( - `INSERT INTO work_allocations + + if (employees.length === 0) { + ctx.response.status = 403; + ctx.response.body = { + error: "Employee not found or not in your department", + }; + return; + } + + // Use provided rate or get contractor's current rate + let finalRate = rate; + if (!finalRate) { + const rates = await db.query( + "SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1", + [contractorId], + ); + finalRate = rates.length > 0 ? rates[0].rate : null; + } + + const sanitizedActivity = activity ? sanitizeInput(activity) : null; + const sanitizedDescription = description + ? sanitizeInput(description) + : null; + + const result = await db.execute( + `INSERT INTO work_allocations (employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [employeeId, currentUser.id, contractorId, subDepartmentId || null, sanitizedActivity, sanitizedDescription, assignedDate, finalRate, units || null, totalAmount || null] - ); - - const newAllocation = await db.query( - `SELECT wa.*, + [ + employeeId, + currentUser.id, + contractorId, + subDepartmentId || null, + sanitizedActivity, + sanitizedDescription, + assignedDate, + finalRate, + units || null, + totalAmount || null, + ], + ); + + const newAllocation = await db.query( + `SELECT wa.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, c.name as contractor_name, @@ -170,56 +220,66 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN departments d ON e.department_id = d.id WHERE wa.id = ?`, - [result.insertId] - ); - - ctx.response.status = 201; - ctx.response.body = newAllocation[0]; - } catch (error) { - console.error("Create work allocation error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [result.insertId], + ); + + ctx.response.status = 201; + ctx.response.body = newAllocation[0]; + } catch (error) { + console.error("Create work allocation error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Update work allocation status (Supervisor or SuperAdmin) -router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const allocationId = ctx.params.id; - const body = await ctx.request.body.json() as { status: string; completionDate?: string }; - const { status, completionDate } = body; - - if (!status) { - ctx.response.status = 400; - ctx.response.body = { error: "Status required" }; - return; - } - - // Verify allocation exists and user has access - let query = "SELECT * FROM work_allocations WHERE id = ?"; - const params: unknown[] = [allocationId]; - - if (currentUser.role === "Supervisor") { - query += " AND supervisor_id = ?"; - params.push(currentUser.id); - } - - const allocations = await db.query(query, params); - - if (allocations.length === 0) { - ctx.response.status = 403; - ctx.response.body = { error: "Work allocation not found or access denied" }; - return; - } - - await db.execute( - "UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?", - [status, completionDate || null, allocationId] - ); - - const updatedAllocation = await db.query( - `SELECT wa.*, +router.put( + "/:id/status", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const allocationId = ctx.params.id; + const body = await ctx.request.body.json() as { + status: string; + completionDate?: string; + }; + const { status, completionDate } = body; + + if (!status) { + ctx.response.status = 400; + ctx.response.body = { error: "Status required" }; + return; + } + + // Verify allocation exists and user has access + let query = "SELECT * FROM work_allocations WHERE id = ?"; + const params: unknown[] = [allocationId]; + + if (currentUser.role === "Supervisor") { + query += " AND supervisor_id = ?"; + params.push(currentUser.id); + } + + const allocations = await db.query(query, params); + + if (allocations.length === 0) { + ctx.response.status = 403; + ctx.response.body = { + error: "Work allocation not found or access denied", + }; + return; + } + + await db.execute( + "UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?", + [status, completionDate || null, allocationId], + ); + + const updatedAllocation = await db.query( + `SELECT wa.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, c.name as contractor_name, @@ -232,47 +292,57 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN departments d ON e.department_id = d.id WHERE wa.id = ?`, - [allocationId] - ); - - ctx.response.body = updatedAllocation[0]; - } catch (error) { - console.error("Update work allocation error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + [allocationId], + ); + + ctx.response.body = updatedAllocation[0]; + } catch (error) { + console.error("Update work allocation error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; + } + }, +); // Delete work allocation (Supervisor or SuperAdmin) -router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { - try { - const currentUser = getCurrentUser(ctx); - const allocationId = ctx.params.id; - - // Verify allocation exists and user has access - let query = "SELECT * FROM work_allocations WHERE id = ?"; - const params: unknown[] = [allocationId]; - - if (currentUser.role === "Supervisor") { - query += " AND supervisor_id = ?"; - params.push(currentUser.id); +router.delete( + "/:id", + authenticateToken, + authorize("Supervisor", "SuperAdmin"), + async (ctx) => { + try { + const currentUser = getCurrentUser(ctx); + const allocationId = ctx.params.id; + + // Verify allocation exists and user has access + let query = "SELECT * FROM work_allocations WHERE id = ?"; + const params: unknown[] = [allocationId]; + + if (currentUser.role === "Supervisor") { + query += " AND supervisor_id = ?"; + params.push(currentUser.id); + } + + const allocations = await db.query(query, params); + + if (allocations.length === 0) { + ctx.response.status = 403; + ctx.response.body = { + error: "Work allocation not found or access denied", + }; + return; + } + + await db.execute("DELETE FROM work_allocations WHERE id = ?", [ + allocationId, + ]); + ctx.response.body = { message: "Work allocation deleted successfully" }; + } catch (error) { + console.error("Delete work allocation error:", error); + ctx.response.status = 500; + ctx.response.body = { error: "Internal server error" }; } - - const allocations = await db.query(query, params); - - if (allocations.length === 0) { - ctx.response.status = 403; - ctx.response.body = { error: "Work allocation not found or access denied" }; - return; - } - - await db.execute("DELETE FROM work_allocations WHERE id = ?", [allocationId]); - ctx.response.body = { message: "Work allocation deleted successfully" }; - } catch (error) { - console.error("Delete work allocation error:", error); - ctx.response.status = 500; - ctx.response.body = { error: "Internal server error" }; - } -}); + }, +); export default router; diff --git a/backend-deno/scripts/seed.ts b/backend-deno/scripts/seed.ts index 95eef49..f7e7e16 100644 --- a/backend-deno/scripts/seed.ts +++ b/backend-deno/scripts/seed.ts @@ -1,4 +1,4 @@ -import { hash, genSalt } from "bcrypt"; +import { genSalt, hash } from "bcrypt"; import { db } from "../config/database.ts"; import { config } from "../config/env.ts"; @@ -17,9 +17,9 @@ async function seedDatabase() { // 1. Seed Departments console.log("📁 Seeding departments..."); const existingDepts = await db.query<{ count: number }[]>( - "SELECT COUNT(*) as count FROM departments" + "SELECT COUNT(*) as count FROM departments", ); - + if (existingDepts[0].count === 0) { await db.execute(` INSERT INTO departments (name) VALUES @@ -34,58 +34,99 @@ async function seedDatabase() { // 2. Seed Sub-departments and Activities for all departments console.log("📂 Seeding sub-departments and activities..."); - + // Get department IDs const tudkiDeptResult = await db.query<{ id: number }[]>( "SELECT id FROM departments WHERE name = ?", - ["Tudki"] + ["Tudki"], ); const danaDeptResult = await db.query<{ id: number }[]>( "SELECT id FROM departments WHERE name = ?", - ["Dana"] + ["Dana"], ); const groundnutDeptResult = await db.query<{ id: number }[]>( "SELECT id FROM departments WHERE name = ?", - ["Groundnut"] + ["Groundnut"], ); - + const tudkiId = tudkiDeptResult[0]?.id; const danaId = danaDeptResult[0]?.id; const groundnutId = groundnutDeptResult[0]?.id; // Define sub-departments and activities per department based on activities.md - const departmentData: { [key: number]: { subDept: string; activities: { name: string; unit: string }[] }[] } = {}; - + const departmentData: { + [key: number]: { + subDept: string; + activities: { name: string; unit: string }[]; + }[]; + } = {}; + if (groundnutId) { departmentData[groundnutId] = [ { subDept: "Loading/Unloading", activities: [ - { name: "Mufali Aavak Katai (Groundnut Arrival Cutting)", unit: "Per Bag" }, - { name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)", unit: "Per Bag" }, + { + name: "Mufali Aavak Katai (Groundnut Arrival Cutting)", + unit: "Per Bag", + }, + { + name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)", + unit: "Per Bag", + }, { name: "Dhaang Se Katai (Cutting from Stack)", unit: "Per Bag" }, - { name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)", unit: "Per Bag" }, - { name: "Guthali dhada Pala Tulai Silai Dhaang / Loading", unit: "Per Bag" }, + { + name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)", + unit: "Per Bag", + }, + { + name: "Guthali dhada Pala Tulai Silai Dhaang / Loading", + unit: "Per Bag", + }, { name: "Mufali Patthar Bori silai Dhaang", unit: "Per Bag" }, - { name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)", unit: "Per Bag" }, - { name: "Bardana Bandal Loading (Gunny Bundle Loading)", unit: "Per Bag" }, + { + name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)", + unit: "Per Bag", + }, + { + name: "Bardana Bandal Loading (Gunny Bundle Loading)", + unit: "Per Bag", + }, { name: "Bardana Gatthi Loading/Unloading", unit: "Per Bag" }, { name: "Black Dana Loading/Unloading", unit: "Per Bag" }, { name: "Dala - Chomu & Jaipur (Branch)", unit: "Per Bag" }, - ] + ], + }, + { + subDept: "Pre Cleaning", + activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Destoner", + activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Water", + activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }], }, - { subDept: "Pre Cleaning", activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Water", activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }] }, { subDept: "Decordicater & Cleaning and Round Chalna", activities: [ { name: "Decordicater", unit: "Fixed Rate-Per Person" }, - { name: "Round Chalna (Round Sieving)", unit: "Fixed Rate-Per Person" }, + { + name: "Round Chalna (Round Sieving)", + unit: "Fixed Rate-Per Person", + }, { name: "Cleaning", unit: "Fixed Rate-Per Person" }, - ] + ], + }, + { + subDept: "Round Chalna No.1", + activities: [{ + name: "Round Chalna No.1 (Round Sieving No.1)", + unit: "Fixed Rate-Per Person", + }], }, - { subDept: "Round Chalna No.1", activities: [{ name: "Round Chalna No.1 (Round Sieving No.1)", unit: "Fixed Rate-Per Person" }] }, ]; } @@ -94,16 +135,41 @@ async function seedDatabase() { { subDept: "Loading/Unloading", activities: [ - { name: "Tulai Silai Loading (Weighing Stitching Loading)", unit: "Per Bag" }, + { + name: "Tulai Silai Loading (Weighing Stitching Loading)", + unit: "Per Bag", + }, { name: "Dhaang se Loading (Loading from Stack)", unit: "Per Bag" }, { name: "Silai Dhaang (Stitching Stack)", unit: "Per Bag" }, - { name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass", unit: "Per Bag" }, - { name: "Dana Unloading/Dhaang (Grain Unloading/Stack)", unit: "Per Bag" }, - { name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)", unit: "Per Bag" }, - { name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg", unit: "Per Bag" }, - { name: "Kachri Dhaang se loading (Waste Loading from Stack)", unit: "Per Bag" }, - { name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)", unit: "Per Bag" }, - { name: "Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)", unit: "Per Bag" }, + { + name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass", + unit: "Per Bag", + }, + { + name: "Dana Unloading/Dhaang (Grain Unloading/Stack)", + unit: "Per Bag", + }, + { + name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)", + unit: "Per Bag", + }, + { + name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg", + unit: "Per Bag", + }, + { + name: "Kachri Dhaang se loading (Waste Loading from Stack)", + unit: "Per Bag", + }, + { + name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)", + unit: "Per Bag", + }, + { + name: + "Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)", + unit: "Per Bag", + }, { name: "Bardana Paltai (Gunny Turning)", unit: "Per Bag" }, { name: "Ekai No. 2 me Keep Katai Khula Bag", unit: "Per Bag" }, { name: "Ekai No. 2 me Keep Katai Silai Kholkar", unit: "Per Bag" }, @@ -111,15 +177,39 @@ async function seedDatabase() { { name: "Kachri Bharai Silai Dhaang Chatt Par", unit: "Per Bag" }, { name: "Bardana Unloading (Gunny Unloading)", unit: "Per Bag" }, { name: "Grading", unit: "Per Bag" }, - ] + ], + }, + { + subDept: "Destoner", + activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Gravity", + activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Tank", + activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Sortex", + activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "X-Ray", + activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Kachri", + activities: [{ + name: "Kachri (Waste)", + unit: "Fixed Rate-Per Person", + }], + }, + { + subDept: "Other Works", + activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }], }, - { subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Gravity", activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] }, - { subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Kachri", activities: [{ name: "Kachri (Waste)", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] }, ]; } @@ -128,32 +218,90 @@ async function seedDatabase() { { subDept: "Loading/Unloading", activities: [ - { name: "Dana Loading/Unloading (Grain Loading/Unloading)", unit: "Per Bag" }, + { + name: "Dana Loading/Unloading (Grain Loading/Unloading)", + unit: "Per Bag", + }, { name: "Loading/Unloading 40 Kg", unit: "Per Bag" }, - { name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang", unit: "Per Bag" }, - { name: "Grading Chalne se Maal Bharai Tulai Silai Loading", unit: "Per Bag" }, - { name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)", unit: "Per Bag" }, - { name: "Keep katai Bahar Se (Hopper Cutting from Outside)", unit: "Per Bag" }, - { name: "Keep katai Andar Se (Hopper Cutting from Inside)", unit: "Per Bag" }, - { name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang", unit: "Per Bag" }, - { name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading", unit: "Per Bag" }, + { + name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang", + unit: "Per Bag", + }, + { + name: "Grading Chalne se Maal Bharai Tulai Silai Loading", + unit: "Per Bag", + }, + { + name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)", + unit: "Per Bag", + }, + { + name: "Keep katai Bahar Se (Hopper Cutting from Outside)", + unit: "Per Bag", + }, + { + name: "Keep katai Andar Se (Hopper Cutting from Inside)", + unit: "Per Bag", + }, + { + name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang", + unit: "Per Bag", + }, + { + name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading", + unit: "Per Bag", + }, { name: "Katta Paltai (Bag Turning)", unit: "Per Bag" }, { name: "Dhada Pala Bharai Tulai Silai Dhaang", unit: "Per Bag" }, { name: "Dhada Pala Bharai Tulai Silai Loading", unit: "Per Bag" }, { name: "Sike Maal Ki Silai Dhaang Andar", unit: "Per Bag" }, { name: "Sike Maal Ki Silai Dhaang Bahar", unit: "Per Bag" }, - { name: "Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)", unit: "Per Bag" }, - ] + { + name: + "Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)", + unit: "Per Bag", + }, + ], + }, + { + subDept: "Tank", + activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Grader (Machine)", + activities: [{ + name: "Grader (Machine)", + unit: "Fixed Rate-Per Person", + }], + }, + { + subDept: "Sortex", + activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "X-Ray", + activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Rejection", + activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Store", + activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Roster", + activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Blancher", + activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }], + }, + { + subDept: "Other Works", + activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }], }, - { subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Grader (Machine)", activities: [{ name: "Grader (Machine)", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] }, - { subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Rejection", activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Store", activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Roster", activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Blancher", activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }] }, - { subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] }, ]; } @@ -178,23 +326,23 @@ async function seedDatabase() { for (const [deptId, subDepts] of Object.entries(departmentData)) { const existingSubDepts = await db.query<{ count: number }[]>( "SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?", - [deptId] + [deptId], ); - + if (existingSubDepts[0].count === 0) { for (const { subDept, activities } of subDepts) { // Insert sub-department await db.execute( "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", - [deptId, subDept] + [deptId, subDept], ); - + // Get the sub-department ID const subDeptResult = await db.query<{ id: number }[]>( "SELECT id FROM sub_departments WHERE department_id = ? AND name = ?", - [deptId, subDept] + [deptId, subDept], ); - + if (subDeptResult.length > 0) { const subDeptId = subDeptResult[0].id; // Insert activities for this sub-department @@ -202,7 +350,7 @@ async function seedDatabase() { try { await db.execute( "INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)", - [subDeptId, activity.name, activity.unit] + [subDeptId, activity.name, activity.unit], ); } catch (_e) { // Activity might already exist @@ -218,21 +366,28 @@ async function seedDatabase() { console.log("👤 Seeding SuperAdmin user..."); const existingAdmin = await db.query<{ id: number }[]>( "SELECT id FROM users WHERE username = ?", - ["admin"] + ["admin"], ); - + const adminPassword = await hashPassword("admin123"); - + if (existingAdmin.length > 0) { await db.execute( "UPDATE users SET password = ?, is_active = TRUE WHERE username = ?", - [adminPassword, "admin"] + [adminPassword, "admin"], ); console.log(" ✅ SuperAdmin password updated"); } else { await db.execute( "INSERT INTO users (username, name, email, password, role, is_active) VALUES (?, ?, ?, ?, ?, ?)", - ["admin", "Super Admin", "admin@workallocate.com", adminPassword, "SuperAdmin", true] + [ + "admin", + "Super Admin", + "admin@workallocate.com", + adminPassword, + "SuperAdmin", + true, + ], ); console.log(" ✅ SuperAdmin created"); } @@ -240,41 +395,50 @@ async function seedDatabase() { // 4. Seed Supervisors for all departments console.log("👥 Seeding supervisors..."); const supervisorPassword = await hashPassword("supervisor123"); - + const supervisors = [ - { - username: "rajesh.sharma.tudki", - name: "Rajesh Sharma", - email: "rajesh.sharma@workallocate.com", + { + username: "rajesh.sharma.tudki", + name: "Rajesh Sharma", + email: "rajesh.sharma@workallocate.com", deptId: tudkiId, - phone: "9414567890" + phone: "9414567890", }, - { - username: "sunil.verma.dana", - name: "Sunil Verma", - email: "sunil.verma@workallocate.com", + { + username: "sunil.verma.dana", + name: "Sunil Verma", + email: "sunil.verma@workallocate.com", deptId: danaId, - phone: "9414567891" + phone: "9414567891", }, - { - username: "mahesh.agarwal.groundnut", - name: "Mahesh Agarwal", - email: "mahesh.agarwal@workallocate.com", + { + username: "mahesh.agarwal.groundnut", + name: "Mahesh Agarwal", + email: "mahesh.agarwal@workallocate.com", deptId: groundnutId, - phone: "9414567892" - } + phone: "9414567892", + }, ]; for (const sup of supervisors) { if (sup.deptId) { const existing = await db.query<{ id: number }[]>( "SELECT id FROM users WHERE username = ?", - [sup.username] + [sup.username], ); if (existing.length === 0) { await db.execute( "INSERT INTO users (username, name, email, password, role, department_id, is_active, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true, sup.phone] + [ + sup.username, + sup.name, + sup.email, + supervisorPassword, + "Supervisor", + sup.deptId, + true, + sup.phone, + ], ); console.log(` ✅ ${sup.name} created`); } else { @@ -286,13 +450,13 @@ async function seedDatabase() { // 5. Seed Contractors for all departments console.log("🏗️ Seeding contractors..."); const contractorPassword = await hashPassword("contractor123"); - + const contractors = [ // Groundnut Department Contractors - { - username: "ramesh.patel.gn", - name: "Ramesh Patel", - email: "ramesh.patel@workallocate.com", + { + username: "ramesh.patel.gn", + name: "Ramesh Patel", + email: "ramesh.patel@workallocate.com", deptId: groundnutId, phone: "9829012345", aadhar: "234567891234", @@ -301,12 +465,12 @@ async function seedDatabase() { bankIfsc: "HDFC0001234", agreementNo: "AGR-GN-2024-001", pfNo: "RJ/JPR/12345/001", - esicNo: "12-34-567890-001-0001" + esicNo: "12-34-567890-001-0001", }, - { - username: "kishan.meena.gn", - name: "Kishan Meena", - email: "kishan.meena@workallocate.com", + { + username: "kishan.meena.gn", + name: "Kishan Meena", + email: "kishan.meena@workallocate.com", deptId: groundnutId, phone: "9829012346", aadhar: "345678912345", @@ -315,13 +479,13 @@ async function seedDatabase() { bankIfsc: "SBIN0005678", agreementNo: "AGR-GN-2024-002", pfNo: "RJ/JPR/12345/002", - esicNo: "12-34-567890-001-0002" + esicNo: "12-34-567890-001-0002", }, // Dana Department Contractors - { - username: "gopal.sharma.dana", - name: "Gopal Sharma", - email: "gopal.sharma@workallocate.com", + { + username: "gopal.sharma.dana", + name: "Gopal Sharma", + email: "gopal.sharma@workallocate.com", deptId: danaId, phone: "9829012347", aadhar: "456789123456", @@ -330,12 +494,12 @@ async function seedDatabase() { bankIfsc: "PUNB0009876", agreementNo: "AGR-DN-2024-001", pfNo: "RJ/JPR/12345/003", - esicNo: "12-34-567890-002-0001" + esicNo: "12-34-567890-002-0001", }, - { - username: "mohan.yadav.dana", - name: "Mohan Yadav", - email: "mohan.yadav@workallocate.com", + { + username: "mohan.yadav.dana", + name: "Mohan Yadav", + email: "mohan.yadav@workallocate.com", deptId: danaId, phone: "9829012348", aadhar: "567891234567", @@ -344,13 +508,13 @@ async function seedDatabase() { bankIfsc: "BARB0004567", agreementNo: "AGR-DN-2024-002", pfNo: "RJ/JPR/12345/004", - esicNo: "12-34-567890-002-0002" + esicNo: "12-34-567890-002-0002", }, // Tudki Department Contractors - { - username: "suresh.kumar.tudki", - name: "Suresh Kumar", - email: "suresh.kumar@workallocate.com", + { + username: "suresh.kumar.tudki", + name: "Suresh Kumar", + email: "suresh.kumar@workallocate.com", deptId: tudkiId, phone: "9829012349", aadhar: "678912345678", @@ -359,12 +523,12 @@ async function seedDatabase() { bankIfsc: "ICIC0003456", agreementNo: "AGR-TK-2024-001", pfNo: "RJ/JPR/12345/005", - esicNo: "12-34-567890-003-0001" + esicNo: "12-34-567890-003-0001", }, - { - username: "dinesh.gupta.tudki", - name: "Dinesh Gupta", - email: "dinesh.gupta@workallocate.com", + { + username: "dinesh.gupta.tudki", + name: "Dinesh Gupta", + email: "dinesh.gupta@workallocate.com", deptId: tudkiId, phone: "9829012350", aadhar: "789123456789", @@ -373,14 +537,14 @@ async function seedDatabase() { bankIfsc: "UTIB0002345", agreementNo: "AGR-TK-2024-002", pfNo: "RJ/JPR/12345/006", - esicNo: "12-34-567890-003-0002" - } + esicNo: "12-34-567890-003-0002", + }, ]; for (const con of contractors) { const existing = await db.query<{ id: number }[]>( "SELECT id FROM users WHERE username = ?", - [con.username] + [con.username], ); if (existing.length === 0) { await db.execute( @@ -388,9 +552,23 @@ async function seedDatabase() { phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, contractor_agreement_number, pf_number, esic_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true, - con.phone, con.aadhar, con.bankAccount, con.bankName, con.bankIfsc, - con.agreementNo, con.pfNo, con.esicNo] + [ + con.username, + con.name, + con.email, + contractorPassword, + "Contractor", + con.deptId, + true, + con.phone, + con.aadhar, + con.bankAccount, + con.bankName, + con.bankIfsc, + con.agreementNo, + con.pfNo, + con.esicNo, + ], ); console.log(` ✅ ${con.name} created`); } else { @@ -401,13 +579,13 @@ async function seedDatabase() { // 6. Seed Employees for all departments console.log("👷 Seeding employees..."); const employeePassword = await hashPassword("employee123"); - + // Get contractor IDs for employee assignment const contractorIds: { [key: string]: number } = {}; for (const con of contractors) { const result = await db.query<{ id: number }[]>( "SELECT id FROM users WHERE username = ?", - [con.username] + [con.username], ); if (result.length > 0) { contractorIds[con.username] = result[0].id; @@ -416,9 +594,9 @@ async function seedDatabase() { const employees = [ // Groundnut Department Employees - Under Ramesh Patel - { - username: "ravi.singh.gn1", - name: "Ravi Singh", + { + username: "ravi.singh.gn1", + name: "Ravi Singh", email: "ravi.singh@workallocate.com", deptId: groundnutId, contractorUsername: "ramesh.patel.gn", @@ -426,11 +604,11 @@ async function seedDatabase() { aadhar: "111122223333", bankAccount: "30100111122233", bankName: "State Bank of India", - bankIfsc: "SBIN0001111" + bankIfsc: "SBIN0001111", }, - { - username: "amit.kumar.gn2", - name: "Amit Kumar", + { + username: "amit.kumar.gn2", + name: "Amit Kumar", email: "amit.kumar@workallocate.com", deptId: groundnutId, contractorUsername: "ramesh.patel.gn", @@ -438,11 +616,11 @@ async function seedDatabase() { aadhar: "222233334444", bankAccount: "30100222233344", bankName: "Punjab National Bank", - bankIfsc: "PUNB0002222" + bankIfsc: "PUNB0002222", }, - { - username: "vijay.meena.gn3", - name: "Vijay Meena", + { + username: "vijay.meena.gn3", + name: "Vijay Meena", email: "vijay.meena@workallocate.com", deptId: groundnutId, contractorUsername: "ramesh.patel.gn", @@ -450,12 +628,12 @@ async function seedDatabase() { aadhar: "333344445555", bankAccount: "30100333344455", bankName: "HDFC Bank", - bankIfsc: "HDFC0003333" + bankIfsc: "HDFC0003333", }, // Groundnut Department Employees - Under Kishan Meena - { - username: "sanjay.yadav.gn4", - name: "Sanjay Yadav", + { + username: "sanjay.yadav.gn4", + name: "Sanjay Yadav", email: "sanjay.yadav@workallocate.com", deptId: groundnutId, contractorUsername: "kishan.meena.gn", @@ -463,11 +641,11 @@ async function seedDatabase() { aadhar: "444455556666", bankAccount: "30100444455566", bankName: "Bank of Baroda", - bankIfsc: "BARB0004444" + bankIfsc: "BARB0004444", }, - { - username: "prakash.sharma.gn5", - name: "Prakash Sharma", + { + username: "prakash.sharma.gn5", + name: "Prakash Sharma", email: "prakash.sharma@workallocate.com", deptId: groundnutId, contractorUsername: "kishan.meena.gn", @@ -475,12 +653,12 @@ async function seedDatabase() { aadhar: "555566667777", bankAccount: "30100555566677", bankName: "ICICI Bank", - bankIfsc: "ICIC0005555" + bankIfsc: "ICIC0005555", }, // Dana Department Employees - Under Gopal Sharma - { - username: "rampal.verma.dn1", - name: "Rampal Verma", + { + username: "rampal.verma.dn1", + name: "Rampal Verma", email: "rampal.verma@workallocate.com", deptId: danaId, contractorUsername: "gopal.sharma.dana", @@ -488,11 +666,11 @@ async function seedDatabase() { aadhar: "666677778888", bankAccount: "30100666677788", bankName: "State Bank of India", - bankIfsc: "SBIN0006666" + bankIfsc: "SBIN0006666", }, - { - username: "lakhan.singh.dn2", - name: "Lakhan Singh", + { + username: "lakhan.singh.dn2", + name: "Lakhan Singh", email: "lakhan.singh@workallocate.com", deptId: danaId, contractorUsername: "gopal.sharma.dana", @@ -500,11 +678,11 @@ async function seedDatabase() { aadhar: "777788889999", bankAccount: "30100777788899", bankName: "Punjab National Bank", - bankIfsc: "PUNB0007777" + bankIfsc: "PUNB0007777", }, - { - username: "bharat.meena.dn3", - name: "Bharat Meena", + { + username: "bharat.meena.dn3", + name: "Bharat Meena", email: "bharat.meena@workallocate.com", deptId: danaId, contractorUsername: "gopal.sharma.dana", @@ -512,12 +690,12 @@ async function seedDatabase() { aadhar: "888899990000", bankAccount: "30100888899900", bankName: "HDFC Bank", - bankIfsc: "HDFC0008888" + bankIfsc: "HDFC0008888", }, // Dana Department Employees - Under Mohan Yadav - { - username: "kailash.patel.dn4", - name: "Kailash Patel", + { + username: "kailash.patel.dn4", + name: "Kailash Patel", email: "kailash.patel@workallocate.com", deptId: danaId, contractorUsername: "mohan.yadav.dana", @@ -525,11 +703,11 @@ async function seedDatabase() { aadhar: "999900001111", bankAccount: "30100999900011", bankName: "Bank of Baroda", - bankIfsc: "BARB0009999" + bankIfsc: "BARB0009999", }, - { - username: "shyam.gupta.dn5", - name: "Shyam Gupta", + { + username: "shyam.gupta.dn5", + name: "Shyam Gupta", email: "shyam.gupta@workallocate.com", deptId: danaId, contractorUsername: "mohan.yadav.dana", @@ -537,12 +715,12 @@ async function seedDatabase() { aadhar: "000011112222", bankAccount: "30100000011122", bankName: "ICICI Bank", - bankIfsc: "ICIC0000000" + bankIfsc: "ICIC0000000", }, // Tudki Department Employees - Under Suresh Kumar - { - username: "ganesh.kumar.tk1", - name: "Ganesh Kumar", + { + username: "ganesh.kumar.tk1", + name: "Ganesh Kumar", email: "ganesh.kumar@workallocate.com", deptId: tudkiId, contractorUsername: "suresh.kumar.tudki", @@ -550,11 +728,11 @@ async function seedDatabase() { aadhar: "112233445566", bankAccount: "30100112233445", bankName: "State Bank of India", - bankIfsc: "SBIN0001122" + bankIfsc: "SBIN0001122", }, - { - username: "naresh.yadav.tk2", - name: "Naresh Yadav", + { + username: "naresh.yadav.tk2", + name: "Naresh Yadav", email: "naresh.yadav@workallocate.com", deptId: tudkiId, contractorUsername: "suresh.kumar.tudki", @@ -562,11 +740,11 @@ async function seedDatabase() { aadhar: "223344556677", bankAccount: "30100223344556", bankName: "Punjab National Bank", - bankIfsc: "PUNB0002233" + bankIfsc: "PUNB0002233", }, - { - username: "mukesh.sharma.tk3", - name: "Mukesh Sharma", + { + username: "mukesh.sharma.tk3", + name: "Mukesh Sharma", email: "mukesh.sharma@workallocate.com", deptId: tudkiId, contractorUsername: "suresh.kumar.tudki", @@ -574,12 +752,12 @@ async function seedDatabase() { aadhar: "334455667788", bankAccount: "30100334455667", bankName: "HDFC Bank", - bankIfsc: "HDFC0003344" + bankIfsc: "HDFC0003344", }, // Tudki Department Employees - Under Dinesh Gupta - { - username: "pappu.singh.tk4", - name: "Pappu Singh", + { + username: "pappu.singh.tk4", + name: "Pappu Singh", email: "pappu.singh@workallocate.com", deptId: tudkiId, contractorUsername: "dinesh.gupta.tudki", @@ -587,11 +765,11 @@ async function seedDatabase() { aadhar: "445566778899", bankAccount: "30100445566778", bankName: "Bank of Baroda", - bankIfsc: "BARB0004455" + bankIfsc: "BARB0004455", }, - { - username: "deepak.verma.tk5", - name: "Deepak Verma", + { + username: "deepak.verma.tk5", + name: "Deepak Verma", email: "deepak.verma@workallocate.com", deptId: tudkiId, contractorUsername: "dinesh.gupta.tudki", @@ -599,11 +777,11 @@ async function seedDatabase() { aadhar: "556677889900", bankAccount: "30100556677889", bankName: "ICICI Bank", - bankIfsc: "ICIC0005566" + bankIfsc: "ICIC0005566", }, - { - username: "rahul.meena.tk6", - name: "Rahul Meena", + { + username: "rahul.meena.tk6", + name: "Rahul Meena", email: "rahul.meena@workallocate.com", deptId: tudkiId, contractorUsername: "dinesh.gupta.tudki", @@ -611,14 +789,14 @@ async function seedDatabase() { aadhar: "667788990011", bankAccount: "30100667788990", bankName: "Axis Bank", - bankIfsc: "UTIB0006677" - } + bankIfsc: "UTIB0006677", + }, ]; for (const emp of employees) { const existing = await db.query<{ id: number }[]>( "SELECT id FROM users WHERE username = ?", - [emp.username] + [emp.username], ); if (existing.length === 0) { const contractorId = contractorIds[emp.contractorUsername]; @@ -627,8 +805,21 @@ async function seedDatabase() { `INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active, phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [emp.username, emp.name, emp.email, employeePassword, "Employee", emp.deptId, contractorId, true, - emp.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc] + [ + emp.username, + emp.name, + emp.email, + employeePassword, + "Employee", + emp.deptId, + contractorId, + true, + emp.phone, + emp.aadhar, + emp.bankAccount, + emp.bankName, + emp.bankIfsc, + ], ); console.log(` ✅ ${emp.name} created`); } @@ -640,51 +831,65 @@ async function seedDatabase() { // 7. Seed Contractor Rates for all contractors console.log("💰 Seeding contractor rates..."); const today = new Date().toISOString().split("T")[0]; - + // Get all sub-departments for rate assignment - const allSubDepts = await db.query<{ id: number; name: string; department_id: number }[]>( - "SELECT id, name, department_id FROM sub_departments" + const allSubDepts = await db.query< + { id: number; name: string; department_id: number }[] + >( + "SELECT id, name, department_id FROM sub_departments", ); - + // Create rates for each contractor based on their department for (const [username, contractorId] of Object.entries(contractorIds)) { const existingRate = await db.query<{ id: number }[]>( "SELECT id FROM contractor_rates WHERE contractor_id = ?", - [contractorId] + [contractorId], ); - + if (existingRate.length === 0) { // Find the contractor's department - const contractor = contractors.find(c => c.username === username); + const contractor = contractors.find((c) => c.username === username); if (contractor) { // Get sub-departments for this contractor's department - const deptSubDepts = allSubDepts.filter(sd => sd.department_id === contractor.deptId); - + const deptSubDepts = allSubDepts.filter((sd) => + sd.department_id === contractor.deptId + ); + // Create rates for Loading/Unloading sub-department (Per Bag rates) - const loadingSubDept = deptSubDepts.find(sd => sd.name === "Loading/Unloading"); + const loadingSubDept = deptSubDepts.find((sd) => + sd.name === "Loading/Unloading" + ); if (loadingSubDept) { // Get activities for this sub-department const activities = await db.query<{ id: number; name: string }[]>( "SELECT id, name FROM activities WHERE sub_department_id = ? LIMIT 3", - [loadingSubDept.id] + [loadingSubDept.id], ); - + for (const activity of activities) { const rate = 5 + Math.random() * 3; // Random rate between 5-8 per bag await db.execute( "INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)", - [contractorId, loadingSubDept.id, activity.name, rate.toFixed(2), today] + [ + contractorId, + loadingSubDept.id, + activity.name, + rate.toFixed(2), + today, + ], ); } } - + // Create fixed rates for other sub-departments - const fixedSubDepts = deptSubDepts.filter(sd => sd.name !== "Loading/Unloading"); + const fixedSubDepts = deptSubDepts.filter((sd) => + sd.name !== "Loading/Unloading" + ); for (const subDept of fixedSubDepts.slice(0, 2)) { // Limit to 2 fixed rate sub-depts per contractor const rate = 300 + Math.random() * 200; // Random rate between 300-500 per person await db.execute( "INSERT INTO contractor_rates (contractor_id, sub_department_id, rate, effective_date) VALUES (?, ?, ?, ?)", - [contractorId, subDept.id, rate.toFixed(2), today] + [contractorId, subDept.id, rate.toFixed(2), today], ); } } @@ -716,7 +921,6 @@ async function seedDatabase() { - Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `); - } catch (error) { console.error("❌ Error seeding database:", (error as Error).message); Deno.exit(1); diff --git a/backend-deno/types/index.ts b/backend-deno/types/index.ts index 244e934..5ae8468 100644 --- a/backend-deno/types/index.ts +++ b/backend-deno/types/index.ts @@ -51,7 +51,11 @@ export interface SubDepartment { } // Work allocation types -export type AllocationStatus = "Pending" | "InProgress" | "Completed" | "Cancelled"; +export type AllocationStatus = + | "Pending" + | "InProgress" + | "Completed" + | "Cancelled"; export interface WorkAllocation { id: number; @@ -76,7 +80,12 @@ export interface WorkAllocation { } // Attendance types -export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late"; +export type AttendanceStatus = + | "CheckedIn" + | "CheckedOut" + | "Absent" + | "HalfDay" + | "Late"; export interface Attendance { id: number; diff --git a/docker-compose.yml b/docker-compose.yml index 0bf66c8..a2f5419 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,16 @@ services: - mysql_data:/var/lib/mysql - ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-padmin123"] + test: [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "root", + "-padmin123", + ] interval: 5s timeout: 5s retries: 10 diff --git a/eslint.config.js b/eslint.config.js index 3364500..e01c739 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,13 +1,13 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist', 'node_modules', 'backend', 'backend-deno'] }, + { ignores: ["dist", "node_modules", "backend", "backend-deno"] }, { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, ...tseslint.configs.recommended, @@ -16,21 +16,26 @@ export default tseslint.config( ecmaVersion: 2020, globals: globals.browser, parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', + ecmaVersion: "latest", + ecmaFeatures: { tsx: true }, + sourceType: "module", }, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - '@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'warn', - 'no-unused-vars': 'off', + "react-refresh/only-export-components": ["warn", { + allowConstantExport: true, + }], + "@typescript-eslint/no-unused-vars": ["warn", { + varsIgnorePattern: "^_", + argsIgnorePattern: "^_", + }], + "@typescript-eslint/no-explicit-any": "warn", + "no-unused-vars": "off", }, }, -) +); diff --git a/index.html b/index.html index f083ce2..2b85627 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/package-lock.json b/package-lock.json index a0fa34e..7408a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,10 @@ "name": "my-dashboard", "version": "0.0.0", "dependencies": { - "lucide-react": "^0.555.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "recharts": "^3.5.0", + "lucide-react": "0.555.0", + "react": "19.2.0", + "react-dom": "19.2.0", + "recharts": "3.5.0", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/package.json b/package.json index 31af9bc..c7bb705 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "preview": "vite preview" }, "dependencies": { - "lucide-react": "^0.555.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "recharts": "^3.5.0", + "lucide-react": "0.555.0", + "react": "19.2.0", + "react-dom": "19.2.0", + "recharts": "3.5.0", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 433af0f..76592f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,11 +1,10 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: lucide-react: @@ -21,19 +20,19 @@ importers: specifier: ^3.5.0 version: 3.5.0(@types/react@19.2.7)(eslint@9.39.1(jiti@1.21.7))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1) devDependencies: - '@eslint/js': + "@eslint/js": specifier: ^9.39.1 version: 9.39.1 - '@types/node': + "@types/node": specifier: ^24.10.1 version: 24.10.1 - '@types/react': + "@types/react": specifier: ^19.2.5 version: 19.2.7 - '@types/react-dom': + "@types/react-dom": specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) - '@vitejs/plugin-react': + "@vitejs/plugin-react": specifier: ^5.1.1 version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)) autoprefixer: @@ -65,334 +64,469 @@ importers: version: 7.2.4(@types/node@24.10.1)(jiti@1.21.7) packages: + "@alloc/quick-lru@5.2.0": + resolution: { + integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, + } + engines: { node: ">=10" } - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} + "@babel/code-frame@7.27.1": + resolution: { + integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, + } + engines: { node: ">=6.9.0" } - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} + "@babel/compat-data@7.28.5": + resolution: { + integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==, + } + engines: { node: ">=6.9.0" } - '@babel/compat-data@7.28.5': - resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} - engines: {node: '>=6.9.0'} + "@babel/core@7.28.5": + resolution: { + integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==, + } + engines: { node: ">=6.9.0" } - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} - engines: {node: '>=6.9.0'} + "@babel/generator@7.28.5": + resolution: { + integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==, + } + engines: { node: ">=6.9.0" } - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} - engines: {node: '>=6.9.0'} + "@babel/helper-compilation-targets@7.27.2": + resolution: { + integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} + "@babel/helper-globals@7.28.0": + resolution: { + integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} + "@babel/helper-module-imports@7.27.1": + resolution: { + integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} + "@babel/helper-module-transforms@7.28.3": + resolution: { + integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0 + "@babel/core": ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} + "@babel/helper-plugin-utils@7.27.1": + resolution: { + integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} + "@babel/helper-string-parser@7.27.1": + resolution: { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} + "@babel/helper-validator-identifier@7.28.5": + resolution: { + integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} + "@babel/helper-validator-option@7.27.1": + resolution: { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: ">=6.9.0" } - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} + "@babel/helpers@7.28.4": + resolution: { + integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==, + } + engines: { node: ">=6.9.0" } - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} + "@babel/parser@7.28.5": + resolution: { + integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==, + } + engines: { node: ">=6.0.0" } hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} + "@babel/plugin-transform-react-jsx-self@7.27.1": + resolution: { + integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 + "@babel/core": ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} + "@babel/plugin-transform-react-jsx-source@7.27.1": + resolution: { + integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 + "@babel/core": ^7.0.0-0 - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} + "@babel/template@7.27.2": + resolution: { + integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==, + } + engines: { node: ">=6.9.0" } - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} - engines: {node: '>=6.9.0'} + "@babel/traverse@7.28.5": + resolution: { + integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==, + } + engines: { node: ">=6.9.0" } - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} + "@babel/types@7.28.5": + resolution: { + integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==, + } + engines: { node: ">=6.9.0" } - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} + "@esbuild/aix-ppc64@0.25.12": + resolution: { + integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} + "@esbuild/android-arm64@0.25.12": + resolution: { + integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} + "@esbuild/android-arm@0.25.12": + resolution: { + integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, + } + engines: { node: ">=18" } cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} + "@esbuild/android-x64@0.25.12": + resolution: { + integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, + } + engines: { node: ">=18" } cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} + "@esbuild/darwin-arm64@0.25.12": + resolution: { + integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} + "@esbuild/darwin-x64@0.25.12": + resolution: { + integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, + } + engines: { node: ">=18" } cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} + "@esbuild/freebsd-arm64@0.25.12": + resolution: { + integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} + "@esbuild/freebsd-x64@0.25.12": + resolution: { + integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} + "@esbuild/linux-arm64@0.25.12": + resolution: { + integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, + } + engines: { node: ">=18" } cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} + "@esbuild/linux-arm@0.25.12": + resolution: { + integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, + } + engines: { node: ">=18" } cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} + "@esbuild/linux-ia32@0.25.12": + resolution: { + integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, + } + engines: { node: ">=18" } cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} + "@esbuild/linux-loong64@0.25.12": + resolution: { + integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, + } + engines: { node: ">=18" } cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} + "@esbuild/linux-mips64el@0.25.12": + resolution: { + integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, + } + engines: { node: ">=18" } cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} + "@esbuild/linux-ppc64@0.25.12": + resolution: { + integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} + "@esbuild/linux-riscv64@0.25.12": + resolution: { + integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, + } + engines: { node: ">=18" } cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} + "@esbuild/linux-s390x@0.25.12": + resolution: { + integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, + } + engines: { node: ">=18" } cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} + "@esbuild/linux-x64@0.25.12": + resolution: { + integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, + } + engines: { node: ">=18" } cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} + "@esbuild/netbsd-arm64@0.25.12": + resolution: { + integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} + "@esbuild/netbsd-x64@0.25.12": + resolution: { + integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} + "@esbuild/openbsd-arm64@0.25.12": + resolution: { + integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} + "@esbuild/openbsd-x64@0.25.12": + resolution: { + integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, + } + engines: { node: ">=18" } cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} + "@esbuild/openharmony-arm64@0.25.12": + resolution: { + integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} + "@esbuild/sunos-x64@0.25.12": + resolution: { + integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, + } + engines: { node: ">=18" } cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} + "@esbuild/win32-arm64@0.25.12": + resolution: { + integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} + "@esbuild/win32-ia32@0.25.12": + resolution: { + integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, + } + engines: { node: ">=18" } cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} + "@esbuild/win32-x64@0.25.12": + resolution: { + integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, + } + engines: { node: ">=18" } cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + "@eslint-community/eslint-utils@4.9.0": + resolution: { + integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + "@eslint-community/regexpp@4.12.2": + resolution: { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/config-array@0.21.1": + resolution: { + integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/config-helpers@0.4.2": + resolution: { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/core@0.17.0": + resolution: { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/eslintrc@3.3.1": + resolution: { + integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/js@9.39.1': - resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/js@9.39.1": + resolution: { + integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/object-schema@2.1.7": + resolution: { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/plugin-kit@0.4.1": + resolution: { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} + "@humanfs/core@0.19.1": + resolution: { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, + } + engines: { node: ">=18.18.0" } - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} + "@humanfs/node@0.16.7": + resolution: { + integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, + } + engines: { node: ">=18.18.0" } - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + "@humanwhocodes/module-importer@1.0.1": + resolution: { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} + "@humanwhocodes/retry@0.4.3": + resolution: { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + "@jridgewell/gen-mapping@0.3.13": + resolution: { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + "@jridgewell/remapping@2.3.5": + resolution: { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + "@jridgewell/resolve-uri@3.1.2": + resolution: { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + "@jridgewell/sourcemap-codec@1.5.5": + resolution: { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + "@jridgewell/trace-mapping@0.3.31": + resolution: { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + "@nodelib/fs.scandir@2.1.5": + resolution: { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: ">= 8" } - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + "@nodelib/fs.stat@2.0.5": + resolution: { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: ">= 8" } - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + "@nodelib/fs.walk@1.2.8": + resolution: { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: ">= 8" } - '@reduxjs/toolkit@2.11.0': - resolution: {integrity: sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==} + "@reduxjs/toolkit@2.11.0": + resolution: { + integrity: sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==, + } peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -402,467 +536,695 @@ packages: react-redux: optional: true - '@rolldown/pluginutils@1.0.0-beta.47': - resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + "@rolldown/pluginutils@1.0.0-beta.47": + resolution: { + integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==, + } - '@rollup/rollup-android-arm-eabi@4.53.3': - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + "@rollup/rollup-android-arm-eabi@4.53.3": + resolution: { + integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==, + } cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.3': - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + "@rollup/rollup-android-arm64@4.53.3": + resolution: { + integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==, + } cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.3': - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + "@rollup/rollup-darwin-arm64@4.53.3": + resolution: { + integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==, + } cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.53.3': - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + "@rollup/rollup-darwin-x64@4.53.3": + resolution: { + integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==, + } cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.53.3': - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + "@rollup/rollup-freebsd-arm64@4.53.3": + resolution: { + integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==, + } cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.3': - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + "@rollup/rollup-freebsd-x64@4.53.3": + resolution: { + integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==, + } cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + "@rollup/rollup-linux-arm-gnueabihf@4.53.3": + resolution: { + integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==, + } cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.53.3': - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + "@rollup/rollup-linux-arm-musleabihf@4.53.3": + resolution: { + integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==, + } cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.53.3': - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + "@rollup/rollup-linux-arm64-gnu@4.53.3": + resolution: { + integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==, + } cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.53.3': - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + "@rollup/rollup-linux-arm64-musl@4.53.3": + resolution: { + integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==, + } cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.53.3': - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + "@rollup/rollup-linux-loong64-gnu@4.53.3": + resolution: { + integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==, + } cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.53.3': - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + "@rollup/rollup-linux-ppc64-gnu@4.53.3": + resolution: { + integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==, + } cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.53.3': - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + "@rollup/rollup-linux-riscv64-gnu@4.53.3": + resolution: { + integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==, + } cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.53.3': - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + "@rollup/rollup-linux-riscv64-musl@4.53.3": + resolution: { + integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==, + } cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.53.3': - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + "@rollup/rollup-linux-s390x-gnu@4.53.3": + resolution: { + integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==, + } cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.53.3': - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + "@rollup/rollup-linux-x64-gnu@4.53.3": + resolution: { + integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==, + } cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.53.3': - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + "@rollup/rollup-linux-x64-musl@4.53.3": + resolution: { + integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==, + } cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.53.3': - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + "@rollup/rollup-openharmony-arm64@4.53.3": + resolution: { + integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==, + } cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.3': - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + "@rollup/rollup-win32-arm64-msvc@4.53.3": + resolution: { + integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==, + } cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.53.3': - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + "@rollup/rollup-win32-ia32-msvc@4.53.3": + resolution: { + integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==, + } cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.3': - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + "@rollup/rollup-win32-x64-gnu@4.53.3": + resolution: { + integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==, + } cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.3': - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + "@rollup/rollup-win32-x64-msvc@4.53.3": + resolution: { + integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==, + } cpu: [x64] os: [win32] - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + "@standard-schema/spec@1.0.0": + resolution: { + integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==, + } - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + "@standard-schema/utils@0.3.0": + resolution: { + integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==, + } - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + "@types/babel__core@7.20.5": + resolution: { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + "@types/babel__generator@7.27.0": + resolution: { + integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, + } - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + "@types/babel__template@7.4.4": + resolution: { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + "@types/babel__traverse@7.28.0": + resolution: { + integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + } - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + "@types/d3-array@3.2.2": + resolution: { + integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==, + } - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + "@types/d3-color@3.1.3": + resolution: { + integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==, + } - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + "@types/d3-ease@3.0.2": + resolution: { + integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==, + } - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + "@types/d3-interpolate@3.0.4": + resolution: { + integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==, + } - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + "@types/d3-path@3.1.1": + resolution: { + integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==, + } - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + "@types/d3-scale@4.0.9": + resolution: { + integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==, + } - '@types/d3-shape@3.1.7': - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + "@types/d3-shape@3.1.7": + resolution: { + integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==, + } - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + "@types/d3-time@3.0.4": + resolution: { + integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==, + } - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + "@types/d3-timer@3.0.2": + resolution: { + integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==, + } - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + "@types/estree@1.0.8": + resolution: { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + "@types/json-schema@7.0.15": + resolution: { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + "@types/node@24.10.1": + resolution: { + integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==, + } - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + "@types/react-dom@19.2.3": + resolution: { + integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==, + } peerDependencies: - '@types/react': ^19.2.0 + "@types/react": ^19.2.0 - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + "@types/react@19.2.7": + resolution: { + integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==, + } - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + "@types/use-sync-external-store@0.0.6": + resolution: { + integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==, + } - '@vitejs/plugin-react@5.1.1': - resolution: {integrity: sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==} - engines: {node: ^20.19.0 || >=22.12.0} + "@vitejs/plugin-react@5.1.1": + resolution: { + integrity: sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + resolution: { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} + resolution: { + integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==, + } + engines: { node: ">=0.4.0" } hasBin: true ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + resolution: { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, + } ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + resolution: { + integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==, + } anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + resolution: { + integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, + } + engines: { node: ">= 8" } arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + resolution: { + integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, + } argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } autoprefixer@10.4.22: - resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} - engines: {node: ^10 || ^12 || >=14} + resolution: { + integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==, + } + engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: postcss: ^8.1.0 balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } baseline-browser-mapping@2.8.31: - resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} + resolution: { + integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==, + } hasBin: true binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, + } + engines: { node: ">=8" } brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + resolution: { + integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==, + } braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, + } + engines: { node: ">=8" } browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + resolution: { + integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} + resolution: { + integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==, + } + engines: { node: ">= 6" } caniuse-lite@1.0.30001757: - resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + resolution: { + integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==, + } chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + resolution: { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, + } + engines: { node: ">= 8.10.0" } clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, + } + engines: { node: ">=6" } color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + resolution: { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + resolution: { + integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, + } + engines: { node: ">= 6" } concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + resolution: { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + resolution: { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} + resolution: { + integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, + } + engines: { node: ">=4" } hasBin: true csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + resolution: { + integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, + } d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==, + } + engines: { node: ">=12" } d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==, + } + engines: { node: ">=12" } d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==, + } + engines: { node: ">=12" } d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==, + } + engines: { node: ">=12" } d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==, + } + engines: { node: ">=12" } d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==, + } + engines: { node: ">=12" } d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==, + } + engines: { node: ">=12" } d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==, + } + engines: { node: ">=12" } d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==, + } + engines: { node: ">=12" } d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==, + } + engines: { node: ">=12" } d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==, + } + engines: { node: ">=12" } debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + resolution: { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + resolution: { + integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==, + } deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + resolution: { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + resolution: { + integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==, + } dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + resolution: { + integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==, + } electron-to-chromium@1.5.262: - resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} + resolution: { + integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==, + } es-toolkit@1.42.0: - resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==} + resolution: { + integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==, + } esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} + resolution: { + integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, + } + engines: { node: ">=18" } hasBin: true escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} + resolution: { + integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==, + } + engines: { node: ">=18" } peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react-perf@3.3.3: - resolution: {integrity: sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==} - engines: {node: '>=6.9.1'} + resolution: { + integrity: sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==, + } + engines: { node: ">=6.9.1" } peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 eslint-plugin-react-refresh@0.4.24: - resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + resolution: { + integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==, + } peerDependencies: - eslint: '>=8.40' + eslint: ">=8.40" eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } eslint@9.39.1: - resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: { + integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } hasBin: true peerDependencies: - jiti: '*' + jiti: "*" peerDependenciesMeta: jiti: optional: true espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} + resolution: { + integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, + } + engines: { node: ">=0.10" } esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + resolution: { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + resolution: { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + resolution: { + integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, + } fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + resolution: { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} + resolution: { + integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, + } + engines: { node: ">=8.6.0" } fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + resolution: { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + resolution: { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + resolution: { + integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, + } fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + resolution: { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -870,281 +1232,425 @@ packages: optional: true file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + resolution: { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, + } + engines: { node: ">=8" } find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + resolution: { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + resolution: { + integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==, + } fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + resolution: { + integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==, + } fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + resolution: { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + resolution: { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: ">= 6" } glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + resolution: { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} + resolution: { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: ">=18" } globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} + resolution: { + integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, + } + engines: { node: ">=18" } has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + resolution: { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, + } + engines: { node: ">= 0.4" } hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + resolution: { + integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==, + } hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + resolution: { + integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==, + } ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + resolution: { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + resolution: { + integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==, + } immer@11.0.0: - resolution: {integrity: sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==} + resolution: { + integrity: sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==, + } import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + resolution: { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==, + } + engines: { node: ">=12" } is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, + } + engines: { node: ">=8" } is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} + resolution: { + integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==, + } + engines: { node: ">= 0.4" } is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + resolution: { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: ">=0.12.0" } isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + resolution: { + integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==, + } hasBin: true js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + resolution: { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, + } hasBin: true jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: ">=6" } hasBin: true json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + resolution: { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + resolution: { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + resolution: { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } hasBin: true keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + resolution: { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + resolution: { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + resolution: { + integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, + } + engines: { node: ">=14" } lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + resolution: { + integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, + } locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + resolution: { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + resolution: { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } lucide-react@0.555.0: - resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} + resolution: { + integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==, + } peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + resolution: { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: ">= 8" } micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + resolution: { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, + } + engines: { node: ">=8.6" } minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + resolution: { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + resolution: { + integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==, + } nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + resolution: { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + resolution: { + integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, + } normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, + } + engines: { node: ">=0.10.0" } normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==, + } + engines: { node: ">=0.10.0" } object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: ">=0.10.0" } object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} + resolution: { + integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==, + } + engines: { node: ">= 6" } optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} + resolution: { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + resolution: { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + resolution: { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, + } + engines: { node: ">=8.6" } picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} + resolution: { + integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, + } + engines: { node: ">=12" } pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==, + } + engines: { node: ">=0.10.0" } pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} + resolution: { + integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==, + } + engines: { node: ">= 6" } postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} + resolution: { + integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==, + } + engines: { node: ">=14.0.0" } peerDependencies: postcss: ^8.0.0 postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} + resolution: { + integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==, + } + engines: { node: ^12 || ^14 || >= 16 } peerDependencies: postcss: ^8.4.21 postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} + resolution: { + integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==, + } + engines: { node: ">= 18" } peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' + jiti: ">=1.21.0" + postcss: ">=8.0.9" tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: @@ -1158,220 +1664,316 @@ packages: optional: true postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} + resolution: { + integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==, + } + engines: { node: ">=12.0" } peerDependencies: postcss: ^8.2.14 postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} + resolution: { + integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==, + } + engines: { node: ">=4" } postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + resolution: { + integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==, + } postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} + resolution: { + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, + } + engines: { node: ^10 || ^12 || >=14 } prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + resolution: { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + resolution: { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + resolution: { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + resolution: { + integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==, + } peerDependencies: react: ^19.2.0 react-is@19.2.0: - resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + resolution: { + integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==, + } react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + resolution: { + integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==, + } peerDependencies: - '@types/react': ^18.2.25 || ^19 + "@types/react": ^18.2.25 || ^19 react: ^18.0 || ^19 redux: ^5.0.0 peerDependenciesMeta: - '@types/react': + "@types/react": optional: true redux: optional: true react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==, + } + engines: { node: ">=0.10.0" } react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==, + } + engines: { node: ">=0.10.0" } read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + resolution: { + integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==, + } readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + resolution: { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, + } + engines: { node: ">=8.10.0" } recharts@3.5.0: - resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==} - engines: {node: '>=18'} + resolution: { + integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==, + } + engines: { node: ">=18" } peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + resolution: { + integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==, + } peerDependencies: redux: ^5.0.0 redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + resolution: { + integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==, + } reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolution: { + integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==, + } resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolution: { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} + resolution: { + integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==, + } + engines: { node: ">= 0.4" } hasBin: true reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolution: { + integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, + } + engines: { iojs: ">=1.0.0", node: ">=0.10.0" } rollup@4.53.3: - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: { + integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + resolution: { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + resolution: { + integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, + } semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + resolution: { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } hasBin: true shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: ">=8" } sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} + resolution: { + integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==, + } + engines: { node: ">=16 || 14 >=14.17" } hasBin: true supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + resolution: { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + resolution: { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: { node: ">= 0.4" } tailwindcss@3.4.18: - resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} - engines: {node: '>=14.0.0'} + resolution: { + integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==, + } + engines: { node: ">=14.0.0" } hasBin: true thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} + resolution: { + integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==, + } + engines: { node: ">=0.8" } thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + resolution: { + integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, + } tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + resolution: { + integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==, + } tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} + resolution: { + integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, + } + engines: { node: ">=12.0.0" } to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + resolution: { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: ">=8.0" } ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + resolution: { + integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==, + } type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + resolution: { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} + resolution: { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } hasBin: true undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + resolution: { + integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, + } update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + resolution: { + integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==, + } hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: ">= 4.21.0" uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + resolution: { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + resolution: { + integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==, + } peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + resolution: { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + resolution: { + integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==, + } vite@7.2.4: - resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: { + integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" less: ^4.0.0 lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: '>=0.54.8' + stylus: ">=0.54.8" sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - '@types/node': + "@types/node": optional: true jiti: optional: true @@ -1395,54 +1997,65 @@ packages: optional: true which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } hasBin: true word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + resolution: { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + resolution: { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + resolution: { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} + resolution: { + integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==, + } + engines: { node: ">=18.0.0" } peerDependencies: zod: ^3.25.0 || ^4.0.0 zod@4.1.13: - resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + resolution: { + integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==, + } snapshots: + "@alloc/quick-lru@5.2.0": {} - '@alloc/quick-lru@5.2.0': {} - - '@babel/code-frame@7.27.1': + "@babel/code-frame@7.27.1": dependencies: - '@babel/helper-validator-identifier': 7.28.5 + "@babel/helper-validator-identifier": 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.5': {} + "@babel/compat-data@7.28.5": {} - '@babel/core@7.28.5': + "@babel/core@7.28.5": dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/remapping': 2.3.5 + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.5 + "@babel/helper-compilation-targets": 7.27.2 + "@babel/helper-module-transforms": 7.28.3(@babel/core@7.28.5) + "@babel/helpers": 7.28.4 + "@babel/parser": 7.28.5 + "@babel/template": 7.27.2 + "@babel/traverse": 7.28.5 + "@babel/types": 7.28.5 + "@jridgewell/remapping": 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -1451,192 +2064,192 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.5': + "@babel/generator@7.28.5": dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@babel/parser": 7.28.5 + "@babel/types": 7.28.5 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': + "@babel/helper-compilation-targets@7.27.2": dependencies: - '@babel/compat-data': 7.28.5 - '@babel/helper-validator-option': 7.27.1 + "@babel/compat-data": 7.28.5 + "@babel/helper-validator-option": 7.27.1 browserslist: 4.28.0 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + "@babel/helper-globals@7.28.0": {} - '@babel/helper-module-imports@7.27.1': + "@babel/helper-module-imports@7.27.1": dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + "@babel/traverse": 7.28.5 + "@babel/types": 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + "@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)": dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 + "@babel/core": 7.28.5 + "@babel/helper-module-imports": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 + "@babel/traverse": 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + "@babel/helper-plugin-utils@7.27.1": {} - '@babel/helper-string-parser@7.27.1': {} + "@babel/helper-string-parser@7.27.1": {} - '@babel/helper-validator-identifier@7.28.5': {} + "@babel/helper-validator-identifier@7.28.5": {} - '@babel/helper-validator-option@7.27.1': {} + "@babel/helper-validator-option@7.27.1": {} - '@babel/helpers@7.28.4': + "@babel/helpers@7.28.4": dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + "@babel/template": 7.27.2 + "@babel/types": 7.28.5 - '@babel/parser@7.28.5': + "@babel/parser@7.28.5": dependencies: - '@babel/types': 7.28.5 + "@babel/types": 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + "@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)": dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + "@babel/core": 7.28.5 + "@babel/helper-plugin-utils": 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + "@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)": dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + "@babel/core": 7.28.5 + "@babel/helper-plugin-utils": 7.27.1 - '@babel/template@7.27.2': + "@babel/template@7.27.2": dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + "@babel/code-frame": 7.27.1 + "@babel/parser": 7.28.5 + "@babel/types": 7.28.5 - '@babel/traverse@7.28.5': + "@babel/traverse@7.28.5": dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.5 + "@babel/helper-globals": 7.28.0 + "@babel/parser": 7.28.5 + "@babel/template": 7.27.2 + "@babel/types": 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.5': + "@babel/types@7.28.5": dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + "@babel/helper-string-parser": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 - '@esbuild/aix-ppc64@0.25.12': + "@esbuild/aix-ppc64@0.25.12": optional: true - '@esbuild/android-arm64@0.25.12': + "@esbuild/android-arm64@0.25.12": optional: true - '@esbuild/android-arm@0.25.12': + "@esbuild/android-arm@0.25.12": optional: true - '@esbuild/android-x64@0.25.12': + "@esbuild/android-x64@0.25.12": optional: true - '@esbuild/darwin-arm64@0.25.12': + "@esbuild/darwin-arm64@0.25.12": optional: true - '@esbuild/darwin-x64@0.25.12': + "@esbuild/darwin-x64@0.25.12": optional: true - '@esbuild/freebsd-arm64@0.25.12': + "@esbuild/freebsd-arm64@0.25.12": optional: true - '@esbuild/freebsd-x64@0.25.12': + "@esbuild/freebsd-x64@0.25.12": optional: true - '@esbuild/linux-arm64@0.25.12': + "@esbuild/linux-arm64@0.25.12": optional: true - '@esbuild/linux-arm@0.25.12': + "@esbuild/linux-arm@0.25.12": optional: true - '@esbuild/linux-ia32@0.25.12': + "@esbuild/linux-ia32@0.25.12": optional: true - '@esbuild/linux-loong64@0.25.12': + "@esbuild/linux-loong64@0.25.12": optional: true - '@esbuild/linux-mips64el@0.25.12': + "@esbuild/linux-mips64el@0.25.12": optional: true - '@esbuild/linux-ppc64@0.25.12': + "@esbuild/linux-ppc64@0.25.12": optional: true - '@esbuild/linux-riscv64@0.25.12': + "@esbuild/linux-riscv64@0.25.12": optional: true - '@esbuild/linux-s390x@0.25.12': + "@esbuild/linux-s390x@0.25.12": optional: true - '@esbuild/linux-x64@0.25.12': + "@esbuild/linux-x64@0.25.12": optional: true - '@esbuild/netbsd-arm64@0.25.12': + "@esbuild/netbsd-arm64@0.25.12": optional: true - '@esbuild/netbsd-x64@0.25.12': + "@esbuild/netbsd-x64@0.25.12": optional: true - '@esbuild/openbsd-arm64@0.25.12': + "@esbuild/openbsd-arm64@0.25.12": optional: true - '@esbuild/openbsd-x64@0.25.12': + "@esbuild/openbsd-x64@0.25.12": optional: true - '@esbuild/openharmony-arm64@0.25.12': + "@esbuild/openharmony-arm64@0.25.12": optional: true - '@esbuild/sunos-x64@0.25.12': + "@esbuild/sunos-x64@0.25.12": optional: true - '@esbuild/win32-arm64@0.25.12': + "@esbuild/win32-arm64@0.25.12": optional: true - '@esbuild/win32-ia32@0.25.12': + "@esbuild/win32-ia32@0.25.12": optional: true - '@esbuild/win32-x64@0.25.12': + "@esbuild/win32-x64@0.25.12": optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))': + "@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))": dependencies: eslint: 9.39.1(jiti@1.21.7) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.2': {} + "@eslint-community/regexpp@4.12.2": {} - '@eslint/config-array@0.21.1': + "@eslint/config-array@0.21.1": dependencies: - '@eslint/object-schema': 2.1.7 + "@eslint/object-schema": 2.1.7 debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + "@eslint/config-helpers@0.4.2": dependencies: - '@eslint/core': 0.17.0 + "@eslint/core": 0.17.0 - '@eslint/core@0.17.0': + "@eslint/core@0.17.0": dependencies: - '@types/json-schema': 7.0.15 + "@types/json-schema": 7.0.15 - '@eslint/eslintrc@3.3.1': + "@eslint/eslintrc@3.3.1": dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -1650,61 +2263,61 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.1': {} + "@eslint/js@9.39.1": {} - '@eslint/object-schema@2.1.7': {} + "@eslint/object-schema@2.1.7": {} - '@eslint/plugin-kit@0.4.1': + "@eslint/plugin-kit@0.4.1": dependencies: - '@eslint/core': 0.17.0 + "@eslint/core": 0.17.0 levn: 0.4.1 - '@humanfs/core@0.19.1': {} + "@humanfs/core@0.19.1": {} - '@humanfs/node@0.16.7': + "@humanfs/node@0.16.7": dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 + "@humanfs/core": 0.19.1 + "@humanwhocodes/retry": 0.4.3 - '@humanwhocodes/module-importer@1.0.1': {} + "@humanwhocodes/module-importer@1.0.1": {} - '@humanwhocodes/retry@0.4.3': {} + "@humanwhocodes/retry@0.4.3": {} - '@jridgewell/gen-mapping@0.3.13': + "@jridgewell/gen-mapping@0.3.13": dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/remapping@2.3.5': + "@jridgewell/remapping@2.3.5": dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + "@jridgewell/resolve-uri@3.1.2": {} - '@jridgewell/sourcemap-codec@1.5.5': {} + "@jridgewell/sourcemap-codec@1.5.5": {} - '@jridgewell/trace-mapping@0.3.31': + "@jridgewell/trace-mapping@0.3.31": dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 - '@nodelib/fs.scandir@2.1.5': + "@nodelib/fs.scandir@2.1.5": dependencies: - '@nodelib/fs.stat': 2.0.5 + "@nodelib/fs.stat": 2.0.5 run-parallel: 1.2.0 - '@nodelib/fs.stat@2.0.5': {} + "@nodelib/fs.stat@2.0.5": {} - '@nodelib/fs.walk@1.2.8': + "@nodelib/fs.walk@1.2.8": dependencies: - '@nodelib/fs.scandir': 2.1.5 + "@nodelib/fs.scandir": 2.1.5 fastq: 1.19.1 - '@reduxjs/toolkit@2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': + "@reduxjs/toolkit@2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0)": dependencies: - '@standard-schema/spec': 1.0.0 - '@standard-schema/utils': 0.3.0 + "@standard-schema/spec": 1.0.0 + "@standard-schema/utils": 0.3.0 immer: 11.0.0 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) @@ -1713,148 +2326,148 @@ snapshots: react: 19.2.0 react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1) - '@rolldown/pluginutils@1.0.0-beta.47': {} + "@rolldown/pluginutils@1.0.0-beta.47": {} - '@rollup/rollup-android-arm-eabi@4.53.3': + "@rollup/rollup-android-arm-eabi@4.53.3": optional: true - '@rollup/rollup-android-arm64@4.53.3': + "@rollup/rollup-android-arm64@4.53.3": optional: true - '@rollup/rollup-darwin-arm64@4.53.3': + "@rollup/rollup-darwin-arm64@4.53.3": optional: true - '@rollup/rollup-darwin-x64@4.53.3': + "@rollup/rollup-darwin-x64@4.53.3": optional: true - '@rollup/rollup-freebsd-arm64@4.53.3': + "@rollup/rollup-freebsd-arm64@4.53.3": optional: true - '@rollup/rollup-freebsd-x64@4.53.3': + "@rollup/rollup-freebsd-x64@4.53.3": optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + "@rollup/rollup-linux-arm-gnueabihf@4.53.3": optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.3': + "@rollup/rollup-linux-arm-musleabihf@4.53.3": optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.3': + "@rollup/rollup-linux-arm64-gnu@4.53.3": optional: true - '@rollup/rollup-linux-arm64-musl@4.53.3': + "@rollup/rollup-linux-arm64-musl@4.53.3": optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.3': + "@rollup/rollup-linux-loong64-gnu@4.53.3": optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.3': + "@rollup/rollup-linux-ppc64-gnu@4.53.3": optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.3': + "@rollup/rollup-linux-riscv64-gnu@4.53.3": optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.3': + "@rollup/rollup-linux-riscv64-musl@4.53.3": optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.3': + "@rollup/rollup-linux-s390x-gnu@4.53.3": optional: true - '@rollup/rollup-linux-x64-gnu@4.53.3': + "@rollup/rollup-linux-x64-gnu@4.53.3": optional: true - '@rollup/rollup-linux-x64-musl@4.53.3': + "@rollup/rollup-linux-x64-musl@4.53.3": optional: true - '@rollup/rollup-openharmony-arm64@4.53.3': + "@rollup/rollup-openharmony-arm64@4.53.3": optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.3': + "@rollup/rollup-win32-arm64-msvc@4.53.3": optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.3': + "@rollup/rollup-win32-ia32-msvc@4.53.3": optional: true - '@rollup/rollup-win32-x64-gnu@4.53.3': + "@rollup/rollup-win32-x64-gnu@4.53.3": optional: true - '@rollup/rollup-win32-x64-msvc@4.53.3': + "@rollup/rollup-win32-x64-msvc@4.53.3": optional: true - '@standard-schema/spec@1.0.0': {} + "@standard-schema/spec@1.0.0": {} - '@standard-schema/utils@0.3.0': {} + "@standard-schema/utils@0.3.0": {} - '@types/babel__core@7.20.5': + "@types/babel__core@7.20.5": dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + "@babel/parser": 7.28.5 + "@babel/types": 7.28.5 + "@types/babel__generator": 7.27.0 + "@types/babel__template": 7.4.4 + "@types/babel__traverse": 7.28.0 - '@types/babel__generator@7.27.0': + "@types/babel__generator@7.27.0": dependencies: - '@babel/types': 7.28.5 + "@babel/types": 7.28.5 - '@types/babel__template@7.4.4': + "@types/babel__template@7.4.4": dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + "@babel/parser": 7.28.5 + "@babel/types": 7.28.5 - '@types/babel__traverse@7.28.0': + "@types/babel__traverse@7.28.0": dependencies: - '@babel/types': 7.28.5 + "@babel/types": 7.28.5 - '@types/d3-array@3.2.2': {} + "@types/d3-array@3.2.2": {} - '@types/d3-color@3.1.3': {} + "@types/d3-color@3.1.3": {} - '@types/d3-ease@3.0.2': {} + "@types/d3-ease@3.0.2": {} - '@types/d3-interpolate@3.0.4': + "@types/d3-interpolate@3.0.4": dependencies: - '@types/d3-color': 3.1.3 + "@types/d3-color": 3.1.3 - '@types/d3-path@3.1.1': {} + "@types/d3-path@3.1.1": {} - '@types/d3-scale@4.0.9': + "@types/d3-scale@4.0.9": dependencies: - '@types/d3-time': 3.0.4 + "@types/d3-time": 3.0.4 - '@types/d3-shape@3.1.7': + "@types/d3-shape@3.1.7": dependencies: - '@types/d3-path': 3.1.1 + "@types/d3-path": 3.1.1 - '@types/d3-time@3.0.4': {} + "@types/d3-time@3.0.4": {} - '@types/d3-timer@3.0.2': {} + "@types/d3-timer@3.0.2": {} - '@types/estree@1.0.8': {} + "@types/estree@1.0.8": {} - '@types/json-schema@7.0.15': {} + "@types/json-schema@7.0.15": {} - '@types/node@24.10.1': + "@types/node@24.10.1": dependencies: undici-types: 7.16.0 - '@types/react-dom@19.2.3(@types/react@19.2.7)': + "@types/react-dom@19.2.3(@types/react@19.2.7)": dependencies: - '@types/react': 19.2.7 + "@types/react": 19.2.7 - '@types/react@19.2.7': + "@types/react@19.2.7": dependencies: csstype: 3.2.3 - '@types/use-sync-external-store@0.0.6': {} + "@types/use-sync-external-store@0.0.6": {} - '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7))': + "@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7))": dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.47 - '@types/babel__core': 7.20.5 + "@babel/core": 7.28.5 + "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.28.5) + "@babel/plugin-transform-react-jsx-source": 7.27.1(@babel/core@7.28.5) + "@rolldown/pluginutils": 1.0.0-beta.47 + "@types/babel__core": 7.20.5 react-refresh: 0.18.0 vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7) transitivePeerDependencies: @@ -2024,32 +2637,32 @@ snapshots: 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 + "@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 escalade@3.2.0: {} @@ -2057,8 +2670,8 @@ snapshots: eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@1.21.7)): dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 + "@babel/core": 7.28.5 + "@babel/parser": 7.28.5 eslint: 9.39.1(jiti@1.21.7) hermes-parser: 0.25.1 zod: 4.1.13 @@ -2085,18 +2698,18 @@ snapshots: eslint@9.39.1(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.1 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + "@eslint-community/eslint-utils": 4.9.0(eslint@9.39.1(jiti@1.21.7)) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.1 + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.1 + "@eslint/js": 9.39.1 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.7 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -2148,8 +2761,8 @@ snapshots: fast-glob@3.3.3: dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -2419,11 +3032,11 @@ snapshots: react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1): dependencies: - '@types/use-sync-external-store': 0.0.6 + "@types/use-sync-external-store": 0.0.6 react: 19.2.0 use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: - '@types/react': 19.2.7 + "@types/react": 19.2.7 redux: 5.0.1 react-refresh@0.18.0: {} @@ -2440,7 +3053,7 @@ snapshots: recharts@3.5.0(@types/react@19.2.7)(eslint@9.39.1(jiti@1.21.7))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0) + "@reduxjs/toolkit": 2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.42.0 @@ -2456,7 +3069,7 @@ snapshots: use-sync-external-store: 1.6.0(react@19.2.0) victory-vendor: 37.3.6 transitivePeerDependencies: - - '@types/react' + - "@types/react" - eslint - redux @@ -2480,30 +3093,30 @@ snapshots: rollup@4.53.3: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.3 - '@rollup/rollup-android-arm64': 4.53.3 - '@rollup/rollup-darwin-arm64': 4.53.3 - '@rollup/rollup-darwin-x64': 4.53.3 - '@rollup/rollup-freebsd-arm64': 4.53.3 - '@rollup/rollup-freebsd-x64': 4.53.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 - '@rollup/rollup-linux-arm64-musl': 4.53.3 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 - '@rollup/rollup-linux-x64-gnu': 4.53.3 - '@rollup/rollup-linux-x64-musl': 4.53.3 - '@rollup/rollup-openharmony-arm64': 4.53.3 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 - '@rollup/rollup-win32-x64-gnu': 4.53.3 - '@rollup/rollup-win32-x64-msvc': 4.53.3 + "@rollup/rollup-android-arm-eabi": 4.53.3 + "@rollup/rollup-android-arm64": 4.53.3 + "@rollup/rollup-darwin-arm64": 4.53.3 + "@rollup/rollup-darwin-x64": 4.53.3 + "@rollup/rollup-freebsd-arm64": 4.53.3 + "@rollup/rollup-freebsd-x64": 4.53.3 + "@rollup/rollup-linux-arm-gnueabihf": 4.53.3 + "@rollup/rollup-linux-arm-musleabihf": 4.53.3 + "@rollup/rollup-linux-arm64-gnu": 4.53.3 + "@rollup/rollup-linux-arm64-musl": 4.53.3 + "@rollup/rollup-linux-loong64-gnu": 4.53.3 + "@rollup/rollup-linux-ppc64-gnu": 4.53.3 + "@rollup/rollup-linux-riscv64-gnu": 4.53.3 + "@rollup/rollup-linux-riscv64-musl": 4.53.3 + "@rollup/rollup-linux-s390x-gnu": 4.53.3 + "@rollup/rollup-linux-x64-gnu": 4.53.3 + "@rollup/rollup-linux-x64-musl": 4.53.3 + "@rollup/rollup-openharmony-arm64": 4.53.3 + "@rollup/rollup-win32-arm64-msvc": 4.53.3 + "@rollup/rollup-win32-ia32-msvc": 4.53.3 + "@rollup/rollup-win32-x64-gnu": 4.53.3 + "@rollup/rollup-win32-x64-msvc": 4.53.3 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2526,7 +3139,7 @@ snapshots: sucrase@3.35.1: dependencies: - '@jridgewell/gen-mapping': 0.3.13 + "@jridgewell/gen-mapping": 0.3.13 commander: 4.1.1 lines-and-columns: 1.2.4 mz: 2.7.0 @@ -2542,7 +3155,7 @@ snapshots: tailwindcss@3.4.18: dependencies: - '@alloc/quick-lru': 5.2.0 + "@alloc/quick-lru": 5.2.0 arg: 5.0.2 chokidar: 3.6.0 didyoumean: 1.2.2 @@ -2615,13 +3228,13 @@ snapshots: victory-vendor@37.3.6: dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.7 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 + "@types/d3-array": 3.2.2 + "@types/d3-ease": 3.0.2 + "@types/d3-interpolate": 3.0.4 + "@types/d3-scale": 4.0.9 + "@types/d3-shape": 3.1.7 + "@types/d3-time": 3.0.4 + "@types/d3-timer": 3.0.2 d3-array: 3.2.4 d3-ease: 3.0.1 d3-interpolate: 3.0.1 @@ -2639,7 +3252,7 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.1 + "@types/node": 24.10.1 fsevents: 2.3.3 jiti: 1.21.7 diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/public/vite.svg b/public/vite.svg index e7b8dfb..0fee287 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1,40 @@ - \ No newline at end of file + diff --git a/src/App.tsx b/src/App.tsx index 44a873c..00de6ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,46 +1,56 @@ -import React, { useState } from 'react'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; -import { Sidebar } from './components/layout/Sidebar'; -import { Header } from './components/layout/Header'; -import { DashboardPage } from './pages/DashboardPage'; -import { UsersPage } from './pages/UsersPage'; -import { WorkAllocationPage } from './pages/WorkAllocationPage'; -import { AttendancePage } from './pages/AttendancePage'; -import { RatesPage } from './pages/RatesPage'; -import { EmployeeSwapPage } from './pages/EmployeeSwapPage'; -import { LoginPage } from './pages/LoginPage'; -import { ReportingPage } from './pages/ReportingPage'; -import { StandardRatesPage } from './pages/StandardRatesPage'; -import { AllRatesPage } from './pages/AllRatesPage'; -import { ActivitiesPage } from './pages/ActivitiesPage'; +import React, { useState } from "react"; +import { AuthProvider, useAuth } from "./contexts/AuthContext.tsx"; +import { Sidebar } from "./components/layout/Sidebar.tsx"; +import { Header } from "./components/layout/Header.tsx"; +import { DashboardPage } from "./pages/DashboardPage.tsx"; +import { UsersPage } from "./pages/UsersPage.tsx"; +import { WorkAllocationPage } from "./pages/WorkAllocationPage.tsx"; +import { AttendancePage } from "./pages/AttendancePage.tsx"; +import { RatesPage } from "./pages/RatesPage.tsx"; +import { EmployeeSwapPage } from "./pages/EmployeeSwapPage.tsx"; +import { LoginPage } from "./pages/LoginPage.tsx"; +import { ReportingPage } from "./pages/ReportingPage.tsx"; +import { StandardRatesPage } from "./pages/StandardRatesPage.tsx"; +import { AllRatesPage } from "./pages/AllRatesPage.tsx"; +import { ActivitiesPage } from "./pages/ActivitiesPage.tsx"; -type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates' | 'activities'; +type PageType = + | "dashboard" + | "users" + | "allocation" + | "attendance" + | "rates" + | "swaps" + | "reports" + | "standard-rates" + | "all-rates" + | "activities"; const AppContent: React.FC = () => { - const [activePage, setActivePage] = useState('dashboard'); + const [activePage, setActivePage] = useState("dashboard"); const { isAuthenticated, isLoading } = useAuth(); const renderPage = () => { switch (activePage) { - case 'dashboard': + case "dashboard": return ; - case 'users': + case "users": return ; - case 'allocation': + case "allocation": return ; - case 'attendance': + case "attendance": return ; - case 'rates': + case "rates": return ; - case 'swaps': + case "swaps": return ; - case 'reports': + case "reports": return ; - case 'standard-rates': + case "standard-rates": return ; - case 'all-rates': + case "all-rates": return ; - case 'activities': + case "activities": return ; default: return ; @@ -52,7 +62,8 @@ const AppContent: React.FC = () => { return (
-
+
+

Loading...

@@ -67,11 +78,14 @@ const AppContent: React.FC = () => { // Show main app if authenticated return (
- setActivePage(page as PageType)} /> - + setActivePage(page as PageType)} + /> +
- +
{renderPage()}
@@ -88,4 +102,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg index 6c87de9..886452f 100644 --- a/src/assets/react.svg +++ b/src/assets/react.svg @@ -1 +1,16 @@ - \ No newline at end of file + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 329e580..f66d221 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,9 +1,24 @@ -import React, { useState, useEffect } from 'react'; -import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp, Phone, CreditCard, Landmark, FileText } from 'lucide-react'; -import { useAuth } from '../../contexts/AuthContext'; -import { useDepartments } from '../../hooks/useDepartments'; -import { api } from '../../services/api'; -import type { User as UserType } from '../../types'; +import React, { useEffect, useState } from "react"; +import { + Bell, + Building2, + Camera, + ChevronDown, + ChevronUp, + CreditCard, + FileText, + Landmark, + LogOut, + Mail, + Phone, + Shield, + User, + X, +} from "lucide-react"; +import { useAuth } from "../../contexts/AuthContext.tsx"; +import { useDepartments } from "../../hooks/useDepartments.ts"; +import { api } from "../../services/api.ts"; +import type { User as UserType } from "../../types.ts"; interface ProfilePopupProps { isOpen: boolean; @@ -12,47 +27,52 @@ interface ProfilePopupProps { } // Permission definitions for each role -const rolePermissions: Record = { +const rolePermissions: Record< + string, + { title: string; permissions: string[] } +> = { Supervisor: { - title: 'Supervisor Permissions', + title: "Supervisor Permissions", permissions: [ - 'View and manage employees in your department', - 'Create and manage work allocations', - 'Set contractor rates for your department', - 'View attendance records', - 'Manage check-in/check-out for employees', - ] + "View and manage employees in your department", + "Create and manage work allocations", + "Set contractor rates for your department", + "View attendance records", + "Manage check-in/check-out for employees", + ], }, Employee: { - title: 'Employee Permissions', + title: "Employee Permissions", permissions: [ - 'View your work allocations', - 'View your attendance records', - 'Check-in and check-out', - 'View assigned tasks', - ] + "View your work allocations", + "View your attendance records", + "Check-in and check-out", + "View assigned tasks", + ], }, Contractor: { - title: 'Contractor Permissions', + title: "Contractor Permissions", permissions: [ - 'View assigned work allocations', - 'View your rate configurations', - 'Track work completion status', - ] + "View assigned work allocations", + "View your rate configurations", + "Track work completion status", + ], }, SuperAdmin: { - title: 'Super Admin Permissions', + title: "Super Admin Permissions", permissions: [ - 'Full system access', - 'Manage all users and departments', - 'Configure all contractor rates', - 'View all work allocations and reports', - 'System configuration and settings', - ] - } + "Full system access", + "Manage all users and departments", + "Configure all contractor rates", + "View all work allocations and reports", + "System configuration and settings", + ], + }, }; -const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }) => { +const ProfilePopup: React.FC = ( + { isOpen, onClose, onLogout }, +) => { const { user } = useAuth(); const { departments } = useDepartments(); const [showPermissions, setShowPermissions] = useState(false); @@ -68,10 +88,11 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout } if (!isOpen) return null; - const userDepartment = departments.find(d => d.id === user?.department_id); - const userPermissions = rolePermissions[user?.role || 'Employee']; - const isEmployeeOrContractor = user?.role === 'Employee' || user?.role === 'Contractor'; - const isContractor = user?.role === 'Contractor'; + const userDepartment = departments.find((d) => d.id === user?.department_id); + const userPermissions = rolePermissions[user?.role || "Employee"]; + const isEmployeeOrContractor = user?.role === "Employee" || + user?.role === "Contractor"; + const isContractor = user?.role === "Contractor"; return (
@@ -79,22 +100,27 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
-
- {user?.name?.charAt(0).toUpperCase() || 'U'} + {user?.name?.charAt(0).toUpperCase() || "U"}
-

Hi, {user?.name || 'User'}!

+

+ Hi, {user?.name || "User"}! +

- {user?.role || 'User'} + {user?.role || "User"}
@@ -107,7 +133,9 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }

Username

-

{user?.username || 'N/A'}

+

+ {user?.username || "N/A"} +

@@ -117,18 +145,22 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }

Email

-

{user?.email || 'No email'}

+

+ {user?.email || "No email"} +

- {user?.role !== 'SuperAdmin' && userDepartment && ( + {user?.role !== "SuperAdmin" && userDepartment && (

Department

-

{userDepartment.name}

+

+ {userDepartment.name} +

)} @@ -144,11 +176,17 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
-

Personal & Bank Details

-

View your information

+

+ Personal & Bank Details +

+

+ View your information +

- {showDetails ? : } + {showDetails + ? + : } )} @@ -162,14 +200,16 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
Phone Number - {fullUserData.phone_number || 'Not provided'} + + {fullUserData.phone_number || "Not provided"} +
Aadhar Number - {fullUserData.aadhar_number - ? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}` - : 'Not provided'} + {fullUserData.aadhar_number + ? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}` + : "Not provided"}
@@ -183,19 +223,23 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
Bank Name - {fullUserData.bank_name || 'Not provided'} + + {fullUserData.bank_name || "Not provided"} +
Account Number - {fullUserData.bank_account_number - ? `XXXX${fullUserData.bank_account_number.slice(-4)}` - : 'Not provided'} + {fullUserData.bank_account_number + ? `XXXX${fullUserData.bank_account_number.slice(-4)}` + : "Not provided"}
IFSC Code - {fullUserData.bank_ifsc || 'Not provided'} + + {fullUserData.bank_ifsc || "Not provided"} +
@@ -209,15 +253,22 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
Agreement No. - {fullUserData.contractor_agreement_number || 'Not provided'} + + {fullUserData.contractor_agreement_number || + "Not provided"} +
PF Number - {fullUserData.pf_number || 'Not provided'} + + {fullUserData.pf_number || "Not provided"} +
ESIC Number - {fullUserData.esic_number || 'Not provided'} + + {fullUserData.esic_number || "Not provided"} +
@@ -235,19 +286,30 @@ const ProfilePopup: React.FC = ({ isOpen, onClose, onLogout }
-

Your Permissions

-

View what you can do

+

+ Your Permissions +

+

+ View what you can do +

- {showPermissions ? : } + {showPermissions + ? + : } {showPermissions && userPermissions && (
-

{userPermissions.title}

+

+ {userPermissions.title} +

    {userPermissions.permissions.map((perm, idx) => ( -
  • +
  • {perm}
  • @@ -284,18 +346,21 @@ export const Header: React.FC = () => {
    -

    Work Allocation System

    +

    + Work Allocation System +

    diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 6097d84..6582f84 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,6 +1,18 @@ -import React from 'react'; -import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye, Layers } from 'lucide-react'; -import { useAuth } from '../../contexts/AuthContext'; +import React from "react"; +import { + ArrowRightLeft, + Briefcase, + CalendarCheck, + ClipboardList, + DollarSign, + Eye, + FileSpreadsheet, + Layers, + LayoutDashboard, + Scale, + Users, +} from "lucide-react"; +import { useAuth } from "../../contexts/AuthContext.tsx"; interface SidebarItemProps { icon: React.ElementType; @@ -9,13 +21,15 @@ interface SidebarItemProps { onClick: () => void; } -const SidebarItem: React.FC = ({ icon: Icon, label, active, onClick }) => ( +const SidebarItem: React.FC = ( + { icon: Icon, label, active, onClick }, +) => (
-

Work Allocation

+

+ Work Allocation +

Management System

@@ -59,111 +75,120 @@ export const Sidebar: React.FC = ({ activePage, onNavigate }) => { onNavigate('dashboard')} + active={activePage === "dashboard"} + onClick={() => onNavigate("dashboard")} /> - + {/* User Management - SuperAdmin and Supervisor only */} {canManageUsers && ( onNavigate('users')} + active={activePage === "users"} + onClick={() => onNavigate("users")} /> )} - + {/* Work Allocation - SuperAdmin and Supervisor only */} {canManageAllocations && ( onNavigate('allocation')} + active={activePage === "allocation"} + onClick={() => onNavigate("allocation")} /> )} - + {/* Attendance - SuperAdmin and Supervisor only */} {canManageAttendance && ( onNavigate('attendance')} + active={activePage === "attendance"} + onClick={() => onNavigate("attendance")} /> )} - + {/* Contractor Rates - SuperAdmin and Supervisor only */} {canManageRates && ( onNavigate('rates')} + active={activePage === "rates"} + onClick={() => onNavigate("rates")} /> )} - + {/* Employee Swap - SuperAdmin only */} {isSuperAdmin && ( onNavigate('swaps')} + active={activePage === "swaps"} + onClick={() => onNavigate("swaps")} /> )} - + {/* Reports - SuperAdmin and Supervisor */} {canManageRates && ( onNavigate('reports')} + active={activePage === "reports"} + onClick={() => onNavigate("reports")} /> )} - + {/* Standard Rates - SuperAdmin and Supervisor */} {canManageRates && ( onNavigate('standard-rates')} + active={activePage === "standard-rates"} + onClick={() => onNavigate("standard-rates")} /> )} - + {/* All Rates View - SuperAdmin only */} {isSuperAdmin && ( onNavigate('all-rates')} + active={activePage === "all-rates"} + onClick={() => onNavigate("all-rates")} /> )} - + {/* Activities Management - SuperAdmin and Supervisor */} {(isSuperAdmin || isSupervisor) && ( onNavigate('activities')} + active={activePage === "activities"} + onClick={() => onNavigate("activities")} /> )} - + {/* Role indicator at bottom */}
-
Logged in as
-
- {user?.role || 'Unknown'} +
+ Logged in as +
+
+ {user?.role || "Unknown"}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 824b1c3..be6544e 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,40 +1,45 @@ -import React, { ReactNode, ButtonHTMLAttributes } from 'react'; +import React, { ButtonHTMLAttributes, ReactNode } from "react"; interface ButtonProps extends ButtonHTMLAttributes { children: ReactNode; - variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; - size?: 'sm' | 'md' | 'lg'; + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; fullWidth?: boolean; } export const Button: React.FC = ({ children, - variant = 'primary', - size = 'md', + variant = "primary", + size = "md", fullWidth = false, - className = '', + className = "", ...props }) => { - const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'; - + const baseStyles = + "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; + const variantStyles = { - primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', - secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500', - danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', - ghost: 'bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500', + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: + "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: + "bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500", }; const sizeStyles = { - sm: 'px-3 py-1.5 text-sm', - md: 'px-4 py-2 text-sm', - lg: 'px-6 py-3 text-base', + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", }; - const widthStyle = fullWidth ? 'w-full' : ''; + const widthStyle = fullWidth ? "w-full" : ""; return (