From 4608494a3fffbb58ef4799ba8a00a2c790bf6919 Mon Sep 17 00:00:00 2001 From: BOHA Date: Mon, 23 Mar 2026 08:46:51 +0100 Subject: [PATCH] initial commit Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.local.json | 12 + .env.example | 28 + .gitignore | 6 + .../plans/2026-03-23-production-readiness.md | 1172 +++++ .../2026-03-23-production-readiness-design.md | 196 + index.html | 19 + package-lock.json | 3753 +++++++++++++++++ package.json | 61 + prisma/schema.prisma | 612 +++ public/.gitkeep | 0 public/images/logo-dark.png | Bin 0 -> 16982 bytes public/images/logo-light.png | Bin 0 -> 14621 bytes src/App.tsx | 27 + src/admin/AdminApp.tsx | 96 + src/admin/admin.css | 2860 +++++++++++++ src/admin/attendance.css | 434 ++ src/admin/components/AdminDatePicker.tsx | 185 + src/admin/components/AdminLayout.tsx | 107 + src/admin/components/AlertContainer.tsx | 67 + src/admin/components/AttendanceShiftTable.tsx | 181 + src/admin/components/BulkAttendanceModal.tsx | 192 + src/admin/components/ConfirmModal.tsx | 52 + src/admin/components/ErrorBoundary.tsx | 29 + src/admin/components/Forbidden.tsx | 11 + src/admin/components/FormField.tsx | 22 + src/admin/components/Pagination.tsx | 62 + src/admin/components/RichEditor.tsx | 105 + src/admin/components/ShiftFormModal.tsx | 521 +++ src/admin/components/ShortcutsHelp.tsx | 3 + src/admin/components/Sidebar.tsx | 419 ++ src/admin/components/SortIcon.tsx | 20 + .../components/dashboard/DashActivityFeed.tsx | 80 + .../dashboard/DashAttendanceToday.tsx | 50 + .../components/dashboard/DashKpiCards.tsx | 130 + .../components/dashboard/DashProfile.tsx | 344 ++ .../components/dashboard/DashQuickActions.tsx | 378 ++ .../components/dashboard/DashSessions.tsx | 218 + src/admin/context/AlertContext.tsx | 71 + src/admin/context/AuthContext.tsx | 306 ++ src/admin/dashboard.css | 544 +++ src/admin/hooks/useApiCall.ts | 45 + src/admin/hooks/useAttendanceAdmin.ts | 766 ++++ src/admin/hooks/useDebounce.ts | 14 + src/admin/hooks/useListData.ts | 91 + src/admin/hooks/useModalLock.ts | 14 + src/admin/hooks/useTableSort.ts | 19 + src/admin/invoices.css | 141 + src/admin/login.css | 143 + src/admin/offers.css | 775 ++++ src/admin/pages/Attendance.tsx | 929 ++++ src/admin/pages/AttendanceAdmin.tsx | 341 ++ src/admin/pages/AttendanceBalances.tsx | 728 ++++ src/admin/pages/AttendanceCreate.tsx | 324 ++ src/admin/pages/AttendanceHistory.tsx | 586 +++ src/admin/pages/AttendanceLocation.tsx | 335 ++ src/admin/pages/AuditLog.tsx | 437 ++ src/admin/pages/CompanySettings.tsx | 788 ++++ src/admin/pages/Dashboard.tsx | 378 ++ src/admin/pages/InvoiceCreate.tsx | 597 +++ src/admin/pages/InvoiceDetail.tsx | 598 +++ src/admin/pages/Invoices.tsx | 651 +++ src/admin/pages/LeaveApproval.tsx | 503 +++ src/admin/pages/LeaveRequests.tsx | 258 ++ src/admin/pages/Login.tsx | 321 ++ src/admin/pages/NotFound.tsx | 30 + src/admin/pages/OfferDetail.tsx | 1140 +++++ src/admin/pages/Offers.tsx | 656 +++ src/admin/pages/OffersCustomers.tsx | 664 +++ src/admin/pages/OffersTemplates.tsx | 627 +++ src/admin/pages/OrderDetail.tsx | 671 +++ src/admin/pages/Orders.tsx | 290 ++ src/admin/pages/ProjectCreate.tsx | 318 ++ src/admin/pages/ProjectDetail.tsx | 645 +++ src/admin/pages/Projects.tsx | 287 ++ src/admin/pages/ReceivedInvoices.tsx | 986 +++++ src/admin/pages/Settings.tsx | 643 +++ src/admin/pages/Trips.tsx | 653 +++ src/admin/pages/TripsAdmin.tsx | 831 ++++ src/admin/pages/TripsHistory.tsx | 273 ++ src/admin/pages/Users.tsx | 495 +++ src/admin/pages/Vehicles.tsx | 470 +++ src/admin/settings.css | 64 + src/admin/utils/api.ts | 102 + src/admin/utils/attendanceHelpers.ts | 151 + src/admin/utils/dashboardHelpers.ts | 79 + src/admin/utils/formatters.ts | 26 + src/config/database.ts | 7 + src/config/env.ts | 51 + src/context/ThemeContext.tsx | 40 + src/main.tsx | 15 + src/middleware/auth.ts | 51 + src/middleware/security.ts | 15 + src/routes/admin/attendance.ts | 1146 +++++ src/routes/admin/audit-log.ts | 53 + src/routes/admin/auth.ts | 188 + src/routes/admin/bank-accounts.ts | 68 + src/routes/admin/company-settings.ts | 179 + src/routes/admin/customers.ts | 141 + src/routes/admin/dashboard.ts | 252 ++ src/routes/admin/invoices-pdf.ts | 266 ++ src/routes/admin/invoices.ts | 373 ++ src/routes/admin/leave-requests.ts | 238 ++ src/routes/admin/offers-pdf.ts | 721 ++++ src/routes/admin/orders.ts | 526 +++ src/routes/admin/profile.ts | 53 + src/routes/admin/projects.ts | 166 + src/routes/admin/quotations.ts | 326 ++ src/routes/admin/received-invoices.ts | 284 ++ src/routes/admin/roles.ts | 130 + src/routes/admin/scope-templates.ts | 148 + src/routes/admin/sessions.ts | 106 + src/routes/admin/totp.ts | 237 ++ src/routes/admin/trips.ts | 222 + src/routes/admin/users.ts | 226 + src/routes/admin/vehicles.ts | 83 + src/server.ts | 142 + src/services/audit.ts | 34 + src/services/auth.ts | 232 + src/types/fastify.d.ts | 7 + src/types/index.ts | 129 + src/utils/encryption.ts | 35 + src/utils/pagination.ts | 28 + src/utils/response.ts | 30 + src/utils/sequence.ts | 46 + src/utils/totp.ts | 20 + src/vite-env.d.ts | 1 + tsconfig.app.json | 31 + tsconfig.json | 7 + tsconfig.server.json | 23 + vite.config.ts | 29 + 130 files changed, 40361 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 docs/superpowers/plans/2026-03-23-production-readiness.md create mode 100644 docs/superpowers/specs/2026-03-23-production-readiness-design.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/schema.prisma create mode 100644 public/.gitkeep create mode 100644 public/images/logo-dark.png create mode 100644 public/images/logo-light.png create mode 100644 src/App.tsx create mode 100644 src/admin/AdminApp.tsx create mode 100644 src/admin/admin.css create mode 100644 src/admin/attendance.css create mode 100644 src/admin/components/AdminDatePicker.tsx create mode 100644 src/admin/components/AdminLayout.tsx create mode 100644 src/admin/components/AlertContainer.tsx create mode 100644 src/admin/components/AttendanceShiftTable.tsx create mode 100644 src/admin/components/BulkAttendanceModal.tsx create mode 100644 src/admin/components/ConfirmModal.tsx create mode 100644 src/admin/components/ErrorBoundary.tsx create mode 100644 src/admin/components/Forbidden.tsx create mode 100644 src/admin/components/FormField.tsx create mode 100644 src/admin/components/Pagination.tsx create mode 100644 src/admin/components/RichEditor.tsx create mode 100644 src/admin/components/ShiftFormModal.tsx create mode 100644 src/admin/components/ShortcutsHelp.tsx create mode 100644 src/admin/components/Sidebar.tsx create mode 100644 src/admin/components/SortIcon.tsx create mode 100644 src/admin/components/dashboard/DashActivityFeed.tsx create mode 100644 src/admin/components/dashboard/DashAttendanceToday.tsx create mode 100644 src/admin/components/dashboard/DashKpiCards.tsx create mode 100644 src/admin/components/dashboard/DashProfile.tsx create mode 100644 src/admin/components/dashboard/DashQuickActions.tsx create mode 100644 src/admin/components/dashboard/DashSessions.tsx create mode 100644 src/admin/context/AlertContext.tsx create mode 100644 src/admin/context/AuthContext.tsx create mode 100644 src/admin/dashboard.css create mode 100644 src/admin/hooks/useApiCall.ts create mode 100644 src/admin/hooks/useAttendanceAdmin.ts create mode 100644 src/admin/hooks/useDebounce.ts create mode 100644 src/admin/hooks/useListData.ts create mode 100644 src/admin/hooks/useModalLock.ts create mode 100644 src/admin/hooks/useTableSort.ts create mode 100644 src/admin/invoices.css create mode 100644 src/admin/login.css create mode 100644 src/admin/offers.css create mode 100644 src/admin/pages/Attendance.tsx create mode 100644 src/admin/pages/AttendanceAdmin.tsx create mode 100644 src/admin/pages/AttendanceBalances.tsx create mode 100644 src/admin/pages/AttendanceCreate.tsx create mode 100644 src/admin/pages/AttendanceHistory.tsx create mode 100644 src/admin/pages/AttendanceLocation.tsx create mode 100644 src/admin/pages/AuditLog.tsx create mode 100644 src/admin/pages/CompanySettings.tsx create mode 100644 src/admin/pages/Dashboard.tsx create mode 100644 src/admin/pages/InvoiceCreate.tsx create mode 100644 src/admin/pages/InvoiceDetail.tsx create mode 100644 src/admin/pages/Invoices.tsx create mode 100644 src/admin/pages/LeaveApproval.tsx create mode 100644 src/admin/pages/LeaveRequests.tsx create mode 100644 src/admin/pages/Login.tsx create mode 100644 src/admin/pages/NotFound.tsx create mode 100644 src/admin/pages/OfferDetail.tsx create mode 100644 src/admin/pages/Offers.tsx create mode 100644 src/admin/pages/OffersCustomers.tsx create mode 100644 src/admin/pages/OffersTemplates.tsx create mode 100644 src/admin/pages/OrderDetail.tsx create mode 100644 src/admin/pages/Orders.tsx create mode 100644 src/admin/pages/ProjectCreate.tsx create mode 100644 src/admin/pages/ProjectDetail.tsx create mode 100644 src/admin/pages/Projects.tsx create mode 100644 src/admin/pages/ReceivedInvoices.tsx create mode 100644 src/admin/pages/Settings.tsx create mode 100644 src/admin/pages/Trips.tsx create mode 100644 src/admin/pages/TripsAdmin.tsx create mode 100644 src/admin/pages/TripsHistory.tsx create mode 100644 src/admin/pages/Users.tsx create mode 100644 src/admin/pages/Vehicles.tsx create mode 100644 src/admin/settings.css create mode 100644 src/admin/utils/api.ts create mode 100644 src/admin/utils/attendanceHelpers.ts create mode 100644 src/admin/utils/dashboardHelpers.ts create mode 100644 src/admin/utils/formatters.ts create mode 100644 src/config/database.ts create mode 100644 src/config/env.ts create mode 100644 src/context/ThemeContext.tsx create mode 100644 src/main.tsx create mode 100644 src/middleware/auth.ts create mode 100644 src/middleware/security.ts create mode 100644 src/routes/admin/attendance.ts create mode 100644 src/routes/admin/audit-log.ts create mode 100644 src/routes/admin/auth.ts create mode 100644 src/routes/admin/bank-accounts.ts create mode 100644 src/routes/admin/company-settings.ts create mode 100644 src/routes/admin/customers.ts create mode 100644 src/routes/admin/dashboard.ts create mode 100644 src/routes/admin/invoices-pdf.ts create mode 100644 src/routes/admin/invoices.ts create mode 100644 src/routes/admin/leave-requests.ts create mode 100644 src/routes/admin/offers-pdf.ts create mode 100644 src/routes/admin/orders.ts create mode 100644 src/routes/admin/profile.ts create mode 100644 src/routes/admin/projects.ts create mode 100644 src/routes/admin/quotations.ts create mode 100644 src/routes/admin/received-invoices.ts create mode 100644 src/routes/admin/roles.ts create mode 100644 src/routes/admin/scope-templates.ts create mode 100644 src/routes/admin/sessions.ts create mode 100644 src/routes/admin/totp.ts create mode 100644 src/routes/admin/trips.ts create mode 100644 src/routes/admin/users.ts create mode 100644 src/routes/admin/vehicles.ts create mode 100644 src/server.ts create mode 100644 src/services/audit.ts create mode 100644 src/services/auth.ts create mode 100644 src/types/fastify.d.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/encryption.ts create mode 100644 src/utils/pagination.ts create mode 100644 src/utils/response.ts create mode 100644 src/utils/sequence.ts create mode 100644 src/utils/totp.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.server.json create mode 100644 vite.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..876fdd0 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(find D:cortexboha-appresources -type f \\\\\\(-name *.jsx -o -name *.js -o -name *.tsx -o -name *.ts \\\\\\))", + "Bash(find D:cortexboha-appsrc -type f -name *.jsx -o -type f -name *.tsx)", + "Bash(npx tsc:*)", + "Bash(curl -s http://127.0.0.1:3000/api/health)", + "Bash(find /d/cortex/boha-app -type f -name *.php)", + "Bash(find D:/cortex/boha-app/app/Http/Controllers/Admin -type f -name *.php)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f8e0988 --- /dev/null +++ b/.env.example @@ -0,0 +1,28 @@ +# Database +DATABASE_URL=mysql://user:password@localhost:3306/app + +# Server +PORT=3001 +HOST=127.0.0.1 +APP_ENV=local + +# Auth — MUST regenerate for production: openssl rand -hex 32 +JWT_SECRET=generate-with-openssl-rand-hex-32 +ACCESS_TOKEN_EXPIRY=900 +REFRESH_TOKEN_SESSION_EXPIRY=3600 +REFRESH_TOKEN_REMEMBER_EXPIRY=2592000 + +# TOTP — MUST regenerate for production: openssl rand -hex 32 +TOTP_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32 + +# File storage +NAS_PATH=Z:/02_PROJEKTY +MAX_UPLOAD_SIZE=52428800 + +# Email +CONTACT_EMAIL_TO= +CONTACT_EMAIL_FROM= +SMTP_FROM= + +# CORS (production only, comma-separated) +CORS_ORIGINS= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41326f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env +*.log +dist-client/ +*.css.map diff --git a/docs/superpowers/plans/2026-03-23-production-readiness.md b/docs/superpowers/plans/2026-03-23-production-readiness.md new file mode 100644 index 0000000..199f232 --- /dev/null +++ b/docs/superpowers/plans/2026-03-23-production-readiness.md @@ -0,0 +1,1172 @@ +# Production Readiness Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Harden boha-app-ts for production: security fixes, Zod validation, service layer extraction, test suite, and build preparation. + +**Architecture:** Fastify backend with Prisma ORM (MySQL), React SPA frontend, JWT + TOTP auth. Routes are refactored from fat controllers to thin handlers calling service functions. All request bodies validated with Zod schemas. Tests target the HTTP API layer via Supertest. + +**Tech Stack:** TypeScript, Fastify 5, Prisma 6, Zod, Vitest, Supertest, bcryptjs, jsonwebtoken, otpauth + +**Spec:** `docs/superpowers/specs/2026-03-23-production-readiness-design.md` + +--- + +## Phase 1: Security Hardening + +### Task 1: Secrets & Environment + +**Files:** +- Verify: `.gitignore` +- Create: `.env.example` + +- [ ] **Step 1: Verify .env is gitignored and never committed** + +```bash +grep "^\.env$" .gitignore +git log --all --diff-filter=A -- .env +``` + +Expected: `.env` appears in `.gitignore`. Git log shows no results (never committed). +If `.env` was committed, run `git rm --cached .env` and add to `.gitignore`. + +- [ ] **Step 2: Create .env.example** + +Create `.env.example` with placeholder values: + +```env +# Database +DATABASE_URL=mysql://user:password@localhost:3306/app + +# Server +PORT=3001 +HOST=127.0.0.1 +APP_ENV=local + +# Auth — MUST regenerate for production: openssl rand -hex 32 +JWT_SECRET=generate-with-openssl-rand-hex-32 +ACCESS_TOKEN_EXPIRY=900 +REFRESH_TOKEN_SESSION_EXPIRY=3600 +REFRESH_TOKEN_REMEMBER_EXPIRY=2592000 + +# TOTP — MUST regenerate for production: openssl rand -hex 32 +TOTP_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32 + +# File storage +NAS_PATH=Z:/02_PROJEKTY +MAX_UPLOAD_SIZE=52428800 + +# Email +CONTACT_EMAIL_TO= +CONTACT_EMAIL_FROM= +SMTP_FROM= + +# CORS (production only, comma-separated) +CORS_ORIGINS= +``` + +- [ ] **Step 3: Commit** + +```bash +git add .env.example .gitignore +git commit -m "chore: add .env.example with placeholder values" +``` + +--- + +### Task 2: Rate Limiting — Login Endpoint + +**Files:** +- Modify: `src/server.ts` +- Modify: `src/routes/admin/auth.ts` + +- [ ] **Step 1: Add per-route rate limit to login endpoint** + +In `src/routes/admin/auth.ts`, add rate limit config to the login route: + +```typescript +fastify.post('/login', { + config: { + rateLimit: { + max: 20, + timeWindow: '1 minute', + }, + }, +}, async (request, reply) => { + // ... existing login handler +}); +``` + +- [ ] **Step 2: Verify global rate limit stays at 100/min** + +Confirm `src/server.ts` line 53-56 remains: +```typescript +await app.register(rateLimit, { + max: 100, + timeWindow: '1 minute', +}); +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/routes/admin/auth.ts +git commit -m "security: add stricter rate limit on login endpoint (20/min)" +``` + +--- + +### Task 3: Security Headers — CSP & Permissions-Policy + +**Files:** +- Modify: `src/middleware/security.ts` + +- [ ] **Step 1: Add CSP (production only) and Permissions-Policy** + +Replace `src/middleware/security.ts` with: + +```typescript +import { FastifyReply, FastifyRequest } from 'fastify'; +import { config } from '../config/env'; + +export async function securityHeaders( + _request: FastifyRequest, + reply: FastifyReply, +): Promise { + reply.header('X-Content-Type-Options', 'nosniff'); + reply.header('X-Frame-Options', 'DENY'); + reply.header('Referrer-Policy', 'strict-origin-when-cross-origin'); + reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + + if (config.isProduction) { + reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + reply.header( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'", + ); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/middleware/security.ts +git commit -m "security: add CSP (prod) and Permissions-Policy headers" +``` + +--- + +### Task 4: Request Body Limits + +**Files:** +- Modify: `src/server.ts` + +- [ ] **Step 1: Add global body limit to Fastify** + +In `src/server.ts`, add `bodyLimit` to the Fastify constructor: + +```typescript +const app = Fastify({ + logger: { + level: config.isProduction ? 'warn' : 'info', + }, + trustProxy: true, + bodyLimit: 1048576, // 1MB global limit +}); +``` + +- [ ] **Step 2: Add 10KB limit to auth routes** + +In `src/routes/admin/auth.ts`, add bodyLimit to login, refresh, and TOTP routes: + +```typescript +fastify.post('/login', { + bodyLimit: 10240, // 10KB + config: { + rateLimit: { max: 20, timeWindow: '1 minute' }, + }, +}, async (request, reply) => { /* ... */ }); + +fastify.post('/refresh', { bodyLimit: 10240 }, async (request, reply) => { /* ... */ }); +``` + +Also in `src/routes/admin/totp.ts`, add to verify endpoints: + +```typescript +fastify.post('/verify', { bodyLimit: 10240 }, async (request, reply) => { /* ... */ }); +fastify.post('/login/totp', { bodyLimit: 10240 }, async (request, reply) => { /* ... */ }); +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/server.ts src/routes/admin/auth.ts src/routes/admin/totp.ts +git commit -m "security: add request body size limits (1MB global, 10KB auth)" +``` + +--- + +### Task 5: Timing-Safe Auth + +**Files:** +- Modify: `src/services/auth.ts` + +- [ ] **Step 1: Add dummy hash constant and timing-safe comparison** + +At the top of `src/services/auth.ts`, add a pre-computed dummy hash: + +```typescript +// Pre-computed bcrypt hash for timing-safe comparison when user not found +const DUMMY_HASH = '$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO'; +``` + +Then in the `login()` function, replace the early return on user-not-found: + +```typescript +if (!user) { + // Timing-safe: run bcrypt even when user not found + await bcrypt.compare(password, DUMMY_HASH); + return { type: 'error', message: 'Neplatné přihlašovací údaje', status: 401 }; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/services/auth.ts +git commit -m "security: timing-safe auth to prevent username enumeration" +``` + +--- + +## Phase 2: Input Validation (Zod) + +### Task 6: Install Zod, Create Common Helper & Auth Schema + +**Files:** +- Create: `src/schemas/common.ts` +- Create: `src/schemas/auth.schema.ts` +- Modify: `src/routes/admin/auth.ts` + +- [ ] **Step 1: Install zod** + +```bash +npm install zod +``` + +- [ ] **Step 2: Create common validation helper** + +Create `src/schemas/common.ts`: + +```typescript +import { ZodSchema, ZodError } from 'zod'; + +export function parseBody(schema: ZodSchema, body: unknown): { data: T } | { error: string } { + try { + return { data: schema.parse(body) }; + } catch (e) { + if (e instanceof ZodError) { + return { error: e.errors.map(err => err.message).join(', ') }; + } + return { error: 'Neplatný požadavek' }; + } +} +``` + +- [ ] **Step 3: Create auth validation schemas** + +Create `src/schemas/auth.schema.ts`: + +```typescript +import { z } from 'zod'; + +export const LoginSchema = z.object({ + username: z.string().min(1, 'Uživatelské jméno je povinné'), + password: z.string().min(1, 'Heslo je povinné'), + remember_me: z.boolean().optional().default(false), +}); + +export const TotpVerifySchema = z.object({ + login_token: z.string().min(1, 'Token je povinný'), + totp_code: z.string().length(6, 'Kód musí mít 6 číslic'), +}); + +export const TotpBackupSchema = z.object({ + login_token: z.string().min(1, 'Token je povinný'), + backup_code: z.string().min(1, 'Záložní kód je povinný'), +}); + +export type LoginInput = z.infer; +export type TotpVerifyInput = z.infer; +export type TotpBackupInput = z.infer; +``` + +- [ ] **Step 4: Apply schemas in auth routes** + +In `src/routes/admin/auth.ts`, import and use: + +```typescript +import { parseBody } from '../../schemas/common'; +import { LoginSchema, TotpVerifySchema } from '../../schemas/auth.schema'; +``` + +Replace existing manual validation in the login handler with: + +```typescript +const parsed = parseBody(LoginSchema, request.body); +if ('error' in parsed) return error(reply, parsed.error, 400); +const { username, password, remember_me } = parsed.data; +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/schemas/common.ts src/schemas/auth.schema.ts src/routes/admin/auth.ts package.json package-lock.json +git commit -m "feat: add Zod validation for auth endpoints" +``` + +--- + +### Task 7: Create Validation Schemas for All Domains + +**Files:** +- Create: `src/schemas/users.schema.ts` +- Create: `src/schemas/offers.schema.ts` +- Create: `src/schemas/orders.schema.ts` +- Create: `src/schemas/invoices.schema.ts` +- Create: `src/schemas/projects.schema.ts` +- Create: `src/schemas/customers.schema.ts` +- Create: `src/schemas/attendance.schema.ts` +- Create: `src/schemas/trips.schema.ts` +- Create: `src/schemas/vehicles.schema.ts` +- Create: `src/schemas/leave-requests.schema.ts` +- Create: `src/schemas/roles.schema.ts` +- Create: `src/schemas/bank-accounts.schema.ts` +- Create: `src/schemas/company-settings.schema.ts` +- Create: `src/schemas/received-invoices.schema.ts` +- Create: `src/schemas/scope-templates.schema.ts` +- Create: `src/schemas/profile.schema.ts` + +- [ ] **Step 1: Create domain schemas** + +Create each schema file based on the existing `request.body as Record` patterns in each route file. Example for `src/schemas/users.schema.ts`: + +```typescript +import { z } from 'zod'; + +export const CreateUserSchema = z.object({ + username: z.string().min(1, 'Uživatelské jméno je povinné'), + email: z.string().email('Neplatný formát e-mailu'), + password: z.string().min(8, 'Heslo musí mít alespoň 8 znaků'), + first_name: z.string().optional().default(''), + last_name: z.string().optional().default(''), + role_id: z.number().int().positive().optional(), + is_active: z.boolean().optional().default(true), +}); + +export const UpdateUserSchema = CreateUserSchema.partial().omit({ password: true }).extend({ + password: z.string().min(8, 'Heslo musí mít alespoň 8 znaků').optional(), +}); + +export type CreateUserInput = z.infer; +export type UpdateUserInput = z.infer; +``` + +Follow the same pattern for all domains — each schema mirrors the fields currently extracted from `request.body`. Cover ALL route files that use `as Record`: users, offers, orders, invoices, projects, customers, attendance, trips, vehicles, leave-requests, roles, bank-accounts, company-settings, received-invoices, scope-templates, profile, totp. + +- [ ] **Step 2: Apply schemas in all route files** + +For each route file, replace `const body = request.body as Record` with: + +```typescript +const parsed = parseBody(CreateOfferSchema, request.body); +if ('error' in parsed) return error(reply, parsed.error, 400); +const body = parsed.data; +``` + +- [ ] **Step 3: Remove `as Record` casts** + +Search all route files for remaining `as Record` and replace with typed Zod parsing. + +```bash +grep -rn "as Record" src/routes/ +``` + +Expected: zero results after cleanup. + +- [ ] **Step 4: Commit** + +```bash +git add src/schemas/ src/routes/ +git commit -m "feat: add Zod validation schemas for all domain routes" +``` + +--- + +## Phase 3: Service Layer Refactor + +### Task 8: Create Numbering Service + +**Files:** +- Create: `src/services/numbering.service.ts` +- Modify: `src/routes/admin/orders.ts` +- Modify: `src/routes/admin/projects.ts` +- Modify: `src/routes/admin/quotations.ts` + +- [ ] **Step 1: Create numbering service** + +Create `src/services/numbering.service.ts`: + +```typescript +import prisma from '../config/database'; + +/** + * Shared number generator for orders and projects. + * Format: YYtypeCode + 4-digit sequence (e.g., 26710003) + * Queries MAX from both orders and projects tables. + */ +export async function generateSharedNumber(): Promise { + const settings = await prisma.company_settings.findFirst({ select: { order_type_code: true } }); + const typeCode = settings?.order_type_code || '71'; + const yy = String(new Date().getFullYear()).slice(-2); + const prefix = `${yy}${typeCode}`; + const prefixLen = prefix.length; + const likePattern = `${prefix}%`; + + const result = await prisma.$queryRaw<[{ max_seq: bigint | null }]>` + SELECT COALESCE(MAX(seq), 0) as max_seq FROM ( + SELECT CAST(SUBSTRING(order_number, ${prefixLen} + 1) AS UNSIGNED) AS seq + FROM orders WHERE order_number LIKE ${likePattern} + UNION ALL + SELECT CAST(SUBSTRING(project_number, ${prefixLen} + 1) AS UNSIGNED) AS seq + FROM projects WHERE project_number LIKE ${likePattern} + ) combined + `; + const nextNum = Number(result[0]?.max_seq ?? 0) + 1; + return `${prefix}${String(nextNum).padStart(4, '0')}`; +} + +/** + * Next offer number. Queries MAX from quotations table. + * Format: YEAR/PREFIX/NNN (e.g., 2026/NA/008) + */ +export async function generateOfferNumber(): Promise { + const settings = await prisma.company_settings.findFirst({ select: { quotation_prefix: true } }); + const prefix = settings?.quotation_prefix || 'NA'; + const year = new Date().getFullYear(); + const likePattern = `${year}/${prefix}/%`; + + const result = await prisma.$queryRaw<[{ max_num: bigint | null }]>` + SELECT COALESCE(MAX(CAST(SUBSTRING_INDEX(quotation_number, '/', -1) AS UNSIGNED)), 0) as max_num + FROM quotations + WHERE quotation_number LIKE ${likePattern} + `; + const nextNum = Number(result[0]?.max_num ?? 0) + 1; + return `${year}/${prefix}/${String(nextNum).padStart(3, '0')}`; +} + +/** + * Next invoice number via atomic sequence table. + */ +export async function generateInvoiceNumber(year: number): Promise { + return prisma.$transaction(async (tx) => { + const existing = await tx.number_sequences.findFirst({ + where: { type: 'invoice', year }, + }); + + if (existing) { + const nextNum = (existing.last_number ?? 0) + 1; + await tx.number_sequences.update({ + where: { id: existing.id }, + data: { last_number: nextNum }, + }); + return nextNum; + } + + await tx.number_sequences.create({ + data: { type: 'invoice', year, last_number: 1 }, + }); + return 1; + }); +} +``` + +- [ ] **Step 2: Update orders.ts to use numbering service** + +Replace `generateOrderNumber()` and `generateProjectNumber()` in `src/routes/admin/orders.ts` with imports from the service: + +```typescript +import { generateSharedNumber } from '../../services/numbering.service'; +``` + +Remove the local `generateOrderNumber()`, `generateProjectNumber()`, and `generateSharedNumber()` functions. Replace all calls: + +```typescript +const orderNumber = await generateSharedNumber(); +const projectNumber = await generateSharedNumber(); +``` + +- [ ] **Step 3: Update projects.ts and quotations.ts similarly** + +In `src/routes/admin/projects.ts`, replace the inline next-number logic with: + +```typescript +import { generateSharedNumber } from '../../services/numbering.service'; +``` + +In `src/routes/admin/quotations.ts`, replace the inline offer number logic with: + +```typescript +import { generateOfferNumber } from '../../services/numbering.service'; +``` + +- [ ] **Step 4: Update sequence.ts to re-export from numbering service** + +In `src/utils/sequence.ts`, replace the implementation with a re-export to avoid breaking any remaining callers: + +```typescript +// Re-export from canonical location +export { generateInvoiceNumber as getNextNumber } from '../services/numbering.service'; +``` + +Check for any remaining imports of `sequence.ts` and update them to import from `numbering.service.ts` directly: + +```bash +grep -rn "from.*sequence" src/ +``` + +If no callers remain, delete `src/utils/sequence.ts`. + +- [ ] **Step 5: Commit** + +```bash +git add src/services/numbering.service.ts src/routes/admin/orders.ts src/routes/admin/projects.ts src/routes/admin/quotations.ts src/utils/sequence.ts +git commit -m "refactor: extract numbering logic into numbering.service.ts" +``` + +--- + +### Task 9: Extract Offers Service + +**Files:** +- Create: `src/services/offers.service.ts` +- Modify: `src/routes/admin/quotations.ts` + +- [ ] **Step 1: Create offers service** + +Extract the business logic from each route handler in `quotations.ts` into `src/services/offers.service.ts`. The service functions should handle Prisma queries, calculations, and data enrichment. Example: + +```typescript +import prisma from '../config/database'; +import { generateOfferNumber } from './numbering.service'; + +export async function listOffers(params: { page: number; limit: number; skip: number; sort: string; order: string; search?: string; status?: string; customerId?: number }) { + const where: Record = {}; + if (params.status) where.status = params.status; + if (params.customerId) where.customer_id = params.customerId; + // ... build where clause, run query, enrich results + // Return { offers, total } +} + +export async function getOffer(id: number) { /* ... */ } +export async function createOffer(data: CreateOfferInput) { /* ... */ } +export async function updateOffer(id: number, data: UpdateOfferInput) { /* ... */ } +export async function duplicateOffer(id: number) { /* ... */ } +export async function getNextOfferNumber() { return generateOfferNumber(); } +``` + +- [ ] **Step 2: Slim down quotations.ts routes** + +Each route handler becomes: + +```typescript +fastify.get('/', { preHandler: requirePermission('offers.view') }, async (request, reply) => { + const query = request.query as Record; + const pagination = parsePagination(query); + const result = await listOffers({ ...pagination, status: query.status as string, customerId: query.customer_id ? Number(query.customer_id) : undefined }); + return reply.send({ success: true, data: result.offers, pagination: buildPaginationMeta(result.total, pagination.page, pagination.limit) }); +}); +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/services/offers.service.ts src/routes/admin/quotations.ts +git commit -m "refactor: extract offers business logic into offers.service.ts" +``` + +--- + +### Task 10a: Extract Orders Service + +**Files:** +- Create: `src/services/orders.service.ts` +- Modify: `src/routes/admin/orders.ts` + +- [ ] **Step 1:** Follow the same pattern as Task 9. Move order CRUD, status transitions, project creation, and item/section management from `src/routes/admin/orders.ts` into `src/services/orders.service.ts`. + +- [ ] **Step 2: Commit** + +```bash +git add src/services/orders.service.ts src/routes/admin/orders.ts +git commit -m "refactor: extract orders business logic into orders.service.ts" +``` + +--- + +### Task 10b: Extract Invoices Service + +**Files:** +- Create: `src/services/invoices.service.ts` +- Modify: `src/routes/admin/invoices.ts` + +- [ ] **Step 1:** Move invoice CRUD, stats calculation, overdue auto-update, and PDF data preparation from `src/routes/admin/invoices.ts` into `src/services/invoices.service.ts`. + +- [ ] **Step 2: Commit** + +```bash +git add src/services/invoices.service.ts src/routes/admin/invoices.ts +git commit -m "refactor: extract invoices business logic into invoices.service.ts" +``` + +--- + +### Task 10c: Extract Projects Service + +**Files:** +- Create: `src/services/projects.service.ts` +- Modify: `src/routes/admin/projects.ts` + +- [ ] **Step 1:** Move project CRUD, notes management from `src/routes/admin/projects.ts` into `src/services/projects.service.ts`. + +- [ ] **Step 2: Commit** + +```bash +git add src/services/projects.service.ts src/routes/admin/projects.ts +git commit -m "refactor: extract projects business logic into projects.service.ts" +``` + +--- + +### Task 10d: Extract Users Service + +**Files:** +- Create: `src/services/users.service.ts` +- Modify: `src/routes/admin/users.ts` + +- [ ] **Step 1:** Move user CRUD, role assignment, password reset from `src/routes/admin/users.ts` into `src/services/users.service.ts`. + +- [ ] **Step 2: Commit** + +```bash +git add src/services/users.service.ts src/routes/admin/users.ts +git commit -m "refactor: extract users business logic into users.service.ts" +``` + +--- + +### Task 10e: Extract Attendance Service + +**Files:** +- Create: `src/services/attendance.service.ts` +- Modify: `src/routes/admin/attendance.ts` + +- [ ] **Step 1:** Move clock in/out, shift management, monthly calculations from `src/routes/admin/attendance.ts` into `src/services/attendance.service.ts`. + +- [ ] **Step 2: Commit** + +```bash +git add src/services/attendance.service.ts src/routes/admin/attendance.ts +git commit -m "refactor: extract attendance business logic into attendance.service.ts" +``` + +--- + +### Task 10f: Verify Service Extraction + +- [ ] **Step 1: Check remaining Prisma calls in routes** + +```bash +grep -rn "prisma\." src/routes/admin/ | grep -v "import" | wc -l +``` + +Goal: minimal direct Prisma calls in the 7 refactored route files (offers, orders, invoices, projects, users, attendance). Other route files (trips, vehicles, leave-requests, bank-accounts, etc.) are intentionally not refactored — they are simpler CRUD with minimal business logic. + +- [ ] **Step 2: Commit any remaining fixes** + +```bash +git add -A +git commit -m "refactor: finalize service layer extraction" +``` + +--- + +## Phase 4: Testing + +### Task 11: Testing Infrastructure + +**Files:** +- Create: `vitest.config.ts` +- Create: `.env.test` +- Create: `src/__tests__/setup.ts` +- Modify: `package.json` + +- [ ] **Step 1: Install test dependencies** + +```bash +npm install -D vitest supertest @types/supertest +``` + +- [ ] **Step 2: Create vitest config** + +Create `vitest.config.ts`: + +```typescript +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + setupFiles: ['./src/__tests__/setup.ts'], + testTimeout: 15000, + hookTimeout: 15000, + }, +}); +``` + +- [ ] **Step 3: Add .env.test to .gitignore and create .env.test.example** + +Add `.env.test` to `.gitignore`: + +``` +.env.test +``` + +Create `.env.test.example` (committed, no real credentials): + +```env +DATABASE_URL=mysql://user:password@127.0.0.1:3306/app_test +JWT_SECRET=test-jwt-secret-do-not-use-in-production +TOTP_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef +APP_ENV=local +PORT=3099 +HOST=127.0.0.1 +``` + +Then create the actual `.env.test` locally (not committed) with real credentials. + +- [ ] **Step 4: Create test setup** + +Create `src/__tests__/setup.ts`: + +```typescript +import dotenv from 'dotenv'; +dotenv.config({ path: '.env.test' }); +``` + +- [ ] **Step 5: Add test scripts to package.json** + +```json +"test": "vitest run", +"test:watch": "vitest" +``` + +- [ ] **Step 6: Commit** + +```bash +git add vitest.config.ts .env.test.example .gitignore src/__tests__/setup.ts package.json package-lock.json +git commit -m "chore: add vitest testing infrastructure" +``` + +--- + +### Task 12: Auth Flow Tests + +**Files:** +- Create: `src/__tests__/helpers.ts` +- Create: `src/__tests__/auth.test.ts` + +- [ ] **Step 1: Create test helpers** + +Create `src/__tests__/helpers.ts`: + +```typescript +import Fastify from 'fastify'; +import cookie from '@fastify/cookie'; +import rateLimit from '@fastify/rate-limit'; +import authRoutes from '../routes/admin/auth'; +import totpRoutes from '../routes/admin/totp'; + +export async function buildApp() { + const app = Fastify({ logger: false }); + await app.register(cookie); + await app.register(rateLimit, { max: 1000, timeWindow: '1 minute' }); + await app.register(authRoutes, { prefix: '/api/admin' }); + await app.register(totpRoutes, { prefix: '/api/admin/totp' }); + return app; +} + +export function extractCookie(response: any, name: string): string | undefined { + const cookies = response.headers['set-cookie']; + if (!cookies) return undefined; + const arr = Array.isArray(cookies) ? cookies : [cookies]; + for (const c of arr) { + if (c.startsWith(`${name}=`)) { + return c.split(';')[0].split('=')[1]; + } + } + return undefined; +} +``` + +- [ ] **Step 2: Write auth tests** + +Create `src/__tests__/auth.test.ts`: + +```typescript +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { buildApp, extractCookie } from './helpers'; + +let app: Awaited>; + +beforeAll(async () => { app = await buildApp(); }); +afterAll(async () => { await app.close(); }); + +describe('POST /api/admin/login', () => { + it('returns 401 for invalid credentials', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/admin/login', + payload: { username: 'nonexistent', password: 'wrong' }, + }); + expect(res.statusCode).toBe(401); + expect(res.json().success).toBe(false); + }); + + it('returns 400 for missing fields', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/admin/login', + payload: {}, + }); + expect(res.statusCode).toBe(400); + }); + + it('returns access token and sets refresh cookie on valid login', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/admin/login', + payload: { username: 'admin', password: 'test-password' }, + }); + // Depends on test DB having an admin user + if (res.statusCode === 200) { + const body = res.json(); + expect(body.success).toBe(true); + expect(body.data.access_token).toBeDefined(); + expect(extractCookie(res, 'refresh_token')).toBeDefined(); + } + }); +}); + +describe('POST /api/admin/refresh', () => { + it('returns 401 without refresh token', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/admin/refresh', + }); + expect(res.statusCode).toBe(401); + }); +}); + +describe('POST /api/admin/logout', () => { + it('clears refresh token cookie', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/admin/logout', + }); + // Should succeed even without a valid token + expect(res.statusCode).toBeLessThan(500); + }); +}); +``` + +- [ ] **Step 3: Run tests** + +```bash +npm test +``` + +Expected: tests pass (or skip gracefully if test DB not seeded). + +- [ ] **Step 4: Commit** + +```bash +git add src/__tests__/ +git commit -m "test: add auth flow integration tests" +``` + +--- + +### Task 13: Permissions & CRUD Tests + +**Files:** +- Create: `src/__tests__/permissions.test.ts` +- Create: `src/__tests__/numbering.test.ts` + +- [ ] **Step 1: Write permissions tests** + +Create `src/__tests__/permissions.test.ts` testing: +- Unauthenticated requests return 401 +- Non-admin without permission returns 403 +- Admin can access all endpoints + +- [ ] **Step 2: Write numbering tests** + +Create `src/__tests__/numbering.test.ts` testing: +- `generateSharedNumber()` returns correct format (YYtypeCode + 4 digits) +- `generateOfferNumber()` returns correct format (YEAR/PREFIX/NNN) +- Sequential calls return incrementing numbers + +- [ ] **Step 3: Run full test suite** + +```bash +npm test +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/__tests__/ +git commit -m "test: add permissions and numbering tests" +``` + +--- + +## Phase 5: Production Build Preparation + +### Task 14: Graceful Shutdown + +**Files:** +- Modify: `src/server.ts` +- Modify: `src/config/database.ts` + +- [ ] **Step 1: Add shutdown handlers** + +At the end of the `start()` function in `src/server.ts`, after `app.listen()`, add: + +```typescript +const shutdown = async (signal: string) => { + app.log.info(`${signal} received, shutting down gracefully...`); + try { + await app.close(); + const { default: prisma } = await import('./config/database'); + await prisma.$disconnect(); + app.log.info('Server shut down successfully'); + process.exit(0); + } catch (err) { + app.log.error(err, 'Error during shutdown'); + process.exit(1); + } +}; + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/server.ts +git commit -m "feat: add graceful shutdown handling (SIGTERM/SIGINT)" +``` + +--- + +### Task 15: Static File Serving (Production) + +**Files:** +- Modify: `src/server.ts` + +- [ ] **Step 1: Add @fastify/static for production** + +In `src/server.ts`, after the dev-only Vite block, add production static serving: + +```typescript +if (config.isProduction) { + const fastifyStatic = (await import('@fastify/static')).default; + await app.register(fastifyStatic, { + root: path.join(__dirname, '..', 'dist-client'), + prefix: '/', + wildcard: false, + }); + + app.setNotFoundHandler((request, reply) => { + if (request.url.startsWith('/api/')) { + return reply.status(404).send({ success: false, error: 'Not found' }); + } + return reply.sendFile('index.html'); + }); +} +``` + +Add `import path from 'path'` at the top. + +- [ ] **Step 2: Commit** + +```bash +git add src/server.ts +git commit -m "feat: serve static frontend assets in production via @fastify/static" +``` + +--- + +### Task 16: Prisma Migration Baseline + +- [ ] **Step 1: Create baseline migration** + +```bash +mkdir -p prisma/migrations/0_init +npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql +npx prisma migrate resolve --applied 0_init +``` + +This marks the existing schema as the baseline without running any SQL. + +- [ ] **Step 2: Commit** + +```bash +git add prisma/migrations/ +git commit -m "chore: create Prisma migration baseline from existing schema" +``` + +--- + +### Task 17: TOTP Re-encryption Script + +**Files:** +- Create: `scripts/rotate-totp-key.ts` + +- [ ] **Step 1: Create rotation script** + +Create `scripts/rotate-totp-key.ts`: + +```typescript +import crypto from 'crypto'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +function decrypt(ciphertext: string, keyHex: string): string { + const key = Buffer.from(keyHex, 'hex'); + const [ivHex, encHex, tagHex] = ciphertext.split(':'); + const iv = Buffer.from(ivHex, 'hex'); + const encrypted = Buffer.from(encHex, 'hex'); + const tag = Buffer.from(tagHex, 'hex'); + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(tag); + return decipher.update(encrypted, undefined, 'utf8') + decipher.final('utf8'); +} + +function encrypt(plaintext: string, keyHex: string): string { + const key = Buffer.from(keyHex, 'hex'); + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return `${iv.toString('hex')}:${encrypted.toString('hex')}:${tag.toString('hex')}`; +} + +async function main() { + const oldKey = process.argv[2]; + const newKey = process.argv[3]; + const dryRun = process.argv.includes('--dry-run'); + + if (!oldKey || !newKey) { + console.error('Usage: tsx scripts/rotate-totp-key.ts [--dry-run]'); + process.exit(1); + } + + const users = await prisma.users.findMany({ + where: { totp_enabled: true, totp_secret: { not: null } }, + select: { id: true, username: true, totp_secret: true }, + }); + + console.log(`Found ${users.length} users with TOTP enabled`); + + if (dryRun) { + for (const user of users) { + try { + const decrypted = decrypt(user.totp_secret!, oldKey); + const reEncrypted = encrypt(decrypted, newKey); + const verify = decrypt(reEncrypted, newKey); + console.log(` [OK] ${user.username} (id=${user.id}) — decryption and re-encryption verified`); + } catch (e) { + console.error(` [FAIL] ${user.username} (id=${user.id}) — ${e}`); + } + } + console.log('\nDry run complete. No changes made.'); + return; + } + + await prisma.$transaction(async (tx) => { + for (const user of users) { + const decrypted = decrypt(user.totp_secret!, oldKey); + const reEncrypted = encrypt(decrypted, newKey); + await tx.users.update({ where: { id: user.id }, data: { totp_secret: reEncrypted } }); + console.log(` Re-encrypted TOTP for ${user.username} (id=${user.id})`); + } + }); + + console.log('\nAll TOTP secrets re-encrypted successfully.'); + await prisma.$disconnect(); +} + +main().catch((e) => { console.error(e); process.exit(1); }); +``` + +- [ ] **Step 2: Test with dry run** + +```bash +npx tsx scripts/rotate-totp-key.ts --dry-run +``` + +- [ ] **Step 3: Commit** + +```bash +git add scripts/rotate-totp-key.ts +git commit -m "chore: add TOTP encryption key rotation script" +``` + +--- + +### Task 18: Final Cleanup + +- [ ] **Step 1: Check for unused dependencies** + +```bash +npx depcheck +``` + +Review output and remove any unused packages. + +- [ ] **Step 2: Verify .gitignore completeness** + +Ensure `dist/`, `dist-client/`, `.env`, `.env.test`, `node_modules/` are all ignored. + +- [ ] **Step 3: Verify production build works** + +```bash +npm run build +APP_ENV=production node dist/server.js +``` + +Expected: server starts without errors, serves static files, API responds. + +- [ ] **Step 4: Final commit** + +```bash +git add -A +git commit -m "chore: production readiness cleanup" +``` diff --git a/docs/superpowers/specs/2026-03-23-production-readiness-design.md b/docs/superpowers/specs/2026-03-23-production-readiness-design.md new file mode 100644 index 0000000..5c92957 --- /dev/null +++ b/docs/superpowers/specs/2026-03-23-production-readiness-design.md @@ -0,0 +1,196 @@ +# Production Readiness Audit — boha-app-ts + +**Date:** 2026-03-23 +**Status:** Approved +**Scope:** Code hardening, security, validation, architecture refactor, testing, build preparation + +--- + +## Context + +boha-app-ts is a TypeScript/Node.js migration of the PHP boha-app. Stack: Fastify backend, React frontend, Prisma ORM (MySQL), JWT + TOTP auth. The app will be deployed on an Ubuntu server alongside the existing PHP app, reusing the same MySQL database, nginx, and SSL setup. + +This spec covers making the codebase production-ready. Deployment configuration (nginx, PM2, Ubuntu) is out of scope — this focuses purely on code quality, security, and build preparation. + +## Implementation Order + +Sections must be executed in this order due to dependencies: + +1. **Security Hardening** — no dependencies, foundational +2. **Input Validation & TypeScript** — no dependencies on (1), can overlap +3. **Service Layer Refactor** — must complete before testing +4. **Testing** — depends on services existing (tests target service layer) +5. **Production Build Preparation** — last, especially migrations and graceful shutdown + +--- + +## 1. Security Hardening + +### 1.1 Secrets Management +- Verify `.env` is in `.gitignore` and was never committed to git history +- Create `.env.example` with placeholder values (no real secrets) +- Document that production requires new JWT_SECRET and TOTP_ENCRYPTION_KEY +- TOTP re-encryption script — see section 5.4 for implementation details + +### 1.2 Rate Limiting +- Global: 100 requests/min per IP (unchanged — sufficient for internal admin app) +- Login endpoint (`POST /api/admin/login`): 20 requests/min per IP +- All other endpoints: inherit global limit +- Account lockout (5 failed attempts = 15 min lock) remains the primary brute-force defense +- Rate limiting protects infrastructure, lockout protects accounts + +### 1.3 Security Headers +Add to existing security middleware: +- `Content-Security-Policy` — production only (dev mode needs relaxed policy for Vite HMR/eval) + - Production: `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'` + - Dev: no CSP (Vite injects scripts dynamically) +- `Permissions-Policy: camera=(), microphone=(), geolocation=()` +- Existing headers unchanged: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS (prod) + +### 1.4 Request Body Limits +- Global JSON body limit: 1MB via Fastify `bodyLimit` +- Auth endpoints (login, refresh, TOTP): 10KB +- Multipart (file uploads): 10MB (unchanged) + +### 1.5 Timing-Safe Auth +- When user not found during login, still run `bcrypt.compare()` against a dummy hash +- Prevents timing-based username enumeration + +--- + +## 2. Input Validation & TypeScript Strictness + +### 2.1 Zod Validation +- Install `zod` as dependency +- Create validation schemas for all request bodies in `src/schemas/` +- Schemas organized by domain: `auth.schema.ts`, `users.schema.ts`, `offers.schema.ts`, etc. +- Replace all `request.body as Record` with Zod parsing +- Validation errors return 400 with field-level messages in Czech + +### 2.2 TypeScript Strictness +- Verify `strict: true` is already enabled in `tsconfig.server.json` +- Eliminate remaining `as Record` casts — replaced by Zod-inferred types +- Remove any `@ts-ignore` or `any` usage +- This step is effectively part of the Zod migration (2.1) + +### 2.3 Error Response Consistency +- All user-facing error messages in Czech +- Every route uses `error()` / `success()` helpers +- Proper HTTP status codes on all responses + +--- + +## 3. Service Layer & Code Architecture + +### 3.1 Service Extraction +Move business logic from route handlers into `src/services/`: + +| Service | Responsibility | +|---------|---------------| +| `offers.service.ts` | CRUD, numbering, duplication | +| `orders.service.ts` | CRUD, numbering, project creation | +| `invoices.service.ts` | CRUD, stats, PDF data preparation | +| `projects.service.ts` | CRUD, notes, numbering | +| `users.service.ts` | CRUD, role assignment, password reset | +| `attendance.service.ts` | Clock in/out, shift management | +| `numbering.service.ts` | Shared number generation (orders + projects + offers) | + +Existing services (`auth.ts`, `audit.ts`) remain unchanged — already well structured. + +### 3.2 Route Handler Pattern +After refactor, routes follow this pattern: +``` +parse & validate input (Zod) → call service → return response +``` +No business logic in route files. + +### 3.3 Number Generation Consolidation +- Move `generateSharedNumber()` from `orders.ts` into `numbering.service.ts` +- Move offer MAX-based numbering into same service +- Used by orders, projects, offers, and their next-number endpoints +- The `number_sequences` table stays in the database — only the code location changes from `src/utils/sequence.ts` to `src/services/numbering.service.ts` + +--- + +## 4. Testing + +### 4.1 Stack +- **Vitest** — test runner (compatible with existing Vite setup) +- **Supertest** — HTTP integration testing +- Real test database (no mocks) + +### 4.2 Test Database Setup +- Separate `DATABASE_URL` via `.env.test` pointing to a dedicated test database +- Tests use transaction rollback for cleanup (each test runs in a transaction that rolls back) +- Seed script for baseline test data (admin user, roles, permissions) + +### 4.3 Test Coverage + +| Area | Tests | +|------|-------| +| Auth flow | Login, TOTP verify, backup codes, token refresh, rotation, logout, lockout | +| Permissions | Admin bypass, role-based access, forbidden responses | +| Number generation | Offer, order, project shared sequence correctness | +| CRUD | Create/read/update/delete for offers, orders, invoices | +| Edge cases | Expired tokens, invalid TOTP, duplicate usernames, password validation | + +### 4.4 Not Testing +- Frontend React components +- Prisma query internals +- Simple list/get endpoints with no business logic + +### 4.5 Structure +``` +src/__tests__/ + auth.test.ts + permissions.test.ts + offers.test.ts + orders.test.ts + invoices.test.ts + numbering.test.ts +``` + +--- + +## 5. Production Build Preparation + +### 5.1 Graceful Shutdown +- Handle SIGTERM/SIGINT in server.ts +- Close Fastify server (drain in-flight requests) +- Disconnect Prisma client +- Log shutdown events + +### 5.2 Prisma Migration Strategy +- Create baseline migration from existing schema using `prisma migrate diff` +- All future schema changes go through versioned migration files +- Never use `db push` in production + +### 5.3 Environment Template +- Create `.env.example` documenting all required variables with placeholder values +- Mark which values must be regenerated for production (secrets) +- Mark which values are deployment-specific (HOST, PORT, CORS_ORIGINS, NAS_PATH) + +### 5.4 TOTP Re-encryption Script +- Standalone script: `scripts/rotate-totp-key.ts` +- Reads old key and new key from arguments +- Decrypts all `users.totp_secret` with old key, re-encrypts with new key +- Transaction-safe (all or nothing) +- Dry-run mode for verification + +### 5.5 Static File Serving +- Production serves `dist-client/` via `@fastify/static` (already a dependency) +- Dev mode uses Vite middleware (already implemented) + +### 5.6 Cleanup +- Verify no unused dependencies in package.json +- Ensure `dist/` and `dist-client/` are in `.gitignore` + +--- + +## Out of Scope + +- Nginx configuration +- PM2 / process management setup +- Ubuntu server provisioning +- Frontend component refactoring +- Database data migration from PHP app diff --git a/index.html b/index.html new file mode 100644 index 0000000..e99868a --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + BOHA | Admin + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e008962 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3753 @@ +{ + "name": "boha-app-ts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "boha-app-ts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.2.0", + "@fastify/multipart": "^9.4.0", + "@fastify/rate-limit": "^10.3.0", + "@fastify/static": "^9.0.0", + "@prisma/client": "^6.19.2", + "bcryptjs": "^3.0.3", + "date-fns": "^4.1.0", + "dotenv": "^17.3.1", + "fastify": "^5.8.2", + "framer-motion": "^12.38.0", + "hi-base32": "^0.5.1", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.2", + "otpauth": "^9.5.0", + "prisma": "^6.19.2", + "react": "^18.3.1", + "react-datepicker": "^9.1.0", + "react-dom": "^18.3.1", + "react-quill-new": "^3.8.3", + "react-router-dom": "^6.30.3" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/mysql": "^2.15.27", + "@types/node": "^25.5.0", + "@types/nodemailer": "^7.0.11", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^8.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", + "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.1.tgz", + "integrity": "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.4.0.tgz", + "integrity": "sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^3.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "secure-json-parse": "^4.0.0" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.0.0.tgz", + "integrity": "sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^1.0.1", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.2.tgz", + "integrity": "sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", + "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz", + "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/otpauth": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.0.tgz", + "integrity": "sha512-Ldhc6UYl4baR5toGr8nfKC+L/b8/RgHKoIixAebgoNGzUUCET02g04rMEZ2ZsPfeVQhMHcuaOgb28nwMr81zCA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-quill-new": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.8.3.tgz", + "integrity": "sha512-c96PYqFTo0pI4R3e79B3rH9LUIce1kIQbmTBu/imJQZk8305ogyLyBqKKjG2UoInDlquXqePSzmBo2aVia3ttw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "quill": "~2.0.3" + }, + "peerDependencies": { + "quill-delta": "^5.1.0", + "react": "^16 || ^17 || ^18 || ^19", + "react-dom": "^16 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz", + "integrity": "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..53e2952 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "boha-app-ts", + "version": "1.0.0", + "description": "", + "main": "dist/server.js", + "scripts": { + "dev:server": "tsx watch src/server.ts", + "dev:client": "vite", + "dev": "tsx watch src/server.ts", + "build:server": "tsc -p tsconfig.server.json", + "build:client": "vite build", + "build": "npm run build:server && npm run build:client", + "start": "node dist/server.js", + "preview": "vite preview", + "db:generate": "prisma generate", + "db:pull": "prisma db pull", + "db:push": "prisma db push", + "db:studio": "prisma studio" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.2.0", + "@fastify/multipart": "^9.4.0", + "@fastify/rate-limit": "^10.3.0", + "@fastify/static": "^9.0.0", + "@prisma/client": "^6.19.2", + "bcryptjs": "^3.0.3", + "date-fns": "^4.1.0", + "dotenv": "^17.3.1", + "fastify": "^5.8.2", + "framer-motion": "^12.38.0", + "hi-base32": "^0.5.1", + "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.2", + "otpauth": "^9.5.0", + "prisma": "^6.19.2", + "react": "^18.3.1", + "react-datepicker": "^9.1.0", + "react-dom": "^18.3.1", + "react-quill-new": "^3.8.3", + "react-router-dom": "^6.30.3" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/mysql": "^2.15.27", + "@types/node": "^25.5.0", + "@types/nodemailer": "^7.0.11", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^8.0.0" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..55a33b9 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,612 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model attendance { + id Int @id @default(autoincrement()) + user_id Int + shift_date DateTime @db.Date + arrival_time DateTime? @db.DateTime(0) + arrival_lat Decimal? @db.Decimal(10, 8) + arrival_lng Decimal? @db.Decimal(11, 8) + arrival_accuracy Decimal? @db.Decimal(10, 2) + arrival_address String? @db.VarChar(500) + break_start DateTime? @db.DateTime(0) + break_end DateTime? @db.DateTime(0) + departure_time DateTime? @db.DateTime(0) + departure_lat Decimal? @db.Decimal(10, 8) + departure_lng Decimal? @db.Decimal(11, 8) + departure_accuracy Decimal? @db.Decimal(10, 2) + departure_address String? @db.VarChar(500) + notes String? @db.Text + project_id Int? + leave_type attendance_leave_type? @default(work) + leave_hours Decimal? @db.Decimal(4, 2) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "attendance_ibfk_1") + attendance_project_logs attendance_project_logs[] + + @@index([user_id, shift_date], map: "idx_attendance_user_date") + @@index([user_id, departure_time], map: "idx_attendance_user_departure") + @@index([project_id], map: "idx_project_id") +} + +model attendance_project_logs { + id Int @id @default(autoincrement()) + attendance_id Int + project_id Int + started_at DateTime? @db.DateTime(0) + ended_at DateTime? @db.DateTime(0) + hours Int? @db.UnsignedInt + minutes Int? @db.UnsignedInt + attendance attendance @relation(fields: [attendance_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@index([attendance_id], map: "idx_attendance_project_logs_aid") + @@index([project_id], map: "idx_project_id") + @@index([started_at], map: "idx_started_at") +} + +model audit_logs { + id Int @id @default(autoincrement()) + user_id Int? + username String? @db.VarChar(100) + user_ip String? @db.VarChar(45) + action String @db.VarChar(100) + entity_type String? @db.VarChar(50) + entity_id Int? + description String? @db.Text + old_values String? @db.LongText + new_values String? @db.LongText + user_agent String? @db.Text + session_id String? @db.VarChar(128) + created_at DateTime? @default(now()) @db.Timestamp(0) + + @@index([created_at], map: "idx_audit_log_created") + @@index([action], map: "idx_audit_logs_action") + @@index([created_at], map: "idx_audit_logs_created") + @@index([entity_type, entity_id, created_at], map: "idx_audit_logs_entity") + @@index([user_id, created_at], map: "idx_audit_logs_user_created") + @@fulltext([description], map: "idx_audit_search") +} + +model bank_accounts { + id Int @id @default(autoincrement()) + account_name String? @db.VarChar(255) + bank_name String? @db.VarChar(255) + account_number String? @db.VarChar(50) + iban String? @db.VarChar(50) + bic String? @db.VarChar(20) + currency String? @default("CZK") @db.VarChar(10) + is_default Boolean? @default(false) + position Int? @default(0) + created_at DateTime? @default(now()) @db.DateTime(0) + modified_at DateTime? @db.DateTime(0) +} + +model company_settings { + id Int @id @default(autoincrement()) + company_name String? @db.VarChar(255) + street String? @db.VarChar(255) + city String? @db.VarChar(255) + postal_code String? @db.VarChar(20) + country String? @db.VarChar(100) + company_id String? @db.VarChar(50) + vat_id String? @db.VarChar(50) + custom_fields String? @db.LongText + logo_data Bytes? + quotation_prefix String? @db.VarChar(20) + default_currency String? @default("CZK") @db.VarChar(10) + default_vat_rate Decimal? @default(21.00) @db.Decimal(5, 2) + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) + order_type_code String? @db.VarChar(10) + invoice_type_code String? @db.VarChar(10) + require_2fa Boolean @default(false) +} + +model customers { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + street String? @db.VarChar(255) + city String? @db.VarChar(255) + postal_code String? @db.VarChar(20) + country String? @db.VarChar(100) + company_id String? @db.VarChar(50) + vat_id String? @db.VarChar(50) + custom_fields String? @db.LongText + created_at DateTime? @default(now()) @db.DateTime(0) + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + sync_version Int? @default(0) + invoices invoices[] + orders orders[] + projects projects[] + quotations quotations[] +} + +model invoice_items { + id Int @id @default(autoincrement()) + invoice_id Int + description String? @db.VarChar(500) + quantity Decimal? @default(1.000) @db.Decimal(12, 3) + unit String? @db.VarChar(20) + unit_price Decimal? @default(0.00) @db.Decimal(12, 2) + vat_rate Decimal? @default(21.00) @db.Decimal(5, 2) + position Int? @default(0) + invoices invoices @relation(fields: [invoice_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "invoice_items_ibfk_1") + + @@index([invoice_id], map: "invoice_id") +} + +model invoices { + id Int @id @default(autoincrement()) + invoice_number String? @db.VarChar(50) + order_id Int? + customer_id Int? + status String? @default("issued") @db.VarChar(30) + currency String? @default("CZK") @db.VarChar(10) + vat_rate Decimal? @default(21.00) @db.Decimal(5, 2) + apply_vat Boolean? @default(true) + payment_method String? @db.VarChar(50) + constant_symbol String? @db.VarChar(20) + bank_name String? @db.VarChar(255) + bank_swift String? @db.VarChar(20) + bank_iban String? @db.VarChar(50) + bank_account String? @db.VarChar(50) + issue_date DateTime? @db.Date + due_date DateTime? @db.Date + tax_date DateTime? @db.Date + paid_date DateTime? @db.Date + issued_by String? @db.VarChar(255) + notes String? @db.Text + internal_notes String? @db.Text + created_at DateTime? @default(now()) @db.DateTime(0) + modified_at DateTime? @db.DateTime(0) + invoice_items invoice_items[] + orders orders? @relation(fields: [order_id], references: [id], onUpdate: NoAction, map: "invoices_ibfk_1") + customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "invoices_ibfk_2") + + @@index([customer_id], map: "customer_id") + @@index([due_date], map: "idx_invoices_due_date") + @@index([status, issue_date], map: "idx_invoices_status_issue") + @@index([order_id], map: "order_id") +} + +model item_templates { + id Int @id @default(autoincrement()) + name String? @db.VarChar(255) + description String? @db.Text + default_price Decimal? @default(0.00) @db.Decimal(12, 2) + category String? @db.VarChar(100) + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) +} + +model leave_balances { + id Int @id @default(autoincrement()) + user_id Int + year Int + vacation_total Decimal? @default(160.00) @db.Decimal(5, 2) + vacation_used Decimal? @default(0.00) @db.Decimal(5, 2) + sick_used Decimal? @default(0.00) @db.Decimal(5, 2) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "leave_balances_ibfk_1") + + @@unique([user_id, year], map: "idx_leave_balances_user_year") +} + +model leave_requests { + id Int @id @default(autoincrement()) + user_id Int + leave_type leave_requests_leave_type + date_from DateTime @db.Date + date_to DateTime @db.Date + total_hours Decimal @db.Decimal(5, 2) + total_days Int + notes String? @db.Text + status leave_requests_status? @default(pending) + reviewer_id Int? + reviewer_note String? @db.Text + reviewed_at DateTime? @db.DateTime(0) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + users_leave_requests_user_idTousers users @relation("leave_requests_user_idTousers", fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "leave_requests_ibfk_1") + users_leave_requests_reviewer_idTousers users? @relation("leave_requests_reviewer_idTousers", fields: [reviewer_id], references: [id], onUpdate: NoAction, map: "leave_requests_ibfk_2") + + @@index([user_id, status], map: "idx_leave_requests_user_status") + @@index([status], map: "idx_status") + @@index([reviewer_id], map: "reviewer_id") +} + +model number_sequences { + id Int @id @default(autoincrement()) + type String? @db.VarChar(50) + year Int? + last_number Int? @default(0) +} + +model order_items { + id Int @id @default(autoincrement()) + order_id Int + description String? @db.VarChar(500) + item_description String? @db.Text + quantity Decimal? @default(1.000) @db.Decimal(12, 3) + unit String? @db.VarChar(20) + unit_price Decimal? @default(0.00) @db.Decimal(12, 2) + is_included_in_total Boolean? @default(true) + position Int? @default(0) + modified_at DateTime? @db.DateTime(0) + orders orders @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "order_items_ibfk_1") + + @@index([order_id], map: "order_id") +} + +model order_sections { + id Int @id @default(autoincrement()) + order_id Int + title String? @db.VarChar(500) + title_cz String? @db.VarChar(500) + content String? @db.Text + position Int? @default(0) + modified_at DateTime? @db.DateTime(0) + orders orders @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "order_sections_ibfk_1") + + @@index([order_id], map: "order_id") +} + +model orders { + id Int @id @default(autoincrement()) + order_number String? @db.VarChar(50) + customer_order_number String? @db.VarChar(100) + attachment_data Bytes? + attachment_name String? @db.VarChar(255) + quotation_id Int? + customer_id Int? + status String? @default("prijata") @db.VarChar(30) + currency String? @default("CZK") @db.VarChar(10) + language String? @default("cs") @db.VarChar(5) + vat_rate Decimal? @default(21.00) @db.Decimal(5, 2) + apply_vat Boolean? @default(true) + exchange_rate Decimal? @default(1.0000) @db.Decimal(10, 4) + scope_title String? @db.VarChar(500) + scope_description String? @db.Text + notes String? @db.Text + created_at DateTime? @default(now()) @db.DateTime(0) + modified_at DateTime? @db.DateTime(0) + invoices invoices[] + order_items order_items[] + order_sections order_sections[] + quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "orders_ibfk_1") + customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "orders_ibfk_2") + projects projects[] + + @@index([customer_id], map: "customer_id") + @@index([quotation_id], map: "quotation_id") +} + +model permissions { + id Int @id @default(autoincrement()) + name String @unique(map: "name") @db.VarChar(50) + display_name String @db.VarChar(100) + description String? @db.Text + module String @db.VarChar(50) + created_at DateTime? @default(now()) @db.Timestamp(0) + role_permissions role_permissions[] +} + +model project_notes { + id Int @id @default(autoincrement()) + project_id Int + user_id Int? + user_name String? @db.VarChar(100) + content String? @db.Text + created_at DateTime? @default(now()) @db.DateTime(0) + projects projects @relation(fields: [project_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "project_notes_ibfk_1") + + @@index([project_id], map: "project_id") +} + +model projects { + id Int @id @default(autoincrement()) + project_number String? @db.VarChar(50) + name String? @db.VarChar(255) + customer_id Int? + responsible_user_id Int? + quotation_id Int? + order_id Int? + status String? @default("aktivni") @db.VarChar(30) + start_date DateTime? @db.Date + end_date DateTime? @db.Date + notes String? @db.Text + created_at DateTime? @default(now()) @db.DateTime(0) + modified_at DateTime? @db.DateTime(0) + project_notes project_notes[] + users users? @relation(fields: [responsible_user_id], references: [id], onUpdate: NoAction, map: "fk_projects_responsible_user") + customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_1") + quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_2") + orders orders? @relation(fields: [order_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_3") + + @@index([customer_id], map: "customer_id") + @@index([responsible_user_id], map: "fk_projects_responsible_user") + @@index([order_id], map: "order_id") + @@index([quotation_id], map: "quotation_id") +} + +model quotation_items { + id Int @id @default(autoincrement()) + quotation_id Int + position Int? @default(0) + description String? @db.VarChar(500) + item_description String? @db.Text + quantity Decimal? @default(1.000) @db.Decimal(12, 3) + unit String? @db.VarChar(20) + unit_price Decimal? @default(0.00) @db.Decimal(12, 2) + is_included_in_total Boolean? @default(true) + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) + quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "quotation_items_ibfk_1") + + @@index([quotation_id], map: "quotation_id") +} + +model quotations { + id Int @id @default(autoincrement()) + quotation_number String? @db.VarChar(50) + project_code String? @db.VarChar(50) + customer_id Int? + created_at DateTime? @default(now()) @db.DateTime(0) + valid_until DateTime? @db.Date + currency String? @default("CZK") @db.VarChar(10) + language String? @default("cs") @db.VarChar(5) + vat_rate Decimal? @default(21.00) @db.Decimal(5, 2) + apply_vat Boolean? @default(true) + exchange_rate Decimal? @default(1.0000) @db.Decimal(10, 4) + exchange_rate_date DateTime? @db.Date + order_id Int? + status String @default("active") @db.VarChar(20) + scope_title String? @db.VarChar(500) + scope_description String? @db.Text + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + sync_version Int? @default(0) + orders orders[] + projects projects[] + quotation_items quotation_items[] + customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "quotations_ibfk_1") + scope_sections scope_sections[] + + @@index([customer_id], map: "customer_id") + @@index([quotation_number], map: "idx_quotations_number") +} + +model received_invoices { + id Int @id @default(autoincrement()) @db.UnsignedInt + month Int @db.UnsignedTinyInt + year Int @db.UnsignedSmallInt + supplier_name String @db.VarChar(255) + invoice_number String? @db.VarChar(100) + description String? @db.VarChar(500) + amount Decimal @default(0.00) @db.Decimal(12, 2) + currency String @default("CZK") @db.VarChar(3) + vat_rate Decimal @default(21.00) @db.Decimal(5, 2) + vat_amount Decimal @default(0.00) @db.Decimal(12, 2) + issue_date DateTime? @db.Date + due_date DateTime? @db.Date + paid_date DateTime? @db.Date + status received_invoices_status @default(unpaid) + file_data Bytes? @db.MediumBlob + file_name String? @db.VarChar(255) + file_mime String? @db.VarChar(100) + file_size Int? @db.UnsignedInt + notes String? @db.Text + uploaded_by Int? @db.UnsignedInt + created_at DateTime @default(now()) @db.DateTime(0) + modified_at DateTime @default(now()) @db.DateTime(0) + + @@index([year, month], map: "idx_month_year") + @@index([status], map: "idx_status") + @@index([supplier_name], map: "idx_supplier") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model refresh_tokens { + id Int @id @default(autoincrement()) @db.UnsignedInt + user_id Int @db.UnsignedInt + token_hash String @unique(map: "token_hash") @db.VarChar(64) + expires_at DateTime @db.DateTime(0) + replaced_at DateTime? @db.DateTime(0) + replaced_by_hash String? @db.VarChar(64) + remember_me Boolean @default(false) + ip_address String? @db.VarChar(45) + user_agent String? @db.VarChar(255) + created_at DateTime? @default(now()) @db.DateTime(0) + + @@index([expires_at], map: "idx_refresh_tokens_expires_at") + @@index([user_id, expires_at], map: "idx_refresh_tokens_user_exp") + @@index([user_id], map: "idx_refresh_tokens_user_id") +} + +model role_permissions { + role_id Int + permission_id Int + roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "role_permissions_ibfk_1") + permissions permissions @relation(fields: [permission_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "role_permissions_ibfk_2") + + @@id([role_id, permission_id]) + @@index([permission_id], map: "permission_id") +} + +model roles { + id Int @id @default(autoincrement()) + name String @unique(map: "name") @db.VarChar(50) + display_name String @db.VarChar(100) + description String? @db.Text + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + role_permissions role_permissions[] + users users[] +} + +model scope_sections { + id Int @id @default(autoincrement()) + quotation_id Int + position Int? @default(0) + title String? @db.VarChar(500) + title_cz String? @db.VarChar(500) + content String? @db.Text + content_editor_height Int? + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) + quotations quotations @relation(fields: [quotation_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_sections_ibfk_1") + + @@index([quotation_id], map: "quotation_id") +} + +model scope_template_sections { + id Int @id @default(autoincrement()) + scope_template_id Int + position Int? @default(0) + title String? @db.VarChar(500) + title_cz String? @db.VarChar(500) + content String? @db.Text + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) + scope_templates scope_templates @relation(fields: [scope_template_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "scope_template_sections_ibfk_1") + + @@index([scope_template_id], map: "scope_template_id") +} + +model scope_templates { + id Int @id @default(autoincrement()) + name String? @db.VarChar(255) + title String? @db.VarChar(500) + description String? @db.Text + uuid String? @db.VarChar(36) + modified_at DateTime? @db.DateTime(0) + is_deleted Boolean? @default(false) + sync_version Int? @default(0) + scope_template_sections scope_template_sections[] +} + +model totp_login_tokens { + id Int @id @default(autoincrement()) + user_id Int + token_hash String @db.VarChar(64) + expires_at DateTime @db.DateTime(0) + created_at DateTime @default(now()) @db.DateTime(0) + + @@index([expires_at], map: "idx_expires") + @@index([token_hash], map: "idx_totp_login_tokens_hash") + @@index([user_id], map: "idx_user_id") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model trips { + id Int @id @default(autoincrement()) + vehicle_id Int + user_id Int + trip_date DateTime @db.Date + start_km Int + end_km Int + distance Int? + route_from String @db.VarChar(100) + route_to String @db.VarChar(100) + is_business Boolean @default(true) + notes String? @db.Text + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "trips_user_fk") + vehicles vehicles @relation(fields: [vehicle_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "trips_vehicle_fk") + + @@index([trip_date], map: "trip_date") + @@index([user_id], map: "user_id") + @@index([vehicle_id], map: "vehicle_id") +} + +model users { + id Int @id @default(autoincrement()) + username String @unique(map: "username") @db.VarChar(50) + email String @unique(map: "email") @db.VarChar(255) + password_hash String @db.VarChar(255) + first_name String @db.VarChar(50) + last_name String @db.VarChar(50) + role_id Int? + is_active Boolean? @default(true) + last_login DateTime? @db.Timestamp(0) + failed_login_attempts Int? @default(0) + locked_until DateTime? @db.Timestamp(0) + password_changed_at DateTime? @default(now()) @db.Timestamp(0) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + totp_secret String? @db.VarChar(255) + totp_enabled Boolean @default(false) + totp_backup_codes String? @db.Text + attendance attendance[] + leave_balances leave_balances[] + leave_requests_leave_requests_user_idTousers leave_requests[] @relation("leave_requests_user_idTousers") + leave_requests_leave_requests_reviewer_idTousers leave_requests[] @relation("leave_requests_reviewer_idTousers") + projects projects[] + trips trips[] + roles roles? @relation(fields: [role_id], references: [id], onUpdate: NoAction, map: "users_ibfk_1") + + @@index([is_active], map: "idx_users_is_active") + @@index([role_id], map: "idx_users_role_id") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model vehicles { + id Int @id @default(autoincrement()) + spz String @unique(map: "spz") @db.VarChar(20) + name String @db.VarChar(100) + brand String? @db.VarChar(50) + model String? @db.VarChar(50) + initial_km Int @default(0) + actual_km Int @default(0) + is_active Boolean @default(true) + created_at DateTime? @default(now()) @db.Timestamp(0) + updated_at DateTime? @default(now()) @db.Timestamp(0) + trips trips[] +} + +enum leave_requests_leave_type { + vacation + sick + unpaid +} + +enum leave_requests_status { + pending + approved + rejected + cancelled +} + +enum received_invoices_status { + unpaid + paid +} + +enum attendance_leave_type { + work + vacation + sick + holiday + unpaid +} diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/images/logo-dark.png b/public/images/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c77a7f519d7af58a70ed734a28c4127e0dda6d93 GIT binary patch literal 16982 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 +
+
+ ) +} + +export default function App() { + return ( + }> + + } /> + + + ) +} diff --git a/src/admin/AdminApp.tsx b/src/admin/AdminApp.tsx new file mode 100644 index 0000000..455d3a8 --- /dev/null +++ b/src/admin/AdminApp.tsx @@ -0,0 +1,96 @@ +import { lazy, Suspense } from 'react' +import { Routes, Route } from 'react-router-dom' +import { AuthProvider } from './context/AuthContext' +import { AlertProvider } from './context/AlertContext' +import ErrorBoundary from './components/ErrorBoundary' +import AdminLayout from './components/AdminLayout' +import AlertContainer from './components/AlertContainer' +import Login from './pages/Login' +import Dashboard from './pages/Dashboard' +import './admin.css' +import './login.css' +import './dashboard.css' +import './attendance.css' +import './settings.css' +import './offers.css' +import './invoices.css' + +const Users = lazy(() => import('./pages/Users')) +const Attendance = lazy(() => import('./pages/Attendance')) +const AttendanceHistory = lazy(() => import('./pages/AttendanceHistory')) +const AttendanceAdmin = lazy(() => import('./pages/AttendanceAdmin')) +const AttendanceBalances = lazy(() => import('./pages/AttendanceBalances')) +const AttendanceCreate = lazy(() => import('./pages/AttendanceCreate')) +const LeaveRequests = lazy(() => import('./pages/LeaveRequests')) +const LeaveApproval = lazy(() => import('./pages/LeaveApproval')) +const AttendanceLocation = lazy(() => import('./pages/AttendanceLocation')) +const Trips = lazy(() => import('./pages/Trips')) +const TripsHistory = lazy(() => import('./pages/TripsHistory')) +const TripsAdmin = lazy(() => import('./pages/TripsAdmin')) +const Vehicles = lazy(() => import('./pages/Vehicles')) +const Offers = lazy(() => import('./pages/Offers')) +const OfferDetail = lazy(() => import('./pages/OfferDetail')) +const OffersCustomers = lazy(() => import('./pages/OffersCustomers')) +const OffersTemplates = lazy(() => import('./pages/OffersTemplates')) +const CompanySettings = lazy(() => import('./pages/CompanySettings')) +const Orders = lazy(() => import('./pages/Orders')) +const OrderDetail = lazy(() => import('./pages/OrderDetail')) +const Projects = lazy(() => import('./pages/Projects')) +const ProjectCreate = lazy(() => import('./pages/ProjectCreate')) +const ProjectDetail = lazy(() => import('./pages/ProjectDetail')) +const Invoices = lazy(() => import('./pages/Invoices')) +const InvoiceCreate = lazy(() => import('./pages/InvoiceCreate')) +const InvoiceDetail = lazy(() => import('./pages/InvoiceDetail')) +const Settings = lazy(() => import('./pages/Settings')) +const AuditLog = lazy(() => import('./pages/AuditLog')) +const NotFound = lazy(() => import('./pages/NotFound')) + +export default function AdminApp() { + return ( + + + + +
}> + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + +
+
+
+
+ ) +} diff --git a/src/admin/admin.css b/src/admin/admin.css new file mode 100644 index 0000000..9c0e0e1 --- /dev/null +++ b/src/admin/admin.css @@ -0,0 +1,2860 @@ +/* ============================================================================ + Admin Panel Styles + ============================================================================ */ + +/* ============================================================================ + Reset & Base + ============================================================================ */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + overflow-x: hidden; +} + +html, body, #root { + min-height: 100%; + min-height: 100dvh; + max-width: 100vw; +} + +body { + font-family: var(--font-body); + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-primary); + overflow-x: hidden; + overscroll-behavior-x: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.admin-sidebar, +.admin-header, +.admin-card, +.admin-modal { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +#root { + overflow-x: hidden; + touch-action: pan-y pinch-zoom; +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + font-weight: 700; + line-height: 1.2; + color: var(--text-primary); +} + +h1 { font-size: clamp(2.5rem, 5vw, 4rem); } +h2 { font-size: clamp(2rem, 4vw, 3rem); } +h3 { font-size: clamp(1.25rem, 2vw, 1.5rem); } + +p { + color: var(--text-secondary); + line-height: 1.6; +} + +a { + color: inherit; + text-decoration: none; + transition: var(--transition); +} + +img { + max-width: 100%; + height: auto; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +::selection { + background: var(--accent-color); + color: #fff; +} + +/* ============================================================================ + CSS Variables + ============================================================================ */ + +:root { + /* Spacing scale */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + + /* Shared colors */ + --accent-color: #D63031; + --accent-hover: #B52626; + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --info: #3b82f6; + --error: var(--danger); + --muted: #9ca3af; + --gradient: #D63031; + --gradient-subtle: rgba(214, 48, 49, 0.9); + + /* Shared layout */ + --border-radius: 10px; + --border-radius-sm: 8px; + --border-radius-lg: 16px; + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + --font-heading: 'Urbanist', sans-serif; + --font-body: 'Plus Jakarta Sans', sans-serif; + --font-mono: 'DM Mono', 'Menlo', monospace; + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --safe-right: env(safe-area-inset-right, 0px); + --navbar-height: calc(76px + var(--safe-top)); +} + +/* ---- Dark theme ---- */ +[data-theme="dark"] { + --bg-primary: #0f0f0f; + --bg-secondary: #171717; + --bg-tertiary: #1e1e1e; + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --text-muted: #666666; + --text-tertiary: #555555; + --border-color: rgba(255, 255, 255, 0.08); + --border-color-hover: rgba(255, 255, 255, 0.15); + --glass-bg: #171717; + --glass-bg-solid: #171717; + --glass-border: rgba(255, 255, 255, 0.08); + --glass-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15); + --card-bg: #1a1a1a; + --card-bg-hover: #1e1e1e; + --input-bg: #1a1a1a; + --glow-color: rgba(214, 48, 49, 0.15); + --accent-light: rgba(214, 48, 49, 0.1); + --accent-soft: #2a1a1a; + --accent-glow: rgba(214, 48, 49, 0.3); + --success-light: rgba(34, 197, 94, 0.1); + --success-soft: #1a2a1e; + --warning-light: rgba(245, 158, 11, 0.1); + --warning-soft: #2a2518; + --danger-light: rgba(239, 68, 68, 0.1); + --danger-soft: #2a1a1a; + --info-light: rgba(59, 130, 246, 0.1); + --info-soft: #1a1e2a; + --muted-light: rgba(107, 114, 128, 0.15); + --orb-color-1: rgba(214, 48, 49, 0.2); + --orb-color-2: rgba(120, 119, 198, 0.15); + --calendar-icon-filter: invert(1); + --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a0a0a0' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + --table-row-hover: rgba(255, 255, 255, 0.02); + --row-current: color-mix(in srgb, var(--success) 5%, transparent); + --row-current-hover: color-mix(in srgb, var(--success) 8%, transparent); + --row-draft: color-mix(in srgb, var(--warning) 6%, transparent); + --row-expired: color-mix(in srgb, var(--danger) 5%, transparent); +} + +/* ---- Light theme ---- */ +[data-theme="light"] { + --success: #15803d; + --warning: #b45309; + --danger: #b91c1c; + --info: #1d4ed8; + --accent-color: #c73030; + --accent-hover: #b52828; + --muted: #6b7280; + --bg-primary: #F5F4F2; + --bg-secondary: #ffffff; + --bg-tertiary: #EEECEA; + --text-primary: #1A1A1A; + --text-secondary: #555555; + --text-muted: #717180; + --text-tertiary: #8a8a96; + --border-color: rgba(0, 0, 0, 0.10); + --border-color-hover: rgba(0, 0, 0, 0.18); + --glass-bg: #ffffff; + --glass-bg-solid: #ffffff; + --glass-border: rgba(0, 0, 0, 0.08); + --glass-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.04); + --card-bg: #ffffff; + --card-bg-hover: #ffffff; + --input-bg: #ffffff; + --glow-color: rgba(222, 58, 58, 0.08); + --accent-light: rgba(222, 58, 58, 0.08); + --accent-soft: #FFF0F0; + --accent-glow: rgba(222, 58, 58, 0.15); + --success-light: rgba(34, 197, 94, 0.1); + --success-soft: #E8FBF7; + --warning-light: rgba(245, 158, 11, 0.1); + --warning-soft: #FEF9EC; + --danger-light: rgba(239, 68, 68, 0.1); + --danger-soft: #FEF2F2; + --info-light: rgba(59, 130, 246, 0.1); + --info-soft: #EBF3FD; + --muted-light: rgba(107, 114, 128, 0.12); + --calendar-icon-filter: none; + --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23555555' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + --table-row-hover: rgba(0, 0, 0, 0.03); + --row-current: var(--success-soft); + --row-current-hover: color-mix(in srgb, var(--success) 12%, transparent); + --row-draft: var(--warning-soft); + --row-expired: var(--danger-soft); + --orb-color-1: rgba(214, 48, 49, 0.12); + --orb-color-2: rgba(120, 119, 198, 0.1); +} + +/* Light mode - jemnejsi stiny */ +[data-theme="light"] .admin-toast { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06); +} + +[data-theme="light"] .react-datepicker { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important; +} + +[data-theme="light"] .rich-editor .ql-snow .ql-picker-options, +[data-theme="light"] .rich-editor .ql-snow .ql-tooltip { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +[data-theme="light"] .offers-customer-dropdown, +[data-theme="light"] .offers-template-menu { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +[data-theme="light"] .dash-quick-btn:hover { + filter: brightness(0.9); +} + +/* ============================================================================ + Base / Utilities + ============================================================================ */ + +.text-warning { + color: var(--warning) !important; +} + +.text-danger { + color: var(--danger) !important; +} + +.text-success { + color: var(--success) !important; +} + +.text-muted { + color: var(--text-muted) !important; +} + +.text-secondary { + color: var(--text-secondary) !important; +} + +.text-tertiary { + color: var(--text-tertiary) !important; +} + +.text-accent { + color: var(--accent-color) !important; +} + +.fw-600 { + font-weight: 600 !important; +} + +.link-accent { + color: var(--accent-color); + font-weight: 500; + text-decoration: none; +} + +.link-accent:hover { + text-decoration: underline; +} + +/* Layout utilities */ +.flex-1 { flex: 1; } +.flex-row { display: flex; align-items: center; } +.flex-row-gap { display: flex; align-items: center; gap: var(--space-3); } +.flex-between { display: flex; align-items: center; justify-content: space-between; } + +/* Spacing utilities */ +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-6 { margin-bottom: var(--space-6); } +.mt-2 { margin-top: var(--space-2); } +.mt-6 { margin-top: var(--space-6); } +.gap-2 { gap: var(--space-2); } +.gap-4 { gap: var(--space-4); } +.gap-5 { gap: var(--space-5); } + +/* Typography utilities */ +.fw-500 { font-weight: 500; } +.text-right { text-align: right; } +.text-center { text-align: center; } + +/* Spinner variant */ +.admin-spinner-sm { width: 16px; height: 16px; border-width: 2px; } + +/* ============================================================================ + Forms + ============================================================================ */ + +.admin-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.admin-form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.admin-form-label { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); +} + +.admin-form-input { + width: 100%; + padding: 9px 12px; + background: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + outline: none; + transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-sizing: border-box; + min-height: 36px; +} + +.admin-form-input:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-light); +} + +.admin-form-input::placeholder { + color: var(--text-muted); +} + +.admin-form-input[type="date"], +.admin-form-input[type="time"], +.admin-form-input[type="month"], +.admin-form-input[type="number"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + text-align: left; + height: 36px; + max-width: 100%; +} + +.admin-form-input[type="number"]::-webkit-inner-spin-button, +.admin-form-input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.admin-form-input[type="date"]::-webkit-date-and-time-value, +.admin-form-input[type="time"]::-webkit-date-and-time-value, +.admin-form-input[type="month"]::-webkit-date-and-time-value { + text-align: left; + margin: 0; +} + +.admin-form-input[type="date"]::-webkit-datetime-edit, +.admin-form-input[type="time"]::-webkit-datetime-edit, +.admin-form-input[type="month"]::-webkit-datetime-edit { + padding: 0; +} + +.admin-form-input[type="date"]::-webkit-calendar-picker-indicator, +.admin-form-input[type="time"]::-webkit-calendar-picker-indicator, +.admin-form-input[type="month"]::-webkit-calendar-picker-indicator { + filter: var(--calendar-icon-filter, none); + cursor: pointer; +} + +/* Select */ +.admin-form-select { + width: 100%; + padding: 9px 12px; + background: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + outline: none; + transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + min-height: 36px; + box-sizing: border-box; + cursor: pointer; + appearance: none; + background-image: var(--select-arrow); + background-repeat: no-repeat; + background-position: right 0.75rem center; + padding-right: 32px; +} + +.admin-form-select:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-light); +} + +.admin-form-select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +/* Textarea */ +.admin-form-textarea { + width: 100%; + padding: 9px 12px; + background: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + outline: none; + transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); + resize: vertical; + box-sizing: border-box; + min-height: 80px; +} + +.admin-form-textarea:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-light); +} + +/* Checkbox */ +.admin-form-checkbox { + display: inline-flex; + align-items: flex-start; + gap: 0; + cursor: pointer; + user-select: none; +} + +.admin-form-checkbox input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.admin-form-checkbox input + span::before { + content: ''; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-right: 8px; + background: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + vertical-align: middle; + transition: border-color var(--transition), box-shadow var(--transition), background var(--transition); + flex-shrink: 0; +} + +.admin-form-checkbox input:checked + span::before { + background: var(--accent-color); + border-color: var(--accent-color); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + background-size: 12px; + background-position: center; + background-repeat: no-repeat; +} + +.admin-form-checkbox input:focus + span::before { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-light); +} + +.admin-form-checkbox:hover input:not(:checked):not(:disabled):not(:indeterminate) + span::before { + border-color: var(--border-color-hover); + background: var(--bg-secondary); +} + +.admin-form-checkbox input:indeterminate + span::before { + background: var(--accent-color); + border-color: var(--accent-color); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round'%3E%3Cline x1='6' y1='12' x2='18' y2='12'%3E%3C/line%3E%3C/svg%3E"); + background-size: 12px; + background-position: center; + background-repeat: no-repeat; +} + +.admin-form-checkbox input:disabled + span::before { + opacity: 0.5; + cursor: not-allowed; +} + +.admin-form-checkbox:has(input:disabled) { + cursor: not-allowed; + opacity: 0.7; +} + +.admin-form-checkbox span { + display: flex; + align-items: center; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.4; +} + +/* Reorderable List */ +.admin-reorder-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.admin-reorder-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + background: var(--bg-tertiary); + border-radius: var(--border-radius-sm); +} + +.admin-reorder-arrows { + display: flex; + gap: 2px; +} + +.admin-reorder-label { + font-size: 13px; + color: var(--text-primary); +} + +.admin-reorder-label.accent { + color: var(--accent-color); +} + +.admin-reorder-arrows .admin-btn-icon { + width: 22px; + height: 22px; + color: var(--text-muted); +} + +.admin-reorder-arrows .admin-btn-icon:hover:not(:disabled) { + background: var(--bg-primary); + color: var(--text-primary); +} + +.admin-reorder-arrows .admin-btn-icon:disabled { + opacity: 0.25; +} + +/* Form Rows (Grid Layouts) */ +.admin-form-row { + display: grid; + gap: 1rem; + grid-template-columns: repeat(2, 1fr); +} + +.admin-form-row-3 { + grid-template-columns: repeat(3, 1fr); +} + +.admin-form-row-4 { + grid-template-columns: repeat(4, 1fr); +} + +.admin-form-row-5 { + grid-template-columns: 1.2fr 1fr 1fr 1fr 1fr; +} + +@media (max-width: 768px) { + .admin-form-row-4 { + grid-template-columns: repeat(2, 1fr); + } + .admin-form-row-5 { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 640px) { + .admin-form-row, + .admin-form-row-3 { + grid-template-columns: 1fr; + } + .admin-form-row-5 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .admin-form-row-4, + .admin-form-row-5 { + grid-template-columns: 1fr; + } +} + +/* Form Utilities */ +.admin-form-hint { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 0.25rem; +} + +/* Required field indicator */ +.admin-form-label.required::after { + content: ' *'; + color: var(--danger); + font-weight: 600; +} + +/* Inline field errors */ +.admin-form-group.has-error .admin-form-input, +.admin-form-group.has-error .admin-form-select, +.admin-form-group.has-error .admin-form-textarea { + border-color: var(--danger); + box-shadow: 0 0 0 3px var(--danger-light); +} + +.admin-form-group.has-error .admin-form-label { + color: var(--danger); +} + +.admin-form-error { + font-size: 0.75rem; + color: var(--danger); + margin-top: 0.25rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* Monospace for data values (times, dates, numbers, IDs) */ +.admin-mono { + font-family: var(--font-mono); + font-size: 0.875em; + letter-spacing: -0.01em; +} + +.admin-form-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-color); +} + +/* ============================================================================ + Buttons + ============================================================================ */ + +.admin-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 8px 14px; + border: none; + border-radius: var(--border-radius-sm); + font-size: 13px; + font-weight: 550; + font-family: inherit; + cursor: pointer; + transition: var(--transition); +} + +.admin-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.admin-btn-sm { + padding: 6px 11px; + font-size: 12px; +} + +.admin-btn-primary { + background: var(--accent-color); + color: #fff; +} + +.admin-btn-primary:hover:not(:disabled) { + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(214, 48, 49, 0.3); +} + +.admin-btn-primary .admin-spinner { + border-color: rgba(255, 255, 255, 0.3); + border-top-color: #fff; +} + +.admin-btn-secondary { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-secondary); +} + +.admin-btn-secondary:hover:not(:disabled) { + background: var(--bg-secondary); + border-color: var(--border-color-hover); + color: var(--text-primary); +} + + +.admin-btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + width: 32px; + height: 32px; + background: transparent; + border: none; + color: var(--text-secondary); + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: var(--transition); +} + +.admin-btn-icon:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.admin-btn-icon.accent { + color: var(--info); +} + +.admin-btn-icon.accent:hover { + background: color-mix(in srgb, var(--info) 15%, transparent); + color: var(--info); +} + +.admin-btn-icon.danger { + color: var(--danger); +} + +.admin-btn-icon.danger:hover { + background: var(--danger-light); +} + +/* ============================================================================ + Layout + ============================================================================ */ + +.admin-layout { + display: flex; + min-height: 100vh; + min-height: 100dvh; + background: var(--bg-primary); +} + +@media (min-width: 1024px) { + .admin-layout { + align-items: flex-start; + } +} + +/* ============================================================================ + Sidebar + ============================================================================ */ + +/* -- Theme variables for sidebar -- */ +[data-theme="dark"] .admin-sidebar { + --sb-bg: #141414; + --sb-border: #2a2a2a; + --sb-text: #A0A0A0; + --sb-text-hover: #ddd; + --sb-hover-bg: #1f1f1f; + --sb-active-bg: #ffffff; + --sb-active-text: #141414; + --sb-label: #444; + --sb-muted: #555; + --sb-scrollbar: #333; +} + +[data-theme="light"] .admin-sidebar { + --sb-bg: #ffffff; + --sb-border: #E8E6E1; + --sb-text: #7C7C84; + --sb-text-hover: #1A1A1A; + --sb-hover-bg: #F5F4F2; + --sb-active-bg: #141414; + --sb-active-text: #ffffff; + --sb-label: #A0A0A0; + --sb-muted: #A0A0A0; + --sb-scrollbar: #ddd; +} + +.admin-sidebar { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100vh; + height: 100dvh; + z-index: 50; + background: var(--sb-bg); + border-right: 1px solid var(--sb-border); + display: flex; + flex-direction: column; + padding-top: env(safe-area-inset-top, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); + padding-left: env(safe-area-inset-left, 0px); + padding-right: env(safe-area-inset-right, 0px); + transform: translateX(-100%); + visibility: hidden; + transition: transform 0.3s ease, visibility 0.3s ease; + overflow: hidden; + overscroll-behavior: none; +} + +.admin-sidebar.open { + transform: translateX(0); + visibility: visible; + touch-action: none; +} + +@media (min-width: 1024px) { + .admin-sidebar { + right: auto; + width: 220px; + height: 100%; + transform: none; + visibility: visible; + padding: 0; + } +} + +[data-theme="light"] .admin-sidebar { + box-shadow: 1px 0 0 0 var(--sb-border), 4px 0 16px rgba(0, 0, 0, 0.04); +} + +/* Sidebar Overlay (mobile) */ +.admin-sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 49; + backdrop-filter: blur(2px); +} + +.admin-sidebar-overlay.open { + display: block; +} + +@media (min-width: 1024px) { + .admin-sidebar-overlay { + display: none !important; + } +} + +/* Sidebar Header */ +.admin-sidebar-header { + padding: 0 18px; + height: 73px; + border-bottom: 1px solid var(--sb-border); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.admin-sidebar-logo { + height: 28px; + width: auto; +} + +.admin-sidebar-close { + display: block; + padding: 0.5rem; + background: transparent; + border: none; + color: var(--sb-text); + cursor: pointer; + border-radius: 6px; +} + +.admin-sidebar-close:hover { + background: var(--sb-hover-bg); + color: var(--sb-text-hover); +} + +@media (min-width: 1024px) { + .admin-sidebar-close { + display: none; + } +} + +/* Sidebar Navigation */ +.admin-sidebar-nav { + flex: 1; + min-height: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + padding: 4px 0; +} + +.admin-sidebar-nav::-webkit-scrollbar { + width: 5px; +} + +.admin-sidebar-nav::-webkit-scrollbar-track { + background: transparent; +} + +.admin-sidebar-nav::-webkit-scrollbar-thumb { + background: var(--sb-scrollbar); + border-radius: 99px; +} + +/* Nav Section */ +.admin-nav-section { + padding: 14px 10px 6px; +} + +.admin-nav-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--sb-label); + padding: 0 8px; + margin-bottom: 4px; +} + +/* Nav Item */ +.admin-nav-item { + display: flex; + align-items: center; + gap: 9px; + padding: 7px 10px; + border-radius: 7px; + color: var(--sb-text); + cursor: pointer; + transition: all 0.15s ease; + font-size: 13px; + font-weight: 450; + margin-bottom: 1px; + text-decoration: none; + user-select: none; +} + +.admin-nav-item:hover { + background: var(--sb-hover-bg); + color: var(--sb-text-hover); +} + +.admin-nav-item.active { + background: var(--sb-active-bg); + color: var(--sb-active-text); + font-weight: 600; +} + +.admin-nav-item.active svg { + color: var(--accent-color); +} + +.admin-nav-item svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +@media (max-width: 1023px) { + .admin-nav-item { + padding: 10px 12px; + font-size: 15px; + gap: 10px; + } + + .admin-nav-item svg { + width: 18px; + height: 18px; + } +} + +/* Sidebar Footer */ +.admin-sidebar-footer { + margin-top: auto; + border-top: 1px solid var(--sb-border); + padding: 14px 10px; + flex-shrink: 0; +} + +.admin-user-chip { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: 8px; + margin-bottom: 4px; +} + +.admin-user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--accent-color); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 12px; + flex-shrink: 0; +} + +.admin-user-details { + flex: 1; + min-width: 0; +} + +.admin-user-name { + color: var(--sb-text-hover); + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.admin-user-role { + color: var(--sb-muted); + font-size: 11px; +} + +.admin-logout-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 7px 10px; + background: transparent; + border: none; + color: var(--sb-text); + cursor: pointer; + border-radius: 7px; + font-size: 12px; + font-family: inherit; + transition: all 0.15s ease; +} + +.admin-logout-btn:hover { + background: var(--sb-hover-bg); + color: var(--sb-text-hover); +} + +.admin-logout-btn svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +@media (max-width: 480px) { + .admin-sidebar-footer { + padding: 10px 8px; + } + + .admin-user-chip { + padding: 6px; + } + + .admin-logout-btn { + padding: 8px; + font-size: 13px; + } +} + +/* ============================================================================ + Main Content Area + ============================================================================ */ + +.admin-main { + flex: 1; + min-width: 0; + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +@media (min-width: 1024px) { + .admin-main { + margin-left: 220px; + } +} + +/* Header */ +.admin-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 30; + height: calc(73px + env(safe-area-inset-top, 0px)); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + padding-top: env(safe-area-inset-top, 0px); +} + +@media (min-width: 1024px) { + .admin-header { + left: 220px; + padding: 0 1.5rem; + } +} + +.admin-menu-btn { + display: block; + padding: 0.5rem; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--border-radius-sm); +} + +.admin-menu-btn:hover { + background: var(--bg-tertiary); +} + +@media (min-width: 1024px) { + .admin-menu-btn { + display: none; + } +} + +.admin-header-theme-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-secondary); + cursor: pointer; + border-radius: 50%; + transition: var(--transition); + position: relative; + overflow: hidden; +} + +.admin-header-theme-btn:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transform: scale(1.05); +} + +.admin-theme-icon { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-primary); + transition: all 0.3s ease; + opacity: 0; + transform: rotate(180deg) scale(0.5); +} + +.admin-theme-icon.visible { + opacity: 1; + transform: rotate(0) scale(1); +} + +/* Content */ +.admin-content { + flex: 1; + padding: 1rem; + padding-top: calc(73px + 1rem + env(safe-area-inset-top, 0px)); +} + +@media (min-width: 1024px) { + .admin-content { + padding: 28px 32px; + padding-top: calc(73px + 28px); + } +} + +/* ============================================================================ + Page Headers + ============================================================================ */ + +.admin-page-header { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +@media (min-width: 640px) { + .admin-page-header { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.admin-page-title { + font-size: 22px; + font-weight: 700; + color: var(--text-primary); + font-family: var(--font-heading); + margin-bottom: 0.25rem; +} + +.admin-page-subtitle { + color: var(--text-secondary); + font-size: 13px; +} + +.admin-page-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +@media (max-width: 640px) { + .admin-page-actions { + width: 100%; + } + + .admin-page-actions .admin-btn { + flex: 1; + } +} + +/* ============================================================================ + Cards + ============================================================================ */ + +.admin-card { + background: var(--card-bg); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: var(--border-radius); + overflow: hidden; + margin-bottom: 1rem; +} + +.admin-card:last-child { + margin-bottom: 0; +} + +.admin-card-header { + padding: 14px 18px; + border-bottom: 1px solid var(--border-color); +} + +.admin-card-title { + font-size: 14px; + font-weight: 650; + color: var(--text-primary); + margin: 0 0 12px 0; +} + +.admin-card-body { + padding: 18px; +} + +/* ============================================================================ + Grid System + ============================================================================ */ + +.admin-grid { + display: grid; + gap: 1rem; +} + +.admin-grid > .admin-card { + margin-bottom: 0; +} + +.admin-grid-3 { + grid-template-columns: 1fr; +} + +@media (min-width: 640px) { + .admin-grid-3 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .admin-grid-3 { + grid-template-columns: repeat(3, 1fr); + } +} + +.admin-grid-4 { + grid-template-columns: repeat(2, 1fr); +} + +@media (min-width: 768px) { + .admin-grid-4 { + grid-template-columns: repeat(4, 1fr); + } +} + +/* Stat cards, quick links, dashboard modules moved to dashboard.css */ + +/* ============================================================================ + Tables + ============================================================================ */ + +.admin-table-wrapper, +.admin-table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.admin-table { + width: 100%; + min-width: 650px; + border-collapse: collapse; +} + +.admin-table th { + text-align: left; + padding: 10px 16px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-color); + white-space: nowrap; +} + +.admin-table td { + padding: 11px 16px; + border-bottom: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 13px; + line-height: 1.5; + white-space: nowrap; +} + +.admin-table tr:last-child td { + border-bottom: none; +} + +@media (max-width: 768px) { + .admin-table th, + .admin-table td { + padding: 8px 10px; + font-size: 12px; + } + + .admin-table th { + font-size: 10px; + } + + .admin-table-avatar { + width: 32px; + height: 32px; + font-size: 11px; + } + + .admin-table-name { + font-size: 12px; + } + + .admin-table-username { + font-size: 11px; + } +} + +.admin-table-user { + display: flex; + align-items: center; + gap: 0.75rem; + white-space: nowrap; +} + +.admin-table-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: var(--accent-light); + color: var(--accent-color); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 12px; +} + +.admin-table-name { + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; +} + +.admin-table-username { + font-size: 13px; + color: var(--text-muted); + white-space: nowrap; +} + +.admin-table-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* ============================================================================ + Badges + ============================================================================ */ + +.admin-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 3px 9px; + border-radius: 9999px; + font-size: 11.5px; + font-weight: 600; + border: none; + font-family: inherit; + white-space: nowrap; + max-width: 100%; +} + +.admin-badge-wrap { + white-space: normal; + word-break: break-word; + border-radius: var(--border-radius-sm); + text-align: left; +} + +.admin-badge-admin { + background: var(--accent-soft); + color: var(--accent-color); +} + +.admin-badge-viewer { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.admin-badge-active { + background: var(--success-soft); + color: var(--success); + cursor: pointer; + transition: var(--transition); +} + +.admin-badge-active:hover { + background: color-mix(in srgb, var(--success) 20%, transparent); +} + +.admin-badge-inactive { + background: var(--danger-soft); + color: var(--danger); + cursor: pointer; + transition: var(--transition); +} + +.admin-badge-inactive:hover { + background: color-mix(in srgb, var(--danger) 20%, transparent); +} + +.admin-badge-success { + background: var(--success-soft); + color: var(--success); +} + +.admin-badge-warning { + background: var(--warning-soft); + color: var(--warning); +} + +.admin-badge-secondary { + background: var(--bg-tertiary); + color: var(--text-muted); +} + +.admin-badge-info { + background: var(--info-soft); + color: var(--info); +} + +.admin-badge-danger { + background: var(--danger-soft); + color: var(--danger); +} + +/* Status Badges - Leave Requests */ +.badge-pending { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); } +.badge-approved { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } +.badge-rejected { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } +.badge-cancelled { background: var(--muted-light); color: var(--muted); } + +/* Status Badges - Orders */ +.admin-badge-order-prijata { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info); } +.admin-badge-order-realizace { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); } +.admin-badge-order-dokoncena { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } +.admin-badge-order-stornovana { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } + +/* Status Badges - Projects */ +.admin-badge-project-aktivni { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } +.admin-badge-project-dokonceny { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info); } +.admin-badge-project-zruseny { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } + +/* ============================================================================ + Modals + ============================================================================ */ + +.admin-modal-overlay { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + overflow: hidden; + overscroll-behavior: none; + touch-action: none; +} + +.admin-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.6); + touch-action: none; +} + +.admin-modal { + position: relative; + width: 100%; + max-width: 480px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + max-height: calc(100vh - 2rem); + max-height: calc(100dvh - 2rem); + overflow: hidden; + display: flex; + flex-direction: column; + touch-action: auto; +} + +.admin-modal-lg { + max-width: 900px; +} + +.admin-modal-header { + padding: 18px; + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.admin-modal-title { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.admin-modal-body { + padding: 18px; + overflow-y: auto; + flex: 1; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + background: var(--bg-primary); +} + +.admin-modal-footer { + padding: 14px 18px; + border-top: 1px solid var(--border-color); + display: flex; + gap: 0.75rem; + justify-content: flex-end; + flex-shrink: 0; +} + +@media (max-width: 768px) { + .admin-modal-overlay { + padding: 0; + } + + .admin-modal, + .admin-modal.admin-modal-lg { + max-width: 100%; + width: 100%; + height: 100%; + height: 100dvh; + max-height: 100%; + max-height: 100dvh; + border-radius: 0; + border: none; + } + + .admin-modal-header { + padding: 1rem; + padding-top: calc(1rem + env(safe-area-inset-top, 0px)); + } + + .admin-modal-body { + padding: 1rem; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + } + + .admin-modal-footer { + padding: 1rem; + padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + } + + .admin-modal .admin-form-input, + .admin-modal .admin-form-select, + .admin-modal .admin-form-textarea { + max-width: 100%; + } +} + +/* Confirm Modal */ +.admin-confirm-modal { + max-width: 400px; +} + +.admin-confirm-content { + text-align: center; + padding: 2rem 1.5rem; +} + +.admin-confirm-icon { + width: 56px; + height: 56px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.25rem; +} + +.admin-confirm-icon-danger { + background: var(--danger-light); + color: var(--danger); +} + +.admin-confirm-icon-warning { + background: var(--warning-light); + color: var(--warning); +} + +.admin-confirm-icon-info { + background: var(--info-light); + color: var(--info); +} + +.admin-confirm-icon-default { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.admin-confirm-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.admin-confirm-message { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.5; +} + +@media (max-width: 768px) { + .admin-confirm-modal { + max-width: 100%; + height: auto; + max-height: calc(100% - 2rem); + max-height: calc(100dvh - 2rem); + margin: 1rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + } + + .admin-confirm-modal .admin-modal-footer { + padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + } +} + +/* ============================================================================ + Toast Alerts + ============================================================================ */ + +.admin-alert-container { + position: fixed; + bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + right: 1rem; + z-index: 100; + display: flex; + flex-direction: column-reverse; + gap: 0.5rem; + max-width: 400px; + width: calc(100% - 2rem); + pointer-events: none; + transform: translateZ(0); +} + +@media (min-width: 640px) { + .admin-alert-container { + bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px)); + right: 1.5rem; + } +} + +.admin-toast { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + border-radius: var(--border-radius-sm); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + pointer-events: auto; +} + +.admin-toast-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.admin-toast-message { + flex: 1; + font-size: 0.875rem; + color: var(--text-primary); +} + +.admin-toast-close { + flex-shrink: 0; + padding: 0.25rem; + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + border-radius: var(--border-radius-sm); + transition: var(--transition); +} + +.admin-toast-close:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.admin-toast-success .admin-toast-icon { color: var(--success); } +.admin-toast-error .admin-toast-icon { color: var(--danger); } +.admin-toast-warning .admin-toast-icon { color: var(--warning); } +.admin-toast-info .admin-toast-icon { color: var(--info); } + +/* ============================================================================ + Loading & Animations + ============================================================================ */ + +.admin-spinner { + width: 32px; + height: 32px; + border: 2px solid var(--accent-color); + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.admin-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 256px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes float { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(30px, -30px); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +/* ============================================================================ + Skeleton Loading + ============================================================================ */ + +.admin-skeleton { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.5rem; + opacity: 0; + animation: skeleton-fade-in 0.15s ease 0.08s forwards; +} + +@keyframes skeleton-fade-in { + to { opacity: 1; } +} + +.admin-skeleton-row { + display: flex; + gap: 1rem; + align-items: center; +} + +.admin-skeleton-line { + height: 14px; + border-radius: 6px; + background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-color) 50%, var(--bg-tertiary) 75%); + background-size: 200% 100%; + animation: shimmer 1.2s ease-in-out infinite; +} + +.admin-skeleton-line.w-full { width: 100%; } +.admin-skeleton-line.w-3\/4 { width: 75%; } +.admin-skeleton-line.w-1\/2 { width: 50%; } +.admin-skeleton-line.w-1\/3 { width: 33%; } +.admin-skeleton-line.w-1\/4 { width: 25%; } +.admin-skeleton-line.h-8 { height: 32px; } +.admin-skeleton-line.h-10 { height: 40px; } +.admin-skeleton-line.circle { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +/* ============================================================================ + Tabs (Global) + ============================================================================ */ + +.admin-tabs { + display: inline-flex; + gap: 4px; + padding: 4px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.625rem; +} + +.admin-tab { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1.25rem; + background: transparent; + border: none; + border-radius: 0.5rem; + color: var(--text-muted); + font-size: 0.8125rem; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.admin-tab:hover { + color: var(--text-primary); +} + +.admin-tab.active { + color: var(--text-primary); + font-weight: 600; + background: var(--bg-secondary); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--border-color); +} + +/* ============================================================================ + Empty State + ============================================================================ */ + +.admin-empty-state { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 3rem 1.5rem; + color: var(--text-secondary); +} + +.admin-empty-icon { + width: 64px; + height: 64px; + border-radius: 50%; + background: var(--bg-tertiary); + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.25rem; +} + +.admin-empty-state p { + margin-bottom: 1rem; + font-size: 0.95rem; + max-width: 320px; +} + +/* Back link styles moved to login.css */ +/* Attendance styles moved to attendance.css */ + +/* Sessions/devices styles moved to dashboard.css */ + +/* Settings/permissions styles moved to settings.css */ + +.admin-role-locked-notice { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: var(--warning-light); + border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent); + border-radius: 0.5rem; + color: var(--warning); + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +/* Offers styles moved to offers.css */ + +/* Leave badges moved to leave.css */ +/* Order badges moved to orders.css */ +/* Project badges moved to projects.css */ + + +/* ============================================================================ + React DatePicker Overrides + ============================================================================ */ + +.react-datepicker-wrapper { + width: 100%; +} + +.react-datepicker-popper { + z-index: 100 !important; +} + +.react-datepicker { + font-family: inherit !important; + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + border-radius: var(--border-radius-sm) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25) !important; + color: var(--text-primary) !important; + font-size: 0.875rem !important; +} + +.react-datepicker__triangle { + display: none !important; +} + +/* Header */ +.react-datepicker__header { + background-color: var(--bg-tertiary) !important; + border-bottom: 1px solid var(--border-color) !important; + padding-top: 0.75rem !important; +} + +.react-datepicker__current-month, +.react-datepicker-time__header { + color: var(--text-primary) !important; + font-weight: 600 !important; +} + +.react-datepicker__day-name { + color: var(--text-secondary) !important; +} + +/* Days */ +.react-datepicker__day { + color: var(--text-primary) !important; + border-radius: 6px !important; + transition: background 0.15s, color 0.15s !important; +} + +.react-datepicker__day:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__day--selected, +.react-datepicker__day--keyboard-selected { + background-color: var(--accent-color) !important; + color: #fff !important; +} + +.react-datepicker__day--today { + font-weight: 700 !important; +} + +.react-datepicker__day--outside-month { + color: var(--text-muted) !important; + opacity: 0.5; +} + +.react-datepicker__day--disabled { + color: var(--text-muted) !important; + opacity: 0.3 !important; +} + +/* Navigation arrows */ +.react-datepicker__navigation { + top: 0.75rem !important; +} + +.react-datepicker__navigation-icon::before { + border-color: var(--text-secondary) !important; +} + +.react-datepicker__navigation:hover *::before { + border-color: var(--accent-color) !important; +} + +/* Year dropdown */ +.react-datepicker__year-dropdown, +.react-datepicker__month-dropdown, +.react-datepicker__year-read-view, +.react-datepicker__month-read-view { + color: var(--text-primary) !important; +} + +/* Time picker */ +.react-datepicker__time-container { + border-left: 1px solid var(--border-color) !important; +} + +.react-datepicker__time-container .react-datepicker__time { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box { + width: 100% !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item { + color: var(--text-primary) !important; + transition: background 0.15s !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected { + background-color: var(--accent-color) !important; + color: #fff !important; + font-weight: 600 !important; +} + +/* Month picker */ +.react-datepicker__monthPicker { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker-year-header { + background-color: var(--bg-tertiary) !important; + color: var(--text-primary) !important; + border-bottom: 1px solid var(--border-color) !important; +} + +.react-datepicker__month-wrapper { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker__month-text { + color: var(--text-primary) !important; + padding: 0.5rem !important; + border-radius: 6px !important; + transition: background 0.15s !important; + background-color: transparent !important; +} + +.react-datepicker__month-text:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__month-text--keyboard-selected, +.react-datepicker__month-text--selected { + background-color: var(--accent-color) !important; + color: #fff !important; +} + +.react-datepicker__month-text--today { + font-weight: 700 !important; +} + +/* Input */ +.react-datepicker__input-container input { + cursor: pointer; +} + +.react-datepicker__close-icon::after { + background-color: var(--accent-color) !important; +} + +/* Forbidden (403) */ + +.forbidden-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + text-align: center; + padding: 2rem; +} + +.forbidden-icon { + color: var(--accent-color); + margin-bottom: 1.5rem; + opacity: 0.8; +} + +.forbidden-title { + font-family: var(--font-heading); + font-size: 2rem; + font-weight: 700; + color: var(--text-primary); + margin: 0 0 0.75rem; +} + +.forbidden-text { + color: var(--text-secondary); + font-size: 1rem; + max-width: 400px; + line-height: 1.6; + margin: 0 0 2rem; +} + +.forbidden-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + background: var(--accent-color); + color: #fff; + border-radius: var(--border-radius-sm); + text-decoration: none; + font-weight: 600; + transition: var(--transition); +} + +.forbidden-link:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +/* ============================================================================ + Mobile Responsive Enhancements + ============================================================================ */ + +/* Touch targets - min 44px na mobilech */ +@media (max-width: 768px) { + .admin-btn { + min-height: 44px; + padding: 10px 16px; + } + + .admin-btn-sm { + min-height: 36px; + } + + .admin-btn-icon { + min-width: 44px; + min-height: 44px; + } + + .admin-form-input, + .admin-form-select, + .admin-form-textarea { + min-height: 44px; + font-size: 16px; /* zabrání auto-zoomu na iOS */ + } + + .admin-form-checkbox { + min-height: 44px; + padding: 8px 0; + } + + .admin-form-checkbox input + span::before { + width: 20px; + height: 20px; + } + + .admin-form-label { + font-size: 13px; + } +} + +/* Tabulky - kompaktnejsi na mobilech, lepsi scroll indikace */ +@media (max-width: 640px) { + .admin-table-wrapper, + .admin-table-responsive { + margin: 0 -1rem; + padding: 0 1rem; + position: relative; + } + + .admin-table { + min-width: 500px; + } + + .admin-table th, + .admin-table td { + padding: 8px; + font-size: 11px; + } + + .admin-table th { + font-size: 9px; + } + + .admin-table-actions { + gap: 0.25rem; + } +} + +/* Page header na mobilech */ +@media (max-width: 480px) { + .admin-page-title { + font-size: 18px; + } + + .admin-page-subtitle { + font-size: 12px; + } + + .admin-content { + padding: 12px !important; + padding-top: calc(73px + 12px + env(safe-area-inset-top, 0px)) !important; + } + + .admin-card-body { + padding: 12px; + } + + .admin-card-header { + padding: 12px; + } +} + +/* Grid - single column na malych mobilech */ +@media (max-width: 480px) { + .admin-grid-4 { + grid-template-columns: 1fr; + } +} + +/* Confirm modal - ne fullscreen na mobilech */ +@media (max-width: 480px) { + .admin-confirm-content { + padding: 1.5rem 1rem; + } + + .admin-confirm-title { + font-size: 1.1rem; + } + + .admin-confirm-message { + font-size: 0.875rem; + } +} + +/* Skeleton loading na mobilech */ +@media (max-width: 640px) { + .admin-skeleton { + border-radius: 4px; + } +} + +/* Badge na mobilech - vetsi pro touch */ +@media (max-width: 768px) { + .admin-badge { + padding: 4px 10px; + font-size: 12px; + } + + button.admin-badge { + min-height: 32px; + } +} + +/* Prefers reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* Drag handle */ +.admin-drag-handle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: none; + color: var(--text-muted); + cursor: grab; + border-radius: 4px; + padding: 0; + transition: color 0.15s, background 0.15s; + touch-action: none; +} + +.admin-drag-handle:hover { + color: var(--text-primary); + background: var(--bg-secondary); +} + +.admin-drag-handle:active { + cursor: grabbing; +} + +/* ============================================================================ + Pagination + ============================================================================ */ + +.admin-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin-top: 0.5rem; + border-top: 1px solid var(--border-color); + font-size: 13px; +} + +.admin-pagination-info { + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 12px; + white-space: nowrap; +} + +.admin-pagination-controls { + display: flex; + align-items: center; + gap: 2px; +} + +.admin-pagination-page { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + padding: 0 6px; + border: 1px solid transparent; + border-radius: var(--border-radius-sm); + background: none; + color: var(--text-secondary); + font-size: 13px; + font-family: var(--font-mono); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.admin-pagination-page:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.admin-pagination-page.active { + background: var(--accent-color); + color: #fff; + border-color: var(--accent-color); + font-weight: 600; +} + +.admin-pagination-ellipsis { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + color: var(--text-muted); + font-size: 14px; +} + +.admin-pagination-select { + padding: 4px 8px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + background: var(--bg-primary); + color: var(--text-secondary); + font-size: 12px; + cursor: pointer; +} + +@media (max-width: 640px) { + .admin-pagination { + flex-wrap: wrap; + gap: 0.5rem; + } + + .admin-pagination-info { + order: 2; + width: 100%; + text-align: center; + } +} + +/* Error stack (DEV only) */ +.admin-error-stack { + max-width: 600px; + max-height: 200px; + overflow: auto; + padding: 0.75rem 1rem; + margin: 0; + border-radius: var(--border-radius-sm); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--danger-color); + font-family: var(--font-mono); + font-size: 11px; + line-height: 1.5; + text-align: left; + white-space: pre-wrap; + word-break: break-word; +} + +/* Keyboard shortcut badge */ +.admin-kbd { + display: inline-block; + padding: 2px 7px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.4; + border-radius: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + white-space: nowrap; +} + +/* ============================================================================ + File Manager + ============================================================================ */ + +.fm-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.75rem; + flex-wrap: wrap; +} + +.fm-full-path { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-tertiary); + user-select: all; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.fm-toolbar-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.fm-breadcrumb { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0; + font-size: 12px; + min-height: 28px; +} + +.fm-breadcrumb-segment { + display: inline-flex; + align-items: center; +} + +.fm-breadcrumb-sep { + color: var(--text-tertiary); + margin: 0 4px; + user-select: none; +} + +.fm-breadcrumb-btn { + background: none; + border: none; + padding: 2px 6px; + border-radius: 4px; + color: var(--text-secondary); + cursor: pointer; + font-family: var(--font-mono); + font-size: 12px; + transition: all 0.15s ease; +} + +.fm-breadcrumb-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.fm-breadcrumb-btn.active { + color: var(--text-primary); + font-weight: 600; +} + +.fm-new-folder { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.75rem; +} + +.fm-new-folder .admin-form-input { + max-width: 250px; +} + +.fm-content { + position: relative; + border-radius: var(--border-radius-sm); + transition: border-color 0.2s ease; +} + +.fm-content.fm-drag-over { + border: 2px dashed var(--accent-color); + background: var(--accent-light); +} + +.fm-dropzone-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: color-mix(in srgb, var(--bg-primary) 90%, transparent); + border-radius: var(--border-radius-sm); + z-index: 5; + color: var(--accent-color); + font-size: 13px; + font-weight: 500; + pointer-events: none; +} + +.fm-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2.5rem 1rem; + color: var(--text-tertiary); + font-size: 13px; +} + +.fm-folder-link { + background: none; + border: none; + padding: 0; + color: var(--accent-color); + font-weight: 500; + font-size: inherit; + font-family: inherit; + cursor: pointer; +} + +.fm-folder-link:hover { + text-decoration: underline; +} + +.fm-item-count { + font-size: 10px; + color: var(--text-tertiary); + font-weight: 400; +} + +.fm-file-name { + color: var(--text-primary); +} + +.fm-meta { + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 11px; +} + +.fm-actions { + display: inline-flex; + gap: 2px; + justify-content: flex-end; +} + +.fm-name-cell { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.fm-symlink-badge { + display: inline-flex; + align-items: center; + color: var(--text-tertiary); + cursor: help; +} + +/* ============================================= + React DatePicker Overrides + ============================================= */ + +.react-datepicker-wrapper { + width: 100%; +} + +.react-datepicker-popper { + z-index: 100 !important; +} + +/* Prevent flash at top-left before popper calculates position */ +#datepicker-portal .react-datepicker-popper { + opacity: 0; + animation: dp-fade-in 0.01s forwards 0.02s; +} + +@keyframes dp-fade-in { + to { opacity: 1; } +} + +.react-datepicker { + font-family: inherit !important; + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + border-radius: var(--border-radius-sm) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25) !important; + color: var(--text-primary) !important; + font-size: 0.875rem !important; +} + +.react-datepicker__triangle { + display: none !important; +} + +.react-datepicker__header { + background-color: var(--bg-tertiary) !important; + border-bottom: 1px solid var(--border-color) !important; + padding-top: 0.75rem !important; +} + +.react-datepicker__current-month, +.react-datepicker-time__header { + color: var(--text-primary) !important; + font-weight: 600 !important; +} + +.react-datepicker__day-name { + color: var(--text-secondary) !important; +} + +.react-datepicker__day { + color: var(--text-primary) !important; + border-radius: 6px !important; + transition: background 0.15s, color 0.15s !important; +} + +.react-datepicker__day:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__day--selected, +.react-datepicker__day--keyboard-selected { + background-color: var(--accent-color) !important; + color: #fff !important; +} + +.react-datepicker__day--today { + font-weight: 700 !important; +} + +.react-datepicker__day--outside-month { + color: var(--text-muted) !important; + opacity: 0.5; +} + +.react-datepicker__day--disabled { + color: var(--text-muted) !important; + opacity: 0.3 !important; +} + +.react-datepicker__navigation { + top: 0.75rem !important; +} + +.react-datepicker__navigation-icon::before { + border-color: var(--text-secondary) !important; +} + +.react-datepicker__navigation:hover *::before { + border-color: var(--accent-color) !important; +} + +.react-datepicker__year-dropdown, +.react-datepicker__month-dropdown, +.react-datepicker__year-read-view, +.react-datepicker__month-read-view { + color: var(--text-primary) !important; +} + +.react-datepicker__time-container { + border-left: 1px solid var(--border-color) !important; +} + +.react-datepicker__time-container .react-datepicker__time { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box { + width: 100% !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item { + color: var(--text-primary) !important; + transition: background 0.15s !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected { + background-color: var(--accent-color) !important; + color: #fff !important; + font-weight: 600 !important; +} + +.react-datepicker__monthPicker { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker-year-header { + background-color: var(--bg-tertiary) !important; + color: var(--text-primary) !important; + border-bottom: 1px solid var(--border-color) !important; +} + +.react-datepicker__month-wrapper { + background-color: var(--bg-secondary) !important; +} + +.react-datepicker__month-text { + color: var(--text-primary) !important; + padding: 0.5rem !important; + border-radius: 6px !important; + transition: background 0.15s !important; + background-color: transparent !important; +} + +.react-datepicker__month-text:hover { + background-color: var(--accent-light) !important; + color: var(--text-primary) !important; +} + +.react-datepicker__month-text--keyboard-selected, +.react-datepicker__month-text--selected { + background-color: var(--accent-color) !important; + color: #fff !important; +} + +.react-datepicker__month-text--today { + font-weight: 700 !important; +} + +.react-datepicker__input-container input { + cursor: pointer; +} + +.react-datepicker__close-icon::after { + background-color: var(--accent-color) !important; +} + diff --git a/src/admin/attendance.css b/src/admin/attendance.css new file mode 100644 index 0000000..3928d48 --- /dev/null +++ b/src/admin/attendance.css @@ -0,0 +1,434 @@ +/* ============================================================================ + Attendance Module + ============================================================================ */ + +/* Layout */ +.attendance-layout { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +@media (min-width: 1024px) { + .attendance-layout { + flex-direction: row; + align-items: flex-start; + } +} + +.attendance-main { + flex: 1; + min-width: 0; +} + +.attendance-sidebar { + display: flex; + flex-direction: column; + gap: 1rem; +} + +@media (min-width: 1024px) { + .attendance-sidebar { + width: 320px; + flex-shrink: 0; + } +} + +/* Clock Card */ +.attendance-clock-card { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: var(--border-radius); + padding: 2rem; +} + +.attendance-clock-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.attendance-clock-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + font-weight: 500; + color: var(--text-secondary); +} + +.attendance-status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--text-muted); +} + +.attendance-status-dot.active { + background: var(--success); + box-shadow: 0 0 8px color-mix(in srgb, var(--success) 50%, transparent); + animation: pulse 2s ease-in-out infinite; +} + +.attendance-clock-time { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-primary); + font-family: var(--font-heading); +} + +/* Shift Info */ +.attendance-shift-info { + margin-bottom: 2rem; +} + +.attendance-shift-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +@media (max-width: 640px) { + .attendance-shift-row { + grid-template-columns: 1fr; + gap: 0.75rem; + } +} + +.attendance-shift-item { + text-align: center; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: var(--border-radius-sm); +} + +.attendance-shift-label { + display: block; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.attendance-shift-value { + display: block; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-secondary); +} + +.attendance-shift-value.success { + color: var(--success); +} + +/* Clock Actions */ +.attendance-clock-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* Notes */ +.attendance-notes { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.attendance-notes-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +/* Project Section */ +.attendance-project-section { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: var(--border-radius-sm); +} + +.attendance-project-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.attendance-project-header .attendance-shift-label { + margin-bottom: 0; +} + +.attendance-project-section .admin-form-select { + margin-bottom: 0; +} + +.attendance-project-logs { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border-color); + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.attendance-project-log-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + padding: 0.375rem 0.5rem; + background: var(--bg-secondary); + border-radius: var(--border-radius-sm); +} + +.attendance-project-log-name { + font-weight: 500; + color: var(--text-primary); + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.attendance-project-log-time { + color: var(--text-muted); + white-space: nowrap; + font-size: 0.75rem; +} + +.attendance-project-log-duration { + color: var(--text-secondary); + font-weight: 600; + white-space: nowrap; + font-variant-numeric: tabular-nums; +} + +/* Balance Card */ +.attendance-balance-card { + background: var(--gradient); + border-radius: var(--border-radius); + padding: 1.5rem; + color: #fff; +} + +.attendance-balance-title { + font-size: 0.875rem; + font-weight: 500; + opacity: 0.9; + margin-bottom: 0.75rem; + color: inherit; +} + +.attendance-balance-value { + display: flex; + align-items: baseline; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.attendance-balance-number { + font-size: 3rem; + font-weight: 700; + line-height: 1; + color: inherit; +} + +.attendance-balance-unit { + font-size: 1rem; + opacity: 0.9; +} + +.attendance-balance-detail { + display: flex; + justify-content: space-between; + font-size: 0.8125rem; + opacity: 0.8; + margin-bottom: 0.75rem; +} + +.attendance-balance-bar { + height: 6px; + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + overflow: hidden; +} + +.attendance-balance-progress { + height: 100%; + background: rgba(255, 255, 255, 0.9); + border-radius: 3px; + transition: width 0.3s ease; +} + +/* Quick Links */ +.attendance-quick-links { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: var(--border-radius); + padding: 1rem; +} + +.attendance-quick-title { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: 0.75rem; +} + +.attendance-quick-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + margin: -0.25rem -0.5rem; + border-radius: var(--border-radius-sm); + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition); +} + +.attendance-quick-link:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.attendance-quick-link span { + flex: 1; +} + +.attendance-quick-link svg:last-child { + opacity: 0.5; +} + +/* Leave Type Badges */ +.attendance-leave-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: var(--border-radius-sm); + font-size: 0.75rem; + font-weight: 500; + margin-right: 0.25rem; + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.attendance-leave-badge.badge-vacation { + background: color-mix(in srgb, var(--info) 15%, transparent); + color: var(--info); +} + +.attendance-leave-badge.badge-sick { + background: color-mix(in srgb, var(--danger) 15%, transparent); + color: var(--danger); +} + +.attendance-leave-badge.badge-holiday { + background: color-mix(in srgb, var(--success) 15%, transparent); + color: var(--success); +} + +.attendance-leave-badge.badge-unpaid { + background: var(--muted-light); + color: var(--muted); +} + +/* Working Status Badge */ +.attendance-working-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + font-size: 0.75rem; + font-weight: 600; +} + +.attendance-working-badge.working { + background: color-mix(in srgb, var(--success) 15%, transparent); + color: var(--success); +} + +.attendance-working-badge.finished { + background: color-mix(in srgb, var(--danger) 15%, transparent); + color: var(--danger); +} + +/* GPS Link */ +.attendance-gps-link { + text-decoration: none; + font-size: 1rem; +} + +.attendance-gps-link:hover { + transform: scale(1.1); +} + +/* Location Page */ +.attendance-location-map { + height: 400px; + border-radius: var(--border-radius-sm); + margin-bottom: 1.5rem; + background: var(--bg-secondary); + position: relative; + z-index: 0; /* stacking context - Leaflet z-indexy zustanou uvnitr */ +} + +.attendance-location-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; +} + +.attendance-location-card { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + padding: 1rem; +} + +.attendance-location-card.empty { + opacity: 0.6; +} + +.attendance-location-title { + font-size: 1rem; + margin-bottom: 0.5rem; + color: var(--accent-color); + font-weight: 600; +} + +.attendance-location-time { + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.attendance-location-address { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 0.5rem; + line-height: 1.4; +} + +.attendance-location-coords { + font-size: 0.8rem; + color: var(--text-muted); + font-family: monospace; +} diff --git a/src/admin/components/AdminDatePicker.tsx b/src/admin/components/AdminDatePicker.tsx new file mode 100644 index 0000000..5066b45 --- /dev/null +++ b/src/admin/components/AdminDatePicker.tsx @@ -0,0 +1,185 @@ +import { forwardRef, useMemo } from 'react' +import DatePicker, { registerLocale } from 'react-datepicker' +import { cs } from 'date-fns/locale' +import { parse, format } from 'date-fns' +import 'react-datepicker/dist/react-datepicker.css' + +registerLocale('cs', cs) + +// Ensure portal root exists +if (typeof document !== 'undefined' && !document.getElementById('datepicker-portal')) { + const el = document.createElement('div') + el.id = 'datepicker-portal' + document.body.appendChild(el) +} + +const isTouchDevice = () => + typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0) + +interface CustomInputProps { + value?: string + onClick?: () => void + onChange?: (e: React.ChangeEvent) => void + placeholder?: string + required?: boolean + readOnly?: boolean + disabled?: boolean +} + +const CustomInput = forwardRef( + ({ value, onClick, onChange, placeholder, required, readOnly, disabled }, ref) => ( + + ) +) + +interface NativeInputProps { + mode: string + value: string + onChange: (value: string) => void + required?: boolean + minDate?: string + maxDate?: string + disabled?: boolean +} + +const modeToInputType: Record = { month: 'month', time: 'time' } + +function NativeInput({ mode, value, onChange, required, minDate, maxDate, disabled }: NativeInputProps) { + const type = modeToInputType[mode] || 'date' + return ( + onChange(e.target.value)} + className="admin-form-input" + required={required} + disabled={disabled} + min={minDate || undefined} + max={maxDate || undefined} + /> + ) +} + +interface AdminDatePickerProps { + mode?: 'date' | 'month' | 'datetime' | 'time' + value: string + onChange: (value: string) => void + minDate?: string + maxDate?: string + disabled?: boolean + placeholder?: string + required?: boolean +} + +export default function AdminDatePicker({ + mode = 'date', + value, + onChange, + required, + minDate, + maxDate, + disabled, + placeholder, +}: AdminDatePickerProps) { + const useNative = useMemo(() => isTouchDevice(), []) + + if (useNative) { + return ( + + ) + } + + const toDate = (val: string | null | undefined): Date | null => { + if (!val) return null + try { + if (mode === 'date') return parse(val, 'yyyy-MM-dd', new Date()) + if (mode === 'time') { + const [h, m] = val.split(':') + const d = new Date() + d.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0) + return d + } + if (mode === 'month') return parse(val, 'yyyy-MM', new Date()) + } catch { return null } + return null + } + + const handleChange = (date: Date | null) => { + if (!date) { onChange(''); return } + if (mode === 'date') onChange(format(date, 'yyyy-MM-dd')) + else if (mode === 'time') onChange(format(date, 'HH:mm')) + else if (mode === 'month') onChange(format(date, 'yyyy-MM')) + } + + const parseMinMax = (val: string | undefined): Date | undefined => { + if (!val) return undefined + try { + if (mode === 'date') return parse(val, 'yyyy-MM-dd', new Date()) + if (mode === 'month') return parse(val, 'yyyy-MM', new Date()) + } catch { return undefined } + return undefined + } + + const commonProps = { + selected: toDate(value), + onChange: handleChange, + locale: 'cs', + customInput: , + minDate: parseMinMax(minDate), + maxDate: parseMinMax(maxDate), + popperPlacement: 'bottom-start' as const, + portalId: 'datepicker-portal', + disabled, + } + + if (mode === 'time') { + return ( + + ) + } + + if (mode === 'month') { + return ( + + ) + } + + return ( + + ) +} diff --git a/src/admin/components/AdminLayout.tsx b/src/admin/components/AdminLayout.tsx new file mode 100644 index 0000000..5ec334e --- /dev/null +++ b/src/admin/components/AdminLayout.tsx @@ -0,0 +1,107 @@ +import { useState, useCallback } from 'react' +import { Outlet, Navigate, useLocation } from 'react-router-dom' +import { motion } from 'framer-motion' +import { useAuth } from '../context/AuthContext' +import { useTheme } from '../../context/ThemeContext' +import { setLogoutAlert } from '../utils/api' +import useModalLock from '../hooks/useModalLock' +import Sidebar from './Sidebar' +import ShortcutsHelp from './ShortcutsHelp' + +export default function AdminLayout() { + const { isAuthenticated, loading, user, logout } = useAuth() + const { theme, toggleTheme } = useTheme() + const [sidebarOpen, setSidebarOpen] = useState(false) + const [loggingOut, setLoggingOut] = useState(false) + const location = useLocation() + + // Session is managed by AuthProvider (initial check + proactive refresh via setTimeout). + // Do not call checkSession on route changes — concurrent refresh calls with token rotation + // would invalidate each other and kick the user out. + + const handleLogout = useCallback(() => { + setLoggingOut(true) + setSidebarOpen(false) + setLogoutAlert() + setTimeout(() => logout(), 400) + }, [logout]) + + useModalLock(sidebarOpen) + + if (loading) { + return ( +
+
+
+
+
+ ) + } + + if (!isAuthenticated) { + return + } + + // If 2FA is required but user hasn't enabled it, redirect to dashboard (where setup lives) + const needs2FASetup = user?.require2FA && !user?.totpEnabled + if (needs2FASetup && location.pathname !== '/') { + return + } + + return ( + + setSidebarOpen(false)} onLogout={handleLogout} /> + +
+
+ + +
+ + + +
+ +
+ +
+
+ +
+ ) +} diff --git a/src/admin/components/AlertContainer.tsx b/src/admin/components/AlertContainer.tsx new file mode 100644 index 0000000..fc5d14e --- /dev/null +++ b/src/admin/components/AlertContainer.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { useAlertState } from '../context/AlertContext' + +const icons: Record = { + success: ( + + + + + ), + error: ( + + + + + + ), + warning: ( + + + + + + ), + info: ( + + + + + + ), +} + +export default function AlertContainer() { + const { alerts, removeAlert } = useAlertState() + + return ( +
+ + {alerts.map(alert => ( + + {icons[alert.type]} + {alert.message} + + + ))} + +
+ ) +} diff --git a/src/admin/components/AttendanceShiftTable.tsx b/src/admin/components/AttendanceShiftTable.tsx new file mode 100644 index 0000000..c87fda1 --- /dev/null +++ b/src/admin/components/AttendanceShiftTable.tsx @@ -0,0 +1,181 @@ +import { Link } from 'react-router-dom' +import { + formatDate, formatDatetime, formatTime, + calculateWorkMinutes, formatMinutes, + getLeaveTypeName, getLeaveTypeBadgeClass +} from '../utils/attendanceHelpers' + +interface ProjectLog { + id?: number + project_id?: number + project_name?: string + started_at?: string + ended_at?: string | null + hours?: string | number | null + minutes?: string | number | null +} + +interface AttendanceRecord { + id: number + shift_date: string + user_name: string + leave_type?: string + leave_hours?: number + arrival_time?: string | null + departure_time?: string | null + break_start?: string | null + break_end?: string | null + arrival_lat?: number | string | null + arrival_lng?: number | string | null + departure_lat?: number | string | null + departure_lng?: number | string | null + project_name?: string + project_logs?: ProjectLog[] + notes?: string | null +} + +interface AttendanceShiftTableProps { + records: AttendanceRecord[] + onEdit: (record: AttendanceRecord) => void + onDelete: (record: AttendanceRecord) => void +} + +function formatBreak(record: AttendanceRecord): string { + if (record.break_start && record.break_end) { + return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}` + } + if (record.break_start) { + return `${formatTime(record.break_start)} - ?` + } + return '\u2014' +} + +function renderProjectCell(record: AttendanceRecord): React.ReactNode { + if (record.project_logs && record.project_logs.length > 0) { + return ( +
+ {record.project_logs.map((log, i) => { + let h: number, m: number, isActive = false + if (log.hours !== null && log.hours !== undefined) { + h = parseInt(String(log.hours)) || 0 + m = parseInt(String(log.minutes)) || 0 + } else { + isActive = !log.ended_at + const end = log.ended_at ? new Date(log.ended_at) : new Date() + const mins = Math.floor((end.getTime() - new Date(log.started_at!).getTime()) / 60000) + h = Math.floor(mins / 60) + m = mins % 60 + } + return ( + + {log.project_name || `#${log.project_id}`} ({h}:{String(m).padStart(2, '0')}h{isActive ? ' \u25B8' : ''}) + + ) + })} +
+ ) + } + if (record.project_name) { + return {record.project_name} + } + return '\u2014' +} + +export default function AttendanceShiftTable({ records, onEdit, onDelete }: AttendanceShiftTableProps) { + if (records.length === 0) { + return ( +
+

Za tento měsíc nejsou žádné záznamy.

+
+ ) + } + + return ( +
+ + + + + + + + + + + + + + + + + + {records.map((record) => { + const leaveType = record.leave_type || 'work' + const isLeave = leaveType !== 'work' + const workMinutes = isLeave + ? (Number(record.leave_hours) || 8) * 60 + : calculateWorkMinutes(record) + const hasLocation = (record.arrival_lat && record.arrival_lng) || (record.departure_lat && record.departure_lng) + + return ( + + + + + + + + + + + + + + ) + })} + +
DatumZam\u011BstnanecTypP\u0159\u00EDchodPauzaOdchodHodinyProjektGPSPozn\u00E1mkaAkce
{formatDate(record.shift_date)}{record.user_name} + + {getLeaveTypeName(leaveType)} + + {isLeave ? '\u2014' : formatDatetime(record.arrival_time)} + {isLeave ? '\u2014' : formatBreak(record)} + {isLeave ? '\u2014' : formatDatetime(record.departure_time)}{workMinutes > 0 ? `${formatMinutes(workMinutes)} h` : '\u2014'} + {renderProjectCell(record)} + + {hasLocation ? ( + + {'\uD83D\uDCCD'} + + ) : '\u2014'} + + {record.notes || ''} + +
+ + +
+
+
+ ) +} diff --git a/src/admin/components/BulkAttendanceModal.tsx b/src/admin/components/BulkAttendanceModal.tsx new file mode 100644 index 0000000..8eb0a05 --- /dev/null +++ b/src/admin/components/BulkAttendanceModal.tsx @@ -0,0 +1,192 @@ +import { motion, AnimatePresence } from 'framer-motion' +import AdminDatePicker from './AdminDatePicker' +import useModalLock from '../hooks/useModalLock' + +interface BulkAttendanceForm { + month: string + user_ids: string[] + arrival_time: string + departure_time: string + break_start_time: string + break_end_time: string +} + +interface BulkAttendanceUser { + id: number | string + name: string +} + +interface BulkAttendanceModalProps { + show: boolean + onClose: () => void + form: BulkAttendanceForm + setForm: (form: BulkAttendanceForm) => void + users: BulkAttendanceUser[] + onSubmit: () => void + submitting: boolean + toggleUser: (userId: number | string) => void + toggleAllUsers: () => void +} + +export default function BulkAttendanceModal({ + show, + onClose, + form, + setForm, + users, + onSubmit, + submitting, + toggleUser, + toggleAllUsers, +}: BulkAttendanceModalProps) { + useModalLock(show) + + return ( + + {show && ( + +
!submitting && onClose()} /> + +
+

Vyplnit docházku za měsíc

+

+ Vytvoří záznamy pro všechny pracovní dny. Svátky se automaticky označí. Existující záznamy se přeskočí. +

+
+ +
+
+
+ + setForm({ ...form, month: val })} + /> +
+ +
+ +
+ {users.map((user) => ( + + ))} +
+ + Vybráno: {form.user_ids.length} z {users.length} + +
+ +
+
+ + setForm({ ...form, arrival_time: val })} + /> +
+
+ + setForm({ ...form, departure_time: val })} + /> +
+
+ +
+
+ + setForm({ ...form, break_start_time: val })} + /> +
+
+ + setForm({ ...form, break_end_time: val })} + /> +
+
+
+
+ +
+ + +
+
+ + )} + + ) +} diff --git a/src/admin/components/ConfirmModal.tsx b/src/admin/components/ConfirmModal.tsx new file mode 100644 index 0000000..24f7c44 --- /dev/null +++ b/src/admin/components/ConfirmModal.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react' +import { motion, AnimatePresence } from 'framer-motion' + +interface ConfirmModalProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void + title: string + message: ReactNode + confirmText?: string + cancelText?: string + type?: 'danger' | 'warning' | 'default' | 'info' + confirmVariant?: 'danger' | 'primary' + loading?: boolean +} + +export default function ConfirmModal({ isOpen, onClose, onConfirm, title, message, confirmText = 'Potvrdit', cancelText = 'Zrušit', type = 'default', confirmVariant, loading }: ConfirmModalProps) { + return ( + + {isOpen && ( + +
+ +
+
+ + + + + +
+

