From ccd3be0152a38dcd0ba35ab0a6aec72da511133b Mon Sep 17 00:00:00 2001 From: Pablo Gallego Date: Thu, 9 Jul 2026 15:58:00 +0200 Subject: [PATCH 1/2] Checkpoint ProfePlus teacher workflow release --- CHANGELOG.md | 36 + CLAUDE.md | 13 +- README.md | 148 +- package-lock.json | 538 +++--- package.json | 23 +- src/App.tsx | 3 +- src/main.tsx | 10 +- src/modules/attendance/AttendancePage.tsx | 461 ++++- src/modules/gradebook/GradebookPage.tsx | 1576 ++++++++++++----- src/modules/management/ManagementContext.tsx | 446 +++-- .../management/ManagementDatabasePage.test.ts | 1032 +++++++++++ .../management/ManagementDatabasePage.tsx | 1042 +++++++++-- .../management/ManagementSchedulePage.tsx | 26 +- .../management/ManagementStudentsPage.tsx | 413 ++++- .../management/ManagementTasksPage.tsx | 499 +++--- src/modules/management/instrumentAi.test.ts | 86 + src/modules/management/instrumentAi.ts | 195 ++ src/modules/planner/PlannerPage.tsx | 1271 ++++++------- src/modules/reports/ReportsPage.test.ts | 141 ++ src/modules/reports/ReportsPage.tsx | 1402 ++++++++++++--- src/modules/rubrics/ChecklistsSection.tsx | 789 --------- src/modules/rubrics/RubricsPage.tsx | 989 ----------- src/shared/ai/extensionRuntime.ts | 24 +- src/shared/attendance/attendance.test.ts | 17 + src/shared/attendance/attendance.ts | 15 + src/shared/db/database.ts | 131 +- src/shared/db/types.ts | 54 +- src/shared/gradebook/calculations.test.ts | 596 +++++++ src/shared/gradebook/calculations.ts | 380 ++++ .../gradebook/manualAssessments.test.ts | 126 ++ src/shared/gradebook/manualAssessments.ts | 70 + src/shared/import/studentsCsv.test.ts | 122 ++ src/shared/import/studentsCsv.ts | 164 ++ src/shared/planner/printablePlanner.test.ts | 45 + src/shared/planner/printablePlanner.ts | 90 + src/shared/planner/sessionPlan.test.ts | 48 + src/shared/planner/sessionPlan.ts | 48 + src/shared/planner/week.test.ts | 48 + src/shared/planner/week.ts | 69 + src/shared/reports/printableReports.test.ts | 54 + src/shared/reports/printableReports.ts | 133 ++ src/shared/schedule/weekDays.test.ts | 52 + src/shared/schedule/weekDays.ts | 22 + src/shared/students/followUp.test.ts | 63 + src/shared/students/followUp.ts | 68 + src/shared/ui/SidebarNav.tsx | 29 - src/shared/ui/TopTabs.tsx | 7 +- src/styles.css | 951 ++++++---- src/vite-env.d.ts | 3 + vite.config.ts | 6 +- 50 files changed, 10184 insertions(+), 4390 deletions(-) create mode 100644 src/modules/management/ManagementDatabasePage.test.ts create mode 100644 src/modules/management/instrumentAi.test.ts create mode 100644 src/modules/management/instrumentAi.ts create mode 100644 src/modules/reports/ReportsPage.test.ts delete mode 100644 src/modules/rubrics/ChecklistsSection.tsx delete mode 100644 src/modules/rubrics/RubricsPage.tsx create mode 100644 src/shared/attendance/attendance.test.ts create mode 100644 src/shared/attendance/attendance.ts create mode 100644 src/shared/gradebook/calculations.test.ts create mode 100644 src/shared/gradebook/calculations.ts create mode 100644 src/shared/gradebook/manualAssessments.test.ts create mode 100644 src/shared/gradebook/manualAssessments.ts create mode 100644 src/shared/import/studentsCsv.test.ts create mode 100644 src/shared/import/studentsCsv.ts create mode 100644 src/shared/planner/printablePlanner.test.ts create mode 100644 src/shared/planner/printablePlanner.ts create mode 100644 src/shared/planner/sessionPlan.test.ts create mode 100644 src/shared/planner/sessionPlan.ts create mode 100644 src/shared/planner/week.test.ts create mode 100644 src/shared/planner/week.ts create mode 100644 src/shared/reports/printableReports.test.ts create mode 100644 src/shared/reports/printableReports.ts create mode 100644 src/shared/schedule/weekDays.test.ts create mode 100644 src/shared/schedule/weekDays.ts create mode 100644 src/shared/students/followUp.test.ts create mode 100644 src/shared/students/followUp.ts delete mode 100644 src/shared/ui/SidebarNav.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 4890522..f636ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,53 @@ ## [Unreleased] +### Added + +- Added a weekly planner screen for assigning task sessions to class schedule slots. +- Added manual gradebook assessments with editable weights, folders, periods, competencies, and per-student grades. +- Added per-student observations for manual gradebook assessment grades. +- Added CSV student import from the students management screen. +- Added pasted spreadsheet table import for students. +- Added broader student import support for common school roster headers and `Last name, First name` formats. +- Added printable HTML group reports for browser printing or PDF export. +- Added printable individual student report packs with one page per student. +- Added printable weekly planner exports with session objectives, materials, homework, and teacher notes. +- Added student follow-up records for tutorial tracking, family contacts, incidents, agreements, adaptations, and wellbeing notes. +- Added editable student email contact data and included it in non-AI report exports. +- Added extended session planning fields in the weekly planner, including status, objectives, competencies, materials, homework, and teacher notes. +- Added seven-day schedule support so Saturday and Sunday can be enabled when needed. + ### Changed - Replaced direct in-app WebLLM usage with the external AI Proxy Bridge Chrome extension runtime. - Removed the in-app AI assistant/settings route and enabled the extension-provided compact overlay. - Removed `@mlc-ai/web-llm` from the app bundle. - Updated dependencies within the current non-major ranges and fixed all reported npm audit vulnerabilities. +- Hardened AI extension identity checks and made AI report generation ask for explicit confirmation before sending academic data. +- AI reports now anonymize student names by default, with an opt-in control to include names when needed. +- Changed PWA updates to prompt before reloading instead of taking over open sessions silently. +- Fixed gradebook and report scoping for task sessions/comments shared across subjects. +- Fixed task score calculations so stale rubric or checklist rows from inactive templates do not affect grades. +- Fixed task score calculations so duplicate rubric criteria or checklist item rows cannot inflate grades. +- Fixed task diary saves and planner session data counts after the scoped task evaluation schema migration. +- Fixed the weekly planner so it only renders days enabled in the schedule. +- Replaced the schedule block class/break checkbox with an explicit segmented control. +- Normalized attendance notes so clearing or whitespace-only observations is persisted correctly. +- Aligned the Dexie schema version with the exported backup schema version. - Split production vendor chunks and reduced the PWA precache payload from roughly 6.4 MB to roughly 526 KB. - Added schema validation to database backup imports. +- Hardened backup validation for attendance rows, student photo data URLs, rubrics, and checklist templates. +- Hardened backup validation against duplicate logical rows in attendance, gradebook, task sessions, and task assessment tables. +- Hardened backup validation against planner sessions that collide in the same class schedule slot. +- Hardened backup validation for invalid numeric ranges in session counts, positions, and gradebook weights. +- Hardened backup validation for negative rubric assessment scores. +- Hardened backup validation for task diary student and general comments. +- Hardened backup metadata validation for invalid export timestamps. +- Hardened backup validation for impossible dates, duplicated weekdays, and overlapping schedule blocks. - Added image type and size validation before storing student photos. - Added cleanup for task sessions that reference a deleted course. - Added a small Vitest suite so the test command verifies real code. +- Added `typecheck`, `audit`, and `verify` project scripts, with `verify` now covering production builds. ## [0.1.0] - 2026-03-19 diff --git a/CLAUDE.md b/CLAUDE.md index 2c7bb96..9cad3e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,9 @@ npm run dev npm run build npm run preview npm run test +npm run typecheck +npm run audit +npm run verify ``` ## Source Layout @@ -34,12 +37,18 @@ src/ modules/ attendance/ # Journal and attendance workflows gradebook/ # Weighted gradebook - rubrics/ # Rubrics and checklists + planner/ # Weekly task-session planning reports/ # Reporting views management/ # Courses, students, subjects, units, tasks, schedules, backups shared/ ai/ # AI extension runtime client + attendance/ # Attendance normalization helpers db/ # Dexie schema and shared types + gradebook/ # Scoring and manual assessment helpers + import/ # CSV and pasted-table import helpers + planner/ # Planner/session helpers and printable planner exports + reports/ # Printable report builders + students/ # Student follow-up helpers ui/ # Shared components hooks/ # Shared React hooks utils/ # Shared utilities @@ -48,7 +57,7 @@ src/ ## Data - There is no backend. Data persists in the user's browser through IndexedDB. -- Backup imports must validate app name, schema version, allowed table names, and row IDs before clearing data. +- Backup imports must validate app name, schema version, export timestamp, allowed table names, row IDs, references, date/time values, duplicate logical rows, schedule overlaps, and supported legacy tables before clearing data. - Do not add automatic seed data on normal app startup. ## Deployment diff --git a/README.md b/README.md index 85763d4..dc0d772 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,36 @@ # ProfePlus -Teacher notebook web app packaged as a PWA. Data is stored locally in IndexedDB and AI features are routed through the companion Chrome extension. +ProfePlus is an offline-first teacher notebook packaged as a PWA. It is built for day-to-day classroom management: courses, students, subjects, schedules, attendance, work logs, task planning, gradebook, follow-up records, reports, and local backups. + +All academic data is stored locally in the browser with IndexedDB. AI features are routed through the external AI Proxy Bridge Chrome extension; the app does not bundle a local model runtime. ## Current Scope -- Main workspace with journal, gradebook, evaluation tools, reports, and academic management. -- Academic management for courses, students, subjects, units, tasks, schedules, preferences, and database operations. -- Rubric and checklist generation through the AI Proxy Bridge Chrome extension. -- Local-first persistence with Dexie and IndexedDB. -- Backup export/import with schema checks. +- Top-level teacher workspace with separate tabs for attendance, work diary, weekly planner, gradebook, academic management, reports, and configuration. +- Course and student management, including student photos, email contacts, educational measures, tutorial follow-up records, CSV import, and pasted spreadsheet import. +- Subject, unit, task, and schedule management, including subject-course links, subject-student links, weekly time blocks, and task evaluation setup. +- Attendance journal with per-slot attendance status and notes. +- Work diary with per-task session comments, rubric/checklist/direct-grade evaluation data, and persistence per class, subject, date, and schedule slot. +- Weekly planner for assigning task sessions to schedule slots, tracking session status, objectives, competencies, materials, homework, and teacher notes. +- Gradebook with manual assessments, weights, folders, periods, competencies, per-student grades, observations, and task scores. +- Reports module with printable HTML reports, CSV exports, AI-ready datasets, and AI report generation through the extension runtime. +- Local database operations for seeded test data, JSON backup export/import, integrity checks, and data reset. ## Stack - React 19 + TypeScript 5.9 -- Vite 7 +- Vite 7 + vite-plugin-pwa 1.3 - Redux Toolkit 2 -- Dexie 4 +- Dexie 4 with IndexedDB - React Router 7 - Vitest 4 -- vite-plugin-pwa 1.3 ## Requirements - Node.js 22+ - npm 10+ -- Chrome or Chromium for AI features -- AI Proxy Bridge extension loaded from `D:\ProyectosIA\ia extension\dist\extension` +- Chrome or Chromium for AI features and browser QA +- AI Proxy Bridge extension loaded from `D:\ProyectosIA\ia extension\dist\extension` when AI workflows are needed ## Local Development @@ -34,23 +39,46 @@ npm install npm run dev ``` -## Build +The Vite dev server prints the local URL. By default it uses the configured base path from the environment. + +## Scripts ```bash -npm run build +npm run dev # Start Vite in development mode +npm run build # Type-check and build production assets into dist/ +npm run preview # Preview the production build +npm run test # Run Vitest +npm run typecheck # Run TypeScript project checks +npm run audit # Run npm audit +npm run verify # Run typecheck, tests, production build, and audit ``` -The production output is generated in `dist/`. +Use `npm run verify` before publishing or handing off a larger change. -## Tests +## Data Model And Backups -```bash -npm run test -``` +ProfePlus has no backend. Data persists in IndexedDB through Dexie. + +Database tools are available under `Configuración > Base de datos`: + +- Load seeded demo data for local testing. +- Export a JSON backup with app metadata, schema version, export timestamp, and table data. +- Import a JSON backup after validating app name, schema version, export timestamp, allowed tables, row IDs, references, duplicates, dates, schedule overlaps, task assessment shapes, and supported legacy tables. +- Verify database integrity. +- Delete all local app data. + +Backups include `schemaVersion` and `exportedAt`. Unsupported or malformed backups are rejected before existing local tables are cleared. ## AI Extension -ProfePlus does not include its own AI settings page. The AI Proxy Bridge Chrome extension injects its compact overlay in the page, and feature-level AI requests connect to that same extension runtime. +ProfePlus does not include an in-app AI settings page and does not import WebLLM directly. + +AI features use the AI Proxy Bridge Chrome extension: + +- The extension injects its compact overlay into trusted pages. +- Feature-level AI requests go through `src/shared/ai/extensionRuntime.ts`. +- Report generation asks for explicit confirmation before academic data is sent to AI. +- AI reports anonymize student names by default, with an opt-in control to include names when needed. Configuration options: @@ -60,7 +88,74 @@ Configuration options: The extension project is external to this app and should be managed separately. -## Deploying Under a Subdirectory +## Project Layout + +```txt +src/ + app/ Redux store and typed hooks + modules/ + attendance/ Attendance journal and work diary + gradebook/ Weighted gradebook and manual assessment entry + management/ Courses, students, subjects, units, tasks, schedules, backups + planner/ Weekly task-session planner + reports/ Printable, CSV, and AI-assisted reports + shared/ + ai/ AI extension runtime client + attendance/ Attendance normalization helpers + db/ Dexie schema and shared types + gradebook/ Gradebook and scoring calculations + hooks/ Shared React hooks + import/ Student import parsing + planner/ Planner/session helpers and printable exports + reports/ Printable report generation + students/ Student follow-up helpers + ui/ Shared UI components + utils/ General utilities +``` + +Rubrics and checklists are managed inside task management. The legacy `/rubrics` route redirects to `/management/tasks`. + +## Testing And QA + +Automated coverage includes focused Vitest suites for: + +- Backup payload validation. +- Student import parsing. +- Attendance note normalization. +- Gradebook calculations and manual assessment behavior. +- Planner session planning and printable planner exports. +- Printable reports. +- Student follow-up helpers. +- AI-generated instrument parsing and validation. + +The latest full verification passed with: + +```bash +npm run verify +``` + +Expected result at the time of this documentation update: + +- TypeScript project checks pass. +- Vitest reports 12 passing test files and 94 passing tests. +- Production build succeeds. +- `npm audit` reports 0 vulnerabilities. + +Recent browser QA also covered: + +- App load, page identity, non-blank render, and no framework overlay. +- Seeded data and database integrity check. +- Management pages for courses, students, subjects, tasks, and schedule. +- Student follow-up persistence after reload. +- Attendance status and note persistence. +- Work diary student comment persistence after the scoped task evaluation schema migration. +- Planner scheduling UI for a newly created multi-session task. +- Manual gradebook assessment and grade persistence. +- CSV academic report download. +- Mobile planner rendering at 390x844. +- Console health without relevant errors, warnings, page errors, or unhandled rejections. + +## Deploying Under A Subdirectory The app supports root and subdirectory deployments. @@ -74,12 +169,9 @@ Relevant files: - `.env.example` - `.env.production` -## Data Management - -Database tools are available under `Configuracion > Base de datos`: - -- Export a JSON backup. -- Import a JSON backup after app/schema/table validation. -- Delete all local app data. +## Development Conventions -Backups include a `schemaVersion` field so future migrations can reject unsupported files. +- Documentation, variable names, function names, and code comments are written in English. +- User-facing UI text can remain Spanish. +- Keep data operations local-first and avoid introducing backend dependencies unless the architecture is intentionally changed. +- Keep AI calls behind the extension runtime; do not reintroduce direct WebLLM imports in the app bundle. diff --git a/package-lock.json b/package-lock.json index f4c752b..511fbea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,30 @@ { "name": "profeplus", - "version": "0.2.0", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "profeplus", - "version": "0.2.0", + "version": "0.2.2", "dependencies": { "@reduxjs/toolkit": "^2.12.0", - "dexie": "^4.4.2", + "dexie": "^4.4.4", "history": "^5.3.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-redux": "^9.3.0", - "react-router-dom": "^7.15.1" + "react-router-dom": "^7.18.1" }, "devDependencies": { - "@types/node": "^25.8.0", - "@types/react": "^19.2.14", + "@types/node": "^25.9.4", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", "typescript": "^5.9.3", - "vite": "^7.3.3", + "vite": "^7.3.6", "vite-plugin-pwa": "^1.3.0", - "vitest": "^4.1.6" + "vitest": "^4.1.10" } }, "node_modules/@apideck/better-ajv-errors": { @@ -45,13 +45,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -60,9 +60,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -70,21 +70,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -101,14 +101,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -131,14 +131,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -205,9 +205,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -229,29 +229,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -334,9 +334,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -344,9 +344,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -354,9 +354,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -379,27 +379,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1570,33 +1570,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1604,23 +1604,23 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -1635,9 +1635,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -1652,9 +1652,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -1669,9 +1669,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -1686,9 +1686,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -1703,9 +1703,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -1720,9 +1720,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -1737,9 +1737,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -1754,9 +1754,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -1771,9 +1771,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -1788,9 +1788,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -1805,9 +1805,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1822,9 +1822,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1839,9 +1839,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1856,9 +1856,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1873,9 +1873,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1890,9 +1890,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1907,9 +1907,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1924,9 +1924,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1941,9 +1941,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1958,9 +1958,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1975,9 +1975,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1992,9 +1992,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -2009,9 +2009,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -2026,9 +2026,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -2043,9 +2043,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -2739,9 +2739,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.8.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", - "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", "dev": true, "license": "MIT", "dependencies": { @@ -2749,9 +2749,9 @@ } }, "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==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2810,16 +2810,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2828,13 +2828,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.6", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2855,9 +2855,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2868,13 +2868,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.6", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -2882,14 +2882,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2898,9 +2898,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -2908,13 +2908,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.6", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3446,9 +3446,9 @@ } }, "node_modules/dexie": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", - "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.4.tgz", + "integrity": "sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==", "license": "Apache-2.0" }, "node_modules/dunder-proto": { @@ -3633,9 +3633,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3646,32 +3646,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -5029,24 +5029,24 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/react-redux": { @@ -5083,9 +5083,9 @@ } }, "node_modules/react-router": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", - "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -5105,12 +5105,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz", - "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "license": "MIT", "dependencies": { - "react-router": "7.15.1" + "react-router": "7.18.1" }, "engines": { "node": ">=20.0.0" @@ -6135,13 +6135,13 @@ } }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -6241,19 +6241,19 @@ } }, "node_modules/vitest": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.6", - "@vitest/mocker": "4.1.6", - "@vitest/pretty-format": "4.1.6", - "@vitest/runner": "4.1.6", - "@vitest/snapshot": "4.1.6", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -6281,12 +6281,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.6", - "@vitest/browser-preview": "4.1.6", - "@vitest/browser-webdriverio": "4.1.6", - "@vitest/coverage-istanbul": "4.1.6", - "@vitest/coverage-v8": "4.1.6", - "@vitest/ui": "4.1.6", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index f388ce0..5336da3 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,34 @@ { "name": "profeplus", "private": true, - "version": "0.2.0", + "version": "0.2.2", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc -b", + "audit": "npm audit", + "verify": "npm run typecheck && npm run test && npm run build && npm run audit" }, "dependencies": { "@reduxjs/toolkit": "^2.12.0", - "dexie": "^4.4.2", + "dexie": "^4.4.4", "history": "^5.3.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-redux": "^9.3.0", - "react-router-dom": "^7.15.1" + "react-router-dom": "^7.18.1" }, "devDependencies": { - "@types/node": "^25.8.0", - "@types/react": "^19.2.14", + "@types/node": "^25.9.4", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", "typescript": "^5.9.3", - "vite": "^7.3.3", + "vite": "^7.3.6", "vite-plugin-pwa": "^1.3.0", - "vitest": "^4.1.6" + "vitest": "^4.1.10" } } diff --git a/src/App.tsx b/src/App.tsx index 4031e4c..11802e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { ManagementStudentsPage } from "./modules/management/ManagementStudentsP import { ManagementSubjectsPage } from "./modules/management/ManagementSubjectsPage"; import { ManagementTasksPage } from "./modules/management/ManagementTasksPage"; import { ManagementUnitsPage } from "./modules/management/ManagementUnitsPage"; +import { PlannerPage } from "./modules/planner/PlannerPage"; import { ReportsPage } from "./modules/reports/ReportsPage"; import { enableAiExtensionOverlay } from "./shared/ai/extensionOverlay"; import { db } from "./shared/db/database"; @@ -118,7 +119,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/src/main.tsx b/src/main.tsx index 100f5c6..71771da 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,7 +10,15 @@ import { store } from "./app/store"; import { UnsavedChangesDialogProvider } from "./shared/ui/UnsavedChangesDialog"; import "./styles.css"; -registerSW({ immediate: true }); +const updateServiceWorker = registerSW({ + immediate: false, + onNeedRefresh() { + const shouldReload = window.confirm("Hay una nueva version de ProfePlus. ¿Quieres recargar ahora?"); + if (shouldReload) { + void updateServiceWorker(true); + } + } +}); const rawHistory = createBrowserHistory(); type RouterHistory = HistoryRouterProps["history"]; type BlockableRouterHistory = RouterHistory & { diff --git a/src/modules/attendance/AttendancePage.tsx b/src/modules/attendance/AttendancePage.tsx index 20bd3d1..e036401 100644 --- a/src/modules/attendance/AttendancePage.tsx +++ b/src/modules/attendance/AttendancePage.tsx @@ -15,6 +15,7 @@ import type { Task, TaskChecklistAssessment, TaskDailyEvaluationSetting, + TaskDirectGrade, TaskGradebookConfig, TaskRubricAssessment, TaskSession, @@ -22,6 +23,8 @@ import type { TaskSubjectLink, UnitBlock } from "../../shared/db/types"; +import { normalizeAttendanceNote, resolveAttendanceNoteForSave } from "../../shared/attendance/attendance"; +import { matchesTaskScope } from "../../shared/gradebook/calculations"; import { useStudentDisplay } from "../../shared/hooks/useStudentDisplay"; const today = new Date().toISOString().slice(0, 10); @@ -57,12 +60,6 @@ type SubjectSlot = { endTime: string; }; -function statusLabel(value: AttendanceEntry["status"]): string { - if (value === "present") return "Presente"; - if (value === "late") return "Retraso"; - return "Ausente"; -} - function toMinutes(value: string): number { const [hourRaw, minuteRaw] = value.split(":"); const hour = Number(hourRaw); @@ -181,6 +178,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { const [taskDailyEvaluationSettings, setTaskDailyEvaluationSettings] = useState([]); const [taskRubricAssessments, setTaskRubricAssessments] = useState([]); const [taskChecklistAssessments, setTaskChecklistAssessments] = useState([]); + const [taskDirectGrades, setTaskDirectGrades] = useState([]); const [selectedDate, setSelectedDate] = useState(today); const [selectedSlotKey, setSelectedSlotKey] = useState(""); const [selectedTaskId, setSelectedTaskId] = useState(""); @@ -194,6 +192,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { const [selectedChecklistTemplateId, setSelectedChecklistTemplateId] = useState(""); const [taskRubricDraft, setTaskRubricDraft] = useState>(new Map()); const [taskChecklistDraft, setTaskChecklistDraft] = useState>(new Map()); + const [taskDirectGradeDraft, setTaskDirectGradeDraft] = useState>(new Map()); const [taskDirty, setTaskDirty] = useState(false); const [isSavingTask, setIsSavingTask] = useState(false); const [taskNotice, setTaskNotice] = useState(""); @@ -209,6 +208,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { const attendanceAutoSaveTimerRef = useRef(null); const taskAutoSaveTimerRef = useRef(null); const taskEditVersionRef = useRef(0); + const taskPickerContextRef = useRef(""); const loadMetadata = async (): Promise => { const [ @@ -228,6 +228,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { taskDailyEvaluationSettingsData, taskRubricAssessmentsData, taskChecklistAssessmentsData, + taskDirectGradesData, taskGradebookConfigsData ] = await Promise.all([ db.classGroups.orderBy("name").toArray(), @@ -246,6 +247,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { db.taskDailyEvaluationSettings.toArray(), db.taskRubricAssessments.toArray(), db.taskChecklistAssessments.toArray(), + db.taskDirectGrades.toArray(), db.taskGradebookConfigs.toArray() ]); @@ -266,6 +268,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { setTaskDailyEvaluationSettings(taskDailyEvaluationSettingsData); setTaskRubricAssessments(taskRubricAssessmentsData); setTaskChecklistAssessments(taskChecklistAssessmentsData); + setTaskDirectGrades(taskDirectGradesData); }; const dayOfWeek = useMemo(() => { @@ -658,7 +661,13 @@ export function AttendancePage({ mode }: AttendancePageProps) { const taskTitleByClassSlot = useMemo(() => { const taskById = new Map(tasks.map((task) => [task.id, task])); const unitById = new Map(unitBlocks.map((unit) => [unit.id, unit])); - const subjectLinkByTaskId = new Map(taskSubjectLinks.map((link) => [link.taskId, link])); + const subjectLinksByTaskId = new Map(); + for (const link of taskSubjectLinks) { + if (!subjectLinksByTaskId.has(link.taskId)) { + subjectLinksByTaskId.set(link.taskId, []); + } + subjectLinksByTaskId.get(link.taskId)?.push(link); + } const map = new Map(); for (const session of taskSessions) { if (session.date !== selectedDate) { @@ -668,7 +677,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { if (!task) { continue; } - const link = subjectLinkByTaskId.get(session.taskId); + const links = subjectLinksByTaskId.get(session.taskId) ?? []; + const link = links.find((item) => item.subjectId === session.subjectId) ?? links[0]; const unit = link?.unitId ? unitById.get(link.unitId) : null; const unitName = unit?.name || "Sin unidad"; const taskTitle = task.title || "Tarea sin título"; @@ -683,6 +693,9 @@ export function AttendancePage({ mode }: AttendancePageProps) { const unitById = new Map(unitBlocks.map((unit) => [unit.id, unit])); const map = new Map(); for (const link of taskSubjectLinks) { + if (selectedSubjectId && link.subjectId !== selectedSubjectId) { + continue; + } if (!link.unitId || map.has(link.taskId)) { continue; } @@ -692,7 +705,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { } } return map; - }, [taskSubjectLinks, unitBlocks]); + }, [selectedSubjectId, taskSubjectLinks, unitBlocks]); const workTaskOptions = useMemo(() => { if (!selectedClassId || !selectedSubjectId || !selectedWorkUnitId) { @@ -705,7 +718,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { ); const sessionCountByTask = new Map(); for (const session of taskSessions) { - if (session.classId !== selectedClassId || session.subjectId !== selectedSubjectId) { + if (session.classId !== selectedClassId || !taskIdsForSubject.has(session.taskId)) { continue; } sessionCountByTask.set(session.taskId, (sessionCountByTask.get(session.taskId) ?? 0) + 1); @@ -733,12 +746,18 @@ export function AttendancePage({ mode }: AttendancePageProps) { if (mode !== "work" || !selectedTaskForDay || !selectedClassId || !selectedSubjectId) { return []; } + const selectedTaskBelongsToContext = taskSubjectLinks.some( + (link) => + link.taskId === selectedTaskForDay.id && + link.subjectId === selectedSubjectId && + (!selectedWorkUnitId || (link.unitId ?? "") === selectedWorkUnitId) + ); return taskSessions .filter( (session) => session.taskId === selectedTaskForDay.id && session.classId === selectedClassId && - session.subjectId === selectedSubjectId + (session.subjectId === selectedSubjectId || selectedTaskBelongsToContext) ) .sort((a, b) => { const byDate = a.date.localeCompare(b.date); @@ -752,11 +771,20 @@ export function AttendancePage({ mode }: AttendancePageProps) { } return a.scheduleSlotId.localeCompare(b.scheduleSlotId); }); - }, [mode, selectedClassId, selectedSubjectId, selectedTaskForDay, slotOrderById, taskSessions]); + }, [ + mode, + selectedClassId, + selectedSubjectId, + selectedTaskForDay, + selectedWorkUnitId, + slotOrderById, + taskSessions, + taskSubjectLinks + ]); const selectedTaskSessionsForDay = useMemo(() => { if (mode === "work") { - return []; + return workTaskSessions; } return tasksForSelectedDate.find((item) => item.task.id === selectedTaskId)?.sessions ?? []; }, [ @@ -768,7 +796,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { selectedTaskId, slotOrderById, taskSessions, - tasksForSelectedDate + tasksForSelectedDate, + workTaskSessions ]); const selectedTaskSessionForDay = useMemo( () => @@ -818,24 +847,33 @@ export function AttendancePage({ mode }: AttendancePageProps) { ); const taskHasFixedRubric = Boolean(selectedTaskGradebookConfig?.rubricTemplateId); const taskHasFixedChecklist = !taskHasFixedRubric && Boolean(selectedTaskGradebookConfig?.checklistTemplateId); + const taskHasDirectGrade = Boolean(selectedTaskGradebookConfig?.directGradeEnabled); const diaryRubricTemplates = selectedRubricTemplate ? [selectedRubricTemplate] : []; const diaryChecklistTemplates = selectedChecklistTemplate ? [selectedChecklistTemplate] : []; const taskHasAssignedInstrument = Boolean( - selectedTaskGradebookConfig?.rubricTemplateId || selectedTaskGradebookConfig?.checklistTemplateId + selectedTaskGradebookConfig?.rubricTemplateId || + selectedTaskGradebookConfig?.checklistTemplateId || + selectedTaskGradebookConfig?.directGradeEnabled ); useEffect(() => { if (mode !== "attendance") { return; } - if (tasksForSelectedDate.length === 0) { + if (taskForSelectedSlot?.id) { + if (selectedTaskId !== taskForSelectedSlot.id) { + setSelectedTaskId(taskForSelectedSlot.id); + } + return; + } + if (tasksForSelectedDate.length === 0 || !taskForSelectedSlot) { setSelectedTaskId(""); return; } if (!tasksForSelectedDate.some((item) => item.task.id === selectedTaskId)) { setSelectedTaskId(tasksForSelectedDate[0].task.id); } - }, [mode, selectedTaskId, tasksForSelectedDate]); + }, [mode, selectedTaskId, taskForSelectedSlot, tasksForSelectedDate]); useEffect(() => { if (mode !== "work") { @@ -850,11 +888,48 @@ export function AttendancePage({ mode }: AttendancePageProps) { } }, [mode, selectedTaskId, workTaskOptions]); + useEffect(() => { + if (mode !== "attendance") { + return; + } + const contextKey = `${selectedDate}:${selectedSubjectSlot?.key ?? ""}:${taskForSelectedSlot?.id ?? ""}`; + if (taskPickerContextRef.current === contextKey) { + return; + } + taskPickerContextRef.current = contextKey; + + const currentTaskUnitId = + taskSubjectLinks.find( + (link) => + link.subjectId === selectedSubjectSlot?.subjectId && + link.taskId === taskForSelectedSlot?.id + )?.unitId ?? ""; + const hasCurrentTaskUnit = unitsForSelectedSubject.some((unit) => unit.id === currentTaskUnitId); + + setSelectedUnitToAssignId(hasCurrentTaskUnit ? currentTaskUnitId : ""); + setSelectedTaskToAssignId(taskForSelectedSlot?.id ?? ""); + }, [ + mode, + selectedDate, + selectedSubjectSlot?.key, + selectedSubjectSlot?.subjectId, + taskForSelectedSlot?.id, + taskSubjectLinks, + unitsForSelectedSubject + ]); + useEffect(() => { if (unitsForSelectedSubject.length === 0) { setSelectedUnitToAssignId(""); return; } + if (mode === "attendance" && !taskForSelectedSlot) { + const selectedUnitExists = unitsForSelectedSubject.some((unit) => unit.id === selectedUnitToAssignId); + if (selectedUnitToAssignId && !selectedUnitExists) { + setSelectedUnitToAssignId(""); + } + return; + } const currentTaskUnitId = taskSubjectLinks.find( (link) => link.subjectId === selectedSubjectSlot?.subjectId && @@ -871,6 +946,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { setSelectedUnitToAssignId(preferredUnitId); } }, [ + mode, selectedSubjectSlot?.subjectId, selectedUnitToAssignId, taskForSelectedSlot?.id, @@ -891,6 +967,12 @@ export function AttendancePage({ mode }: AttendancePageProps) { setSelectedTaskToAssignId(currentTaskId); return; } + if (mode === "attendance" && !currentTaskId) { + if (selectedTaskToAssignId && !taskPickerOptions.some((task) => task.id === selectedTaskToAssignId)) { + setSelectedTaskToAssignId(""); + } + return; + } if (!taskPickerOptions.some((task) => task.id === selectedTaskToAssignId)) { setSelectedTaskToAssignId(taskPickerOptions[0].id); } @@ -916,29 +998,35 @@ export function AttendancePage({ mode }: AttendancePageProps) { }, [mode, selectedDate, selectedTaskSessionSlotId, selectedTaskSessionsForDay]); useEffect(() => { - if (!selectedTaskForDay || (mode === "attendance" && !selectedTaskSessionForDay)) { + if (!selectedTaskForDay || !selectedTaskSessionForDay || !activeClassId || !activeSubjectId) { setTaskGeneralCommentDraft(""); setTaskStudentCommentDraft(new Map()); setSelectedRubricTemplateId(""); setSelectedChecklistTemplateId(""); setTaskRubricDraft(new Map()); setTaskChecklistDraft(new Map()); + setTaskDirectGradeDraft(new Map()); setTaskNotice(""); setTaskDirty(false); return; } - const diaryDate = mode === "work" ? workDiaryDateKey : selectedDate; - const slotId = mode === "work" ? workDiarySlotId : (selectedTaskSessionForDay?.scheduleSlotId ?? ""); + const diaryDate = mode === "work" ? (selectedTaskSessionForDay?.date ?? workDiaryDateKey) : selectedDate; + const slotId = mode === "work" ? (selectedTaskSessionForDay?.scheduleSlotId ?? workDiarySlotId) : (selectedTaskSessionForDay?.scheduleSlotId ?? ""); const setting = taskDailyEvaluationSettings.find( (item) => item.taskId === selectedTaskForDay.id && item.date === diaryDate && - (item.scheduleSlotId ?? "") === slotId + (item.scheduleSlotId ?? "") === slotId && + matchesTaskScope(item, activeClassId, activeSubjectId) ) ?? null; const legacySetting = taskDailyEvaluationSettings.find( - (item) => item.taskId === selectedTaskForDay.id && item.date === diaryDate && !item.scheduleSlotId + (item) => + item.taskId === selectedTaskForDay.id && + item.date === diaryDate && + !item.scheduleSlotId && + matchesTaskScope(item, activeClassId, activeSubjectId) ) ?? null; setTaskGeneralCommentDraft(setting?.generalComment ?? legacySetting?.generalComment ?? ""); @@ -948,7 +1036,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { if ( row.taskId === selectedTaskForDay.id && row.date === diaryDate && - (row.scheduleSlotId ?? "") === slotId + (row.scheduleSlotId ?? "") === slotId && + matchesTaskScope(row, activeClassId, activeSubjectId) ) { hasScopedComments = true; commentsMap.set(row.studentId, row.comment); @@ -956,7 +1045,12 @@ export function AttendancePage({ mode }: AttendancePageProps) { } if (!hasScopedComments) { for (const row of taskStudentComments) { - if (row.taskId === selectedTaskForDay.id && !row.date && !row.scheduleSlotId) { + if ( + row.taskId === selectedTaskForDay.id && + !row.date && + !row.scheduleSlotId && + matchesTaskScope(row, activeClassId, activeSubjectId) + ) { commentsMap.set(row.studentId, row.comment); } } @@ -981,7 +1075,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { if ( row.taskId !== selectedTaskForDay.id || row.date !== diaryDate || - (row.scheduleSlotId ?? "") !== slotId + (row.scheduleSlotId ?? "") !== slotId || + !matchesTaskScope(row, activeClassId, activeSubjectId) ) { continue; } @@ -994,22 +1089,39 @@ export function AttendancePage({ mode }: AttendancePageProps) { if ( row.taskId !== selectedTaskForDay.id || row.date !== diaryDate || - (row.scheduleSlotId ?? "") !== slotId + (row.scheduleSlotId ?? "") !== slotId || + !matchesTaskScope(row, activeClassId, activeSubjectId) ) { continue; } checklistMap.set(checklistDraftKey(row.studentId, row.itemId), row.checked); } setTaskChecklistDraft(checklistMap); + + const directGradeMap = new Map(); + for (const row of taskDirectGrades) { + if ( + row.taskId !== selectedTaskForDay.id || + row.classId !== activeClassId || + row.subjectId !== activeSubjectId + ) { + continue; + } + directGradeMap.set(row.studentId, String(row.score)); + } + setTaskDirectGradeDraft(directGradeMap); setTaskNotice(""); setTaskDirty(false); }, [ + activeClassId, + activeSubjectId, selectedDate, selectedTaskGradebookConfig, selectedTaskForDay, selectedTaskSessionForDay, taskChecklistAssessments, taskDailyEvaluationSettings, + taskDirectGrades, taskRubricAssessments, taskStudentComments, mode, @@ -1025,10 +1137,11 @@ export function AttendancePage({ mode }: AttendancePageProps) { }; const setDraftNote = (studentId: string, note: string): void => { - const existingNote = attendanceByStudent.get(studentId)?.note ?? ""; + const existingNote = normalizeAttendanceNote(attendanceByStudent.get(studentId)?.note) ?? ""; + const normalizedNote = normalizeAttendanceNote(note) ?? ""; setDraftNoteByStudent((prev) => { const next = new Map(prev); - if (note === existingNote) { + if (normalizedNote === existingNote) { next.delete(studentId); } else { next.set(studentId, note); @@ -1084,7 +1197,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { date: selectedDate, scheduleSlotId: selectedSubjectSlot.slotId, status, - note: noteDraftSnapshot.get(studentId) ?? existing?.note + note: resolveAttendanceNoteForSave(studentId, noteDraftSnapshot, existing?.note) }); } setDraftStatusByStudent((current) => { @@ -1114,7 +1227,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { }; const saveTaskDiary = async (): Promise => { - if (!selectedTaskForDay || (mode === "attendance" && !selectedTaskSessionForDay)) { + if (!selectedTaskForDay || !activeClassId || !activeSubjectId || !selectedTaskSessionForDay) { return false; } if (taskAutoSaveTimerRef.current !== null) { @@ -1124,17 +1237,42 @@ export function AttendancePage({ mode }: AttendancePageProps) { const saveVersion = taskEditVersionRef.current; const taskId = selectedTaskForDay.id; - const diaryDate = mode === "work" ? workDiaryDateKey : selectedDate; - const scheduleSlotId = mode === "work" ? workDiarySlotId : (selectedTaskSessionForDay?.scheduleSlotId ?? ""); + const diaryDate = mode === "work" ? (selectedTaskSessionForDay?.date ?? workDiaryDateKey) : selectedDate; + const scheduleSlotId = mode === "work" ? (selectedTaskSessionForDay?.scheduleSlotId ?? workDiarySlotId) : (selectedTaskSessionForDay?.scheduleSlotId ?? ""); const normalizedGeneralComment = taskGeneralCommentDraft.trim(); const effectiveRubricTemplateId = selectedTaskGradebookConfig?.rubricTemplateId ?? ""; const effectiveChecklistTemplateId = effectiveRubricTemplateId ? "" : (selectedTaskGradebookConfig?.checklistTemplateId ?? ""); + const usesDirectGrade = Boolean(selectedTaskGradebookConfig?.directGradeEnabled); + const normalizedDirectGrades: TaskDirectGrade[] = []; + if (usesDirectGrade) { + for (const student of taskStudents) { + const rawValue = taskDirectGradeDraft.get(student.id)?.trim() ?? ""; + if (!rawValue) { + continue; + } + const score = Number(rawValue.replace(",", ".")); + if (!Number.isFinite(score) || score < 0 || score > 10) { + setTaskNotice("La nota directa debe estar entre 0 y 10."); + return false; + } + normalizedDirectGrades.push({ + id: `task-direct-${taskId}-${activeSubjectId}-${activeClassId}-${student.id}`, + taskId, + subjectId: activeSubjectId, + classId: activeClassId, + studentId: student.id, + score: Number(score.toFixed(2)) + }); + } + } const normalizedComments = taskStudents .map((student) => ({ id: crypto.randomUUID(), taskId, + subjectId: activeSubjectId, + classId: activeClassId, date: diaryDate, scheduleSlotId, studentId: student.id, @@ -1146,29 +1284,32 @@ export function AttendancePage({ mode }: AttendancePageProps) { try { await db.transaction( "rw", - [ - db.taskStudentComments, - db.taskDailyEvaluationSettings, - db.taskRubricAssessments, - db.taskChecklistAssessments - ], + db.taskStudentComments, + db.taskDailyEvaluationSettings, + db.taskRubricAssessments, + db.taskChecklistAssessments, + db.taskDirectGrades, async () => { - await db.taskStudentComments - .where("[taskId+date+scheduleSlotId]") - .equals([taskId, diaryDate, scheduleSlotId]) - .delete(); + const commentIds = (await db.taskStudentComments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([taskId, activeClassId, activeSubjectId, diaryDate, scheduleSlotId]) + .primaryKeys()) as string[]; + await db.taskStudentComments.bulkDelete(commentIds); if (normalizedComments.length > 0) { await db.taskStudentComments.bulkAdd(normalizedComments); } - await db.taskDailyEvaluationSettings - .where("[taskId+date+scheduleSlotId]") - .equals([taskId, diaryDate, scheduleSlotId]) - .delete(); + const settingIds = (await db.taskDailyEvaluationSettings + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([taskId, activeClassId, activeSubjectId, diaryDate, scheduleSlotId]) + .primaryKeys()) as string[]; + await db.taskDailyEvaluationSettings.bulkDelete(settingIds); if (normalizedGeneralComment || effectiveRubricTemplateId || effectiveChecklistTemplateId) { await db.taskDailyEvaluationSettings.add({ - id: `task-eval-${taskId}-${diaryDate}-${scheduleSlotId}`, + id: `task-eval-${taskId}-${activeSubjectId}-${activeClassId}-${diaryDate}-${scheduleSlotId}`, taskId, + subjectId: activeSubjectId, + classId: activeClassId, date: diaryDate, scheduleSlotId, generalComment: normalizedGeneralComment || undefined, @@ -1177,10 +1318,11 @@ export function AttendancePage({ mode }: AttendancePageProps) { }); } - await db.taskRubricAssessments - .where("[taskId+date+scheduleSlotId]") - .equals([taskId, diaryDate, scheduleSlotId]) - .delete(); + const rubricIds = (await db.taskRubricAssessments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([taskId, activeClassId, activeSubjectId, diaryDate, scheduleSlotId]) + .primaryKeys()) as string[]; + await db.taskRubricAssessments.bulkDelete(rubricIds); if (effectiveRubricTemplateId && selectedRubricTemplate) { const rubricRows: TaskRubricAssessment[] = []; for (const student of taskStudents) { @@ -1194,6 +1336,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { rubricRows.push({ id: crypto.randomUUID(), taskId, + subjectId: activeSubjectId, + classId: activeClassId, date: diaryDate, scheduleSlotId, studentId: student.id, @@ -1209,10 +1353,11 @@ export function AttendancePage({ mode }: AttendancePageProps) { } } - await db.taskChecklistAssessments - .where("[taskId+date+scheduleSlotId]") - .equals([taskId, diaryDate, scheduleSlotId]) - .delete(); + const checklistIds = (await db.taskChecklistAssessments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([taskId, activeClassId, activeSubjectId, diaryDate, scheduleSlotId]) + .primaryKeys()) as string[]; + await db.taskChecklistAssessments.bulkDelete(checklistIds); if (effectiveChecklistTemplateId && selectedChecklistTemplate) { const checklistRows: TaskChecklistAssessment[] = []; for (const student of taskStudents) { @@ -1223,6 +1368,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { checklistRows.push({ id: crypto.randomUUID(), taskId, + subjectId: activeSubjectId, + classId: activeClassId, date: diaryDate, scheduleSlotId, studentId: student.id, @@ -1236,6 +1383,16 @@ export function AttendancePage({ mode }: AttendancePageProps) { await db.taskChecklistAssessments.bulkAdd(checklistRows); } } + + if (usesDirectGrade) { + await db.taskDirectGrades + .where("[taskId+subjectId+classId]") + .equals([taskId, activeSubjectId, activeClassId]) + .delete(); + if (normalizedDirectGrades.length > 0) { + await db.taskDirectGrades.bulkAdd(normalizedDirectGrades); + } + } } ); @@ -1261,22 +1418,26 @@ export function AttendancePage({ mode }: AttendancePageProps) { db.tables, async () => { await db.taskSessions.where("id").equals(occupied.id).delete(); - await db.taskDailyEvaluationSettings - .where("[taskId+date+scheduleSlotId]") - .equals([occupied.taskId, selectedDate, selectedSubjectSlot.slotId]) - .delete(); - await db.taskStudentComments - .where("[taskId+date+scheduleSlotId]") - .equals([occupied.taskId, selectedDate, selectedSubjectSlot.slotId]) - .delete(); - await db.taskRubricAssessments - .where("[taskId+date+scheduleSlotId]") - .equals([occupied.taskId, selectedDate, selectedSubjectSlot.slotId]) - .delete(); - await db.taskChecklistAssessments - .where("[taskId+date+scheduleSlotId]") - .equals([occupied.taskId, selectedDate, selectedSubjectSlot.slotId]) - .delete(); + const settingIds = (await db.taskDailyEvaluationSettings + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([occupied.taskId, selectedSubjectSlot.classId, selectedSubjectSlot.subjectId, selectedDate, selectedSubjectSlot.slotId]) + .primaryKeys()) as string[]; + await db.taskDailyEvaluationSettings.bulkDelete(settingIds); + const commentIds = (await db.taskStudentComments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([occupied.taskId, selectedSubjectSlot.classId, selectedSubjectSlot.subjectId, selectedDate, selectedSubjectSlot.slotId]) + .primaryKeys()) as string[]; + await db.taskStudentComments.bulkDelete(commentIds); + const rubricIds = (await db.taskRubricAssessments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([occupied.taskId, selectedSubjectSlot.classId, selectedSubjectSlot.subjectId, selectedDate, selectedSubjectSlot.slotId]) + .primaryKeys()) as string[]; + await db.taskRubricAssessments.bulkDelete(rubricIds); + const checklistIds = (await db.taskChecklistAssessments + .where("[taskId+classId+subjectId+date+scheduleSlotId]") + .equals([occupied.taskId, selectedSubjectSlot.classId, selectedSubjectSlot.subjectId, selectedDate, selectedSubjectSlot.slotId]) + .primaryKeys()) as string[]; + await db.taskChecklistAssessments.bulkDelete(checklistIds); await db.taskSessions.add({ id: crypto.randomUUID(), taskId, @@ -1367,7 +1528,7 @@ export function AttendancePage({ mode }: AttendancePageProps) { ]); useEffect(() => { - if (!taskDirty || !selectedTaskForDay || (mode === "attendance" && !selectedTaskSessionForDay) || isSavingTask) { + if (!taskDirty || !selectedTaskForDay || !selectedTaskSessionForDay || isSavingTask) { return; } if (taskAutoSaveTimerRef.current !== null) { @@ -1388,7 +1549,9 @@ export function AttendancePage({ mode }: AttendancePageProps) { mode, selectedDate, selectedTaskForDay?.id, + selectedTaskSessionForDay?.date, selectedTaskSessionForDay?.scheduleSlotId, + taskDirectGradeDraft, taskChecklistDraft, taskDirty, taskGeneralCommentDraft, @@ -1401,7 +1564,8 @@ export function AttendancePage({ mode }: AttendancePageProps) { const calendarWeekdayLabels = useMemo(() => weekdayLabels(weekStartsOn), [weekStartsOn]); const renderStudentCommentInput = (student: Student) => (