{title}

+

{message}

+
+
+ + +
+
+ + )} + + ) +} diff --git a/src/admin/components/ErrorBoundary.tsx b/src/admin/components/ErrorBoundary.tsx new file mode 100644 index 0000000..e91e24a --- /dev/null +++ b/src/admin/components/ErrorBoundary.tsx @@ -0,0 +1,29 @@ +import { Component, type ReactNode, type ErrorInfo } from 'react' + +interface Props { children: ReactNode } +interface State { hasError: boolean; error: Error | null } + +export default class ErrorBoundary extends Component { + state: State = { hasError: false, error: null } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('ErrorBoundary caught:', error, info) + } + + render() { + if (this.state.hasError) { + return ( +
+

Něco se pokazilo

+

{this.state.error?.message}

+ +
+ ) + } + return this.props.children + } +} diff --git a/src/admin/components/Forbidden.tsx b/src/admin/components/Forbidden.tsx new file mode 100644 index 0000000..0d52a74 --- /dev/null +++ b/src/admin/components/Forbidden.tsx @@ -0,0 +1,11 @@ +import { Link } from 'react-router-dom' + +export default function Forbidden() { + return ( +
+

403

+

Nemáte oprávnění pro přístup k této stránce.

+ Zpět na Dashboard +
+ ) +} diff --git a/src/admin/components/FormField.tsx b/src/admin/components/FormField.tsx new file mode 100644 index 0000000..bbc9304 --- /dev/null +++ b/src/admin/components/FormField.tsx @@ -0,0 +1,22 @@ +import type { CSSProperties, ReactNode } from 'react' + +interface FormFieldProps { + label: ReactNode + children: ReactNode + error?: string + required?: boolean + style?: React.CSSProperties +} + +export default function FormField({ label, children, error, required, style }: FormFieldProps) { + return ( +
+ + {children} + {error && {error}} +
+ ) +} diff --git a/src/admin/components/Pagination.tsx b/src/admin/components/Pagination.tsx new file mode 100644 index 0000000..1ccbb65 --- /dev/null +++ b/src/admin/components/Pagination.tsx @@ -0,0 +1,62 @@ +interface PaginationProps { + pagination: { + total: number + page: number + per_page: number + total_pages: number + } | null + onPageChange: (page: number) => void + onPerPageChange?: (perPage: number) => void +} + +export default function Pagination({ pagination, onPageChange, onPerPageChange }: PaginationProps) { + if (!pagination || pagination.total_pages <= 1) return null + + const { page, total_pages } = pagination + + const getPages = () => { + const pages: (number | string)[] = [] + const delta = 2 + for (let i = 1; i <= total_pages; i++) { + if (i === 1 || i === total_pages || (i >= page - delta && i <= page + delta)) { + pages.push(i) + } else if (pages[pages.length - 1] !== '...') { + pages.push('...') + } + } + return pages + } + + return ( +
+
+ + {getPages().map((p, i) => + typeof p === 'string' ? ( + ... + ) : ( + + ) + )} + +
+ {onPerPageChange && ( + + )} +
+ ) +} diff --git a/src/admin/components/RichEditor.tsx b/src/admin/components/RichEditor.tsx new file mode 100644 index 0000000..9c93d85 --- /dev/null +++ b/src/admin/components/RichEditor.tsx @@ -0,0 +1,105 @@ +import { useMemo, useRef, useCallback } from 'react' +import ReactQuill from 'react-quill-new' +import 'react-quill-new/dist/quill.snow.css' + +const Quill = ReactQuill.Quill + +if (!(Quill as any).__bohaRegistered) { + const Font = Quill.import('attributors/class/font') as any + Font.whitelist = [ + 'arial', 'tahoma', 'verdana', 'georgia', 'times-new-roman', + 'courier-new', 'trebuchet-ms', 'impact', 'comic-sans-ms', + 'lucida-console', 'palatino-linotype', 'garamond' + ] + Quill.register(Font, true) + + const SizeStyle = Quill.import('attributors/style/size') as any + SizeStyle.whitelist = [ + '8px', '9px', '10px', '11px', '12px', '14px', '16px', + '18px', '20px', '24px', '28px', '32px', '36px', '48px' + ] + Quill.register(SizeStyle, true) + ;(Quill as any).__bohaRegistered = true +} + +const Font = Quill.import('attributors/class/font') as any +const SIZE_WHITELIST = [ + '8px', '9px', '10px', '11px', '12px', '14px', '16px', + '18px', '20px', '24px', '28px', '32px', '36px', '48px' +] + +const COLORS = [ + '#000000', '#1a1a1a', '#333333', '#555555', '#777777', '#999999', '#bbbbbb', '#dddddd', '#ffffff', + '#de3a3a', '#e57373', '#c62828', + '#1565c0', '#42a5f5', '#0d47a1', + '#2e7d32', '#66bb6a', '#1b5e20', + '#f57f17', '#ffca28', '#e65100', + '#6a1b9a', '#ab47bc', '#4a148c', + '#00695c', '#26a69a', '#004d40', + '#37474f', '#78909c', '#263238', +] + +const TOOLBAR = [ + [{ font: Font.whitelist }], + [{ size: SIZE_WHITELIST }], + ['bold', 'italic', 'underline', 'strike'], + [{ color: COLORS }, { background: COLORS }], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ indent: '-1' }, { indent: '+1' }], + [{ align: [] }], + ['link'], + ['clean'] +] + +const FORMATS = [ + 'font', 'size', + 'bold', 'italic', 'underline', 'strike', + 'color', 'background', + 'list', 'indent', 'align', + 'link' +] + +interface RichEditorProps { + value: string + onChange: (value: string) => void + placeholder?: string + minHeight?: string +} + +export default function RichEditor({ + value, + onChange, + placeholder = 'Obsah...', + minHeight = '120px' +}: RichEditorProps) { + const quillRef = useRef(null) + const lastValueRef = useRef(value) + + const modules = useMemo(() => ({ + toolbar: TOOLBAR, + clipboard: { + matchVisual: false, + }, + }), []) + + const handleChange = useCallback((content: string, _delta: any, source: string) => { + if (source !== 'user') return + if (content === lastValueRef.current) return + lastValueRef.current = content + onChange(content) + }, [onChange]) + + return ( +
+ +
+ ) +} diff --git a/src/admin/components/ShiftFormModal.tsx b/src/admin/components/ShiftFormModal.tsx new file mode 100644 index 0000000..8f3461d --- /dev/null +++ b/src/admin/components/ShiftFormModal.tsx @@ -0,0 +1,521 @@ +import { motion, AnimatePresence } from 'framer-motion' +import AdminDatePicker from './AdminDatePicker' +import useModalLock from '../hooks/useModalLock' +import { calcFormWorkMinutes, calcProjectMinutesTotal, formatDate } from '../utils/attendanceHelpers' + +let _logKeyCounter = 0 + +// ---------- Shared types ---------- + +export interface ShiftFormData { + user_id: string + shift_date: string + leave_type: string + leave_hours: number + arrival_date: string + arrival_time: string + break_start_date: string + break_start_time: string + break_end_date: string + break_end_time: string + departure_date: string + departure_time: string + notes: string +} + +export interface ProjectLog { + _key?: string + id?: number + project_id: string | number + hours: string | number + minutes: string | number +} + +export interface Project { + id: number | string + project_number: string + name: string +} + +export interface User { + id: number | string + name: string +} + +export interface EditingRecord { + user_name: string + shift_date: string +} + +// ---------- Sub-component props ---------- + +interface ProjectTimeStatusProps { + form: ShiftFormData + projectLogs: ProjectLog[] +} + +interface ProjectLogRowProps { + log: ProjectLog + index: number + projectList: Project[] + onUpdate: (index: number, field: string, value: string) => void + onRemove: (index: number) => void +} + +export interface ShiftFormModalProps { + mode: 'create' | 'edit' + show: boolean + onClose: () => void + onSubmit: () => void + form: ShiftFormData + setForm: (form: ShiftFormData) => void + projectLogs: ProjectLog[] + setProjectLogs: (logs: ProjectLog[]) => void + projectList: Project[] + users: User[] + onShiftDateChange: (value: string) => void + editingRecord: EditingRecord | null +} + +// ---------- ProjectTimeStatus ---------- + +function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) { + const totalWork = calcFormWorkMinutes(form) + const totalProject = calcProjectMinutesTotal(projectLogs) + const remaining = totalWork - totalProject + const hasLogs = projectLogs.some((l) => l.project_id) + + if (!hasLogs || totalWork <= 0) return null + + const isMatch = remaining === 0 + return ( +
+ Odpracováno: {Math.floor(totalWork / 60)}h {totalWork % 60}m | + Přiřazeno: {Math.floor(totalProject / 60)}h {totalProject % 60}m | + Zbývá: {Math.floor(Math.abs(remaining) / 60)}h{' '} + {Math.abs(remaining) % 60}m {remaining < 0 ? '(překročeno)' : ''} +
+ ) +} + +// ---------- ProjectLogRow ---------- + +function ProjectLogRow({ + log, + index, + projectList, + onUpdate, + onRemove, +}: ProjectLogRowProps) { + return ( +
+ + onUpdate(index, 'hours', e.target.value)} + className="admin-form-input" + style={{ width: '60px', marginBottom: 0, textAlign: 'center' }} + placeholder="h" + /> + + h + + onUpdate(index, 'minutes', e.target.value)} + className="admin-form-input" + style={{ width: '60px', marginBottom: 0, textAlign: 'center' }} + placeholder="m" + /> + + m + + +
+ ) +} + +// ---------- ShiftFormModal ---------- + +export default function ShiftFormModal({ + mode, + show, + onClose, + onSubmit, + form, + setForm, + projectLogs, + setProjectLogs, + projectList, + users, + onShiftDateChange, + editingRecord, +}: ShiftFormModalProps) { + useModalLock(show) + const isCreate = mode === 'create' + const isWorkType = form.leave_type === 'work' + + const updateField = (field: keyof ShiftFormData, value: string | number) => { + setForm({ ...form, [field]: value }) + } + + const updateProjectLog = (index: number, field: string, value: string) => { + const updated = [...projectLogs] + updated[index] = { ...updated[index], [field]: value } + setProjectLogs(updated) + } + + const removeProjectLog = (index: number) => { + setProjectLogs(projectLogs.filter((_, j) => j !== index)) + } + + const addProjectLog = () => { + setProjectLogs([ + ...projectLogs, + { _key: `log-${++_logKeyCounter}`, project_id: '', hours: '', minutes: '' }, + ]) + } + + return ( + + {show && ( + +
+ +
+

+ {isCreate ? 'Přidat záznam docházky' : 'Upravit docházku'} +

+ {!isCreate && editingRecord && ( +

+ {editingRecord.user_name} —{' '} + {formatDate(editingRecord.shift_date)} +

+ )} +
+ +
+
+ {isCreate ? ( +
+
+ + +
+
+ + onShiftDateChange(val)} + /> +
+
+ ) : ( +
+ + updateField('shift_date', val)} + /> +
+ )} + +
+ + +
+ + {!isWorkType && ( +
+ + + updateField('leave_hours', parseFloat(e.target.value)) + } + min="0.5" + max="24" + step="0.5" + className="admin-form-input" + /> + {isCreate && ( + + 8 hodin = celý den + + )} +
+ )} + + {isWorkType && ( + <> +
+
+ + + updateField('arrival_date', val) + } + /> +
+
+ + + updateField('arrival_time', val) + } + /> +
+
+ +
+
+ + + updateField('break_start_date', val) + } + /> +
+
+ + + updateField('break_start_time', val) + } + /> +
+
+ +
+
+ + + updateField('break_end_date', val) + } + /> +
+
+ + + updateField('break_end_time', val) + } + /> +
+
+ +
+
+ + + updateField('departure_date', val) + } + /> +
+
+ + + updateField('departure_time', val) + } + /> +
+
+ + )} + + {isWorkType && projectList.length > 0 && ( +
+ + + {projectLogs.map((log, i) => ( + + ))} + +
+ )} + +
+ +