commit b399e803b3551354067934c2d9629766dd233ce4 Author: Lino Mallevaey Date: Sun Nov 23 16:34:11 2025 +0100 Version : 2025.11.22 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..657050c --- /dev/null +++ b/.gitignore @@ -0,0 +1,182 @@ +# ========================================== +# Node.js / Frontend (React + Vite) +# ========================================== + +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +.pnp.* +.yarn/* + +# Build outputs +dist/ +dist-ssr/ +build/ +.next/ +out/ + +# Development +*.local +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log + +# Cache +.cache/ +.vite/ +.eslintcache +.parcel-cache/ + +# ========================================== +# Python / Backend (FastAPI) +# ========================================== + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# Celery +celerybeat-schedule +celerybeat.pid + +# ========================================== +# Database +# ========================================== + +*.db +*.sqlite +*.sqlite3 +*.sql + +# ========================================== +# Environment Variables +# ========================================== + +.env +.env.* +!.env.example + +# ========================================== +# IDE / Editors +# ========================================== + +# VSCode +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json + +# JetBrains IDEs (PyCharm, WebStorm, IntelliJ) +.idea/ +*.iml +*.iws +*.ipr + +# Sublime Text +*.sublime-project +*.sublime-workspace + +# Visual Studio +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ========================================== +# Operating System +# ========================================== + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Linux +*~ +.directory +.Trash-* + +# ========================================== +# Miscellaneous +# ========================================== + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ + +# Archives +*.zip +*.tar.gz +*.rar + +# IDE specific +.history/ diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..cfea368 --- /dev/null +++ b/App.tsx @@ -0,0 +1,67 @@ + +import React, { useState, useEffect } from 'react'; +import { Association } from './types'; +import { api } from './api'; +import LoginScreen from './components/LoginScreen'; +import Dashboard from './components/Dashboard'; + +const App: React.FC = () => { + const [activeAssociation, setActiveAssociation] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadAssociation = async () => { + try { + const storedAssociationId = localStorage.getItem('abacus-active-association-id'); + if (storedAssociationId) { + const association = await api.getAssociation(storedAssociationId); + setActiveAssociation(association); + } + } catch (error) { + console.error("Failed to load association", error); + localStorage.removeItem('abacus-active-association-id'); + } finally { + setLoading(false); + } + }; + loadAssociation(); + }, []); + + const handleLogin = (association: Association) => { + localStorage.setItem('abacus-active-association-id', association.id); + setActiveAssociation(association); + }; + + const handleLogout = () => { + localStorage.removeItem('abacus-active-association-id'); + setActiveAssociation(null); + }; + + const updateAssociation = async (updatedAssociation: Association) => { + setActiveAssociation(updatedAssociation); + }; + + if (loading) { + return ( +
+
Loading Abacus...
+
+ ); + } + + return ( +
+ {activeAssociation ? ( + + ) : ( + + )} +
+ ); +}; + +export default App; diff --git a/README.md b/README.md new file mode 100644 index 0000000..53ccc80 --- /dev/null +++ b/README.md @@ -0,0 +1,368 @@ +# 🧼 Abacus + +> **Application de comptabilitĂ© simplifiĂ©e pour associations** + +Abacus est une application web moderne conçue spĂ©cifiquement pour la gestion comptable des associations. Elle offre une interface intuitive et Ă©lĂ©gante permettant de gĂ©rer facilement vos balances financiĂšres, d'enregistrer vos opĂ©rations et de visualiser vos donnĂ©es comptables en temps rĂ©el. + +--- + +## 📋 Table des matiĂšres + +- [PrĂ©sentation](#-prĂ©sentation) +- [FonctionnalitĂ©s](#-fonctionnalitĂ©s) +- [Technologies utilisĂ©es](#-technologies-utilisĂ©es) +- [Installation](#-installation) +- [Lancement de l'application](#-lancement-de-lapplication) +- [Utilisation](#-utilisation) +- [Commandes CLI](#-commandes-cli) +- [Architecture](#-architecture) +- [Licence](#-licence) + +--- + +## 🎯 PrĂ©sentation + +**Abacus** est nĂ©e du besoin de simplifier la comptabilitĂ© associative. Au lieu de jongler avec des tableurs complexes, Abacus propose une solution web tout-en-un qui centralise : + +- ✅ **La gestion de vos balances** (compte principal, caisse, Ă©pargne, etc.) +- ✅ **L'enregistrement de vos opĂ©rations** (recettes et dĂ©penses) +- ✅ **La visualisation de vos donnĂ©es** avec des graphiques interactifs +- ✅ **L'export PDF** de vos rapports financiers +- ✅ **La sĂ©curitĂ©** avec un systĂšme d'authentification utilisateur +- ✅ **Le multi-tenant** pour gĂ©rer plusieurs associations sur une mĂȘme instance + +L'application a Ă©tĂ© pensĂ©e pour ĂȘtre **minimaliste**, **rapide** et **accessible**, mĂȘme pour les utilisateurs non techniques. + +--- + +## ⚡ FonctionnalitĂ©s + +### 🏠 Dashboard interactif +- Vue d'ensemble de votre santĂ© financiĂšre +- Affichage en carrousel de toutes vos balances +- Graphiques d'Ă©volution des revenus et dĂ©penses +- Tableaux dĂ©taillĂ©s de toutes les opĂ©rations + +### 💰 Gestion des balances +- CrĂ©ation et suppression de balances multiples +- Modification du nom et du montant initial +- Suivi du solde actuel en temps rĂ©el +- Organisation par cartes visuelles + +### 📊 Gestion des opĂ©rations +- Enregistrement de recettes et dĂ©penses +- CatĂ©gorisation des opĂ©rations (salaires, achats, dons, etc.) +- Ajout de descriptions dĂ©taillĂ©es +- Menu contextuel pour Ă©diter ou supprimer +- Modal de confirmation pour les suppressions + +### 📈 Visualisations +- **Graphiques** : Évolution temporelle avec Recharts +- **Tableaux** : Liste dĂ©taillĂ©e et filtrable de toutes les opĂ©rations +- **Carrousel** : Navigation fluide entre vos diffĂ©rentes balances + +### 📄 Export PDF +- GĂ©nĂ©ration de rapports PDF professionnels +- Consolidation de toutes les opĂ©rations par pĂ©riode +- Une page par balance avec design soignĂ© +- Export direct depuis le dashboard + +### 🔐 SĂ©curitĂ© +- Authentification par utilisateur (login/password) +- Hachage sĂ©curisĂ© des mots de passe (bcrypt) +- Isolation multi-tenant des donnĂ©es +- Sessions sĂ©curisĂ©es + +--- + +## đŸ› ïž Technologies utilisĂ©es + +### **Frontend** +| Technologie | Version | Description | +|-------------|---------|-------------| +| [React](https://react.dev/) | 19.2.0 | Framework UI moderne et performant | +| [TypeScript](https://www.typescriptlang.org/) | 5.8.2 | JavaScript typĂ© pour plus de robustesse | +| [Vite](https://vitejs.dev/) | 6.2.0 | Build tool ultra-rapide | +| [Tailwind CSS](https://tailwindcss.com/) | - | Framework CSS utilitaire | +| [Recharts](https://recharts.org/) | 3.3.0 | BibliothĂšque de graphiques React | +| [React PDF](https://react-pdf.org/) | 4.3.1 | GĂ©nĂ©ration de documents PDF | +| [date-fns](https://date-fns.org/) | 4.1.0 | Manipulation de dates | + +### **Backend** +| Technologie | Description | +|-------------|-------------| +| [FastAPI](https://fastapi.tiangolo.com/) | Framework Python moderne et performant | +| [SQLModel](https://sqlmodel.tiangolo.com/) | ORM basĂ© sur SQLAlchemy et Pydantic | +| [MySQL](https://www.mysql.com/) | Base de donnĂ©es relationnelle | +| [PyMySQL](https://pymysql.readthedocs.io/) | Connecteur MySQL pour Python | +| [Uvicorn](https://www.uvicorn.org/) | Serveur ASGI haute performance | +| [Typer](https://typer.tiangolo.com/) | CLI moderne pour Python | +| [Rich](https://github.com/Textualize/rich) | Rendu de texte enrichi dans le terminal | +| [Passlib](https://passlib.readthedocs.io/) | Hachage sĂ©curisĂ© de mots de passe (bcrypt) | + +--- + +## 📩 Installation + +### PrĂ©requis + +Avant de commencer, assurez-vous d'avoir installĂ© : +- **Node.js** (v16 ou supĂ©rieur) - [TĂ©lĂ©charger](https://nodejs.org/) +- **Python** (v3.8 ou supĂ©rieur) - [TĂ©lĂ©charger](https://www.python.org/) +- **MySQL** (v5.7 ou supĂ©rieur) - [TĂ©lĂ©charger](https://www.mysql.com/) + +### 1ïžâƒŁ Cloner le projet + +```bash +git clone +cd abacus +``` + +### 2ïžâƒŁ Configuration du Backend + +#### Installer les dĂ©pendances Python + +```bash +cd backend +pip install -r requirements.txt +``` + +> **Note** : Il est recommandĂ© d'utiliser un environnement virtuel : +> ```bash +> python -m venv venv +> # Windows +> venv\Scripts\activate +> # Linux/Mac +> source venv/bin/activate +> ``` + +#### Configurer la base de donnĂ©es + +1. **CrĂ©er une base de donnĂ©es MySQL** : + ```sql + CREATE DATABASE abacus CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + ``` + +2. **Configurer les variables d'environnement** : + - Copier le fichier d'exemple : + ```bash + # Linux/Mac + cp .env.example .env + # Windows + copy .env.example .env + ``` + + - Éditer le fichier `.env` et renseigner vos informations MySQL : + ```env + DATABASE_URL=mysql+pymysql://utilisateur:motdepasse@localhost:3306/abacus + ``` + > Remplacez `utilisateur`, `motdepasse` et `localhost:3306` par vos paramĂštres MySQL. + +3. **Initialiser les tables** : + ```bash + python cli.py setup-db + ``` + +### 3ïžâƒŁ Configuration du Frontend + +#### Installer les dĂ©pendances Node.js + +Depuis la racine du projet : + +```bash +npm install +``` + +--- + +## 🚀 Lancement de l'application + +### Mode dĂ©veloppement + +Pour dĂ©velopper avec rechargement automatique, lancez le backend et le frontend dans **deux terminaux sĂ©parĂ©s** : + +#### Terminal 1 : Backend + +```bash +cd backend +python cli.py start +``` + +✅ Le backend sera accessible sur **http://localhost:8000** +- API REST : `http://localhost:8000/api` +- Documentation Swagger : `http://localhost:8000/docs` + +#### Terminal 2 : Frontend + +```bash +npm run dev +``` + +✅ L'application sera accessible sur **http://localhost:9873** + +### Mode production + +Pour lancer l'application complĂšte en production : + +```bash +python run_prod.py +``` + +✅ L'application sera accessible sur **http://0.0.0.0:9874** + +Ce script : +1. Compile le frontend React en version optimisĂ©e +2. Copie les fichiers statiques dans le dossier `backend` +3. Lance le serveur FastAPI en mode production + +--- + +## 📖 Utilisation + +### 1. PremiĂšre connexion + +1. Ouvrez votre navigateur sur `http://localhost:9873` (dev) ou `http://localhost:9874` (prod) +2. **CrĂ©ez un compte** en cliquant sur "Register" +3. Remplissez vos informations (nom d'utilisateur et mot de passe) +4. Connectez-vous avec vos identifiants + +### 2. CrĂ©er votre premiĂšre balance + +1. Sur le dashboard, cliquez sur le bouton **"+ Ajouter une balance"** +2. Remplissez les informations : + - **Nom** : ex. "Compte Principal", "Caisse", "Épargne" + - **Montant initial** : le solde de dĂ©part (peut ĂȘtre 0) +3. Validez + +### 3. Ajouter des opĂ©rations + +1. SĂ©lectionnez une balance dans le carrousel +2. Cliquez sur **"+ Ajouter une opĂ©ration"** +3. Renseignez les dĂ©tails : + - **Type** : Recette ou DĂ©pense + - **Montant** : montant de l'opĂ©ration + - **Date** : date de l'opĂ©ration + - **CatĂ©gorie** : type d'opĂ©ration (salaire, achat, don, etc.) + - **Description** : dĂ©tails complĂ©mentaires +4. Validez + +### 4. Visualiser vos donnĂ©es + +- **Carrousel** : Naviguez entre vos balances avec les flĂšches +- **Graphiques** : Consultez l'Ă©volution de vos revenus/dĂ©penses au fil du temps +- **Tableau** : Visualisez toutes les opĂ©rations en dĂ©tail, triables et filtrables + +### 5. Modifier ou supprimer + +- **OpĂ©rations** : Clic droit sur une opĂ©ration → Modifier ou Supprimer +- **Balances** : Menu contextuel sur chaque carte de balance + +### 6. Exporter en PDF + +1. Cliquez sur le bouton **"Exporter PDF"** dans l'en-tĂȘte +2. Le rapport complet sera gĂ©nĂ©rĂ© et tĂ©lĂ©chargĂ© automatiquement +3. Le PDF contient toutes vos balances et opĂ©rations avec un design professionnel + +--- + +## đŸŽ›ïž Commandes CLI + +Le backend dispose d'un outil CLI (`cli.py`) pour faciliter les tĂąches courantes : + +| Commande | Description | +|----------|-------------| +| `python cli.py start` | DĂ©marre le serveur de dĂ©veloppement FastAPI (avec rechargement automatique) | +| `python cli.py setup-db` | CrĂ©e toutes les tables nĂ©cessaires dans la base de donnĂ©es | +| `python cli.py reset-db` | ⚠ **DANGER** : Supprime et recrĂ©e toutes les tables (perte de donnĂ©es) | + +**Exemples** : + +```bash +# DĂ©marrer le serveur +python cli.py start + +# CrĂ©er les tables (premiĂšre installation) +python cli.py setup-db + +# RĂ©initialiser complĂštement la base (dĂ©veloppement uniquement) +python cli.py reset-db +``` + +--- + +## đŸ—ïž Architecture + +``` +abacus/ +├── backend/ # Backend FastAPI +│ ├── api/ # Routes API +│ ├── models/ # ModĂšles SQLModel +│ ├── database.py # Configuration DB +│ ├── cli.py # Outil CLI +│ ├── .env # Variables d'environnement +│ └── requirements.txt # DĂ©pendances Python +│ +├── components/ # Composants React +│ ├── Dashboard.tsx +│ ├── BalanceCard.tsx +│ ├── OperationsTable.tsx +│ ├── OperationsChart.tsx +│ ├── AddBalanceModal.tsx +│ ├── AddOperationModal.tsx +│ ├── ExportButton.tsx +│ ├── PDFDocument.tsx +│ └── ... +│ +├── public/ # Ressources statiques +├── App.tsx # Composant principal +├── api.ts # Client API +├── types.ts # Types TypeScript +├── index.tsx # Point d'entrĂ©e React +├── vite.config.ts # Configuration Vite +├── package.json # DĂ©pendances Node.js +└── README.md # Ce fichier +``` + +### Flux de donnĂ©es + +1. **Frontend** (React) → HTTP Request → **Backend** (FastAPI) +2. **Backend** → SQL Query → **Database** (MySQL) +3. **Database** → Data → **Backend** → JSON Response → **Frontend** + +### Multi-tenant + +Chaque utilisateur a ses propres donnĂ©es isolĂ©es. Le backend filtre automatiquement toutes les requĂȘtes en fonction de l'utilisateur connectĂ©. + +--- + +## 📄 Licence + +Ce projet est sous licence **CC BY-NC-SA 4.0** (Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les MĂȘmes Conditions). + +**Auteur** : Coodlab, Mallevaey Lino +**Version** : 2025.11.22 + +--- + +## đŸ€ Contribution + +Les contributions sont les bienvenues ! N'hĂ©sitez pas Ă  : +- Signaler des bugs +- Proposer de nouvelles fonctionnalitĂ©s +- Soumettre des pull requests + +--- + +## 📞 Support + +Pour toute question ou problĂšme : +- Ouvrez une issue sur le dĂ©pĂŽt GitHub +- Consultez la documentation Swagger : `http://localhost:8000/docs` + +--- + +
+ +**Fait avec ❀ pour simplifier la comptabilitĂ© associative** + +
diff --git a/api.ts b/api.ts new file mode 100644 index 0000000..c14d51c --- /dev/null +++ b/api.ts @@ -0,0 +1,152 @@ +import { Association, Operation, OperationType, Balance } from './types'; + +const API_URL = '/api'; + +export const api = { + async signup(name: string, password: string, balances: { name: string; amount: string }[]): Promise { + const response = await fetch(`${API_URL}/signup`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, password, balances }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Signup failed'); + } + return response.json(); + }, + + async login(name: string, password: string): Promise { + const response = await fetch(`${API_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, password }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Login failed'); + } + return response.json(); + }, + + async getAssociation(id: string): Promise { + const response = await fetch(`${API_URL}/associations/${id}`); + if (!response.ok) { + throw new Error('Failed to fetch association'); + } + const data = await response.json(); + + const mappedOperations = data.operations.map((op: any) => ({ + ...op, + balanceId: op.balance_id || op.balanceId + })); + + const mappedBalances = data.balances.map((balance: any) => ({ + ...balance, + operations: balance.operations.map((op: any) => ({ + ...op, + balanceId: op.balance_id || op.balanceId + })) + })); + + return { + ...data, + operations: mappedOperations, + balances: mappedBalances + }; + }, + + async createOperation(operation: { + name: string; + description: string; + group: string; + amount: number; + type: OperationType; + date: string; + balance_id: string; + }): Promise { + const response = await fetch(`${API_URL}/operations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(operation), + }); + if (!response.ok) { + throw new Error('Failed to create operation'); + } + const data = await response.json(); + return { + ...data, + balanceId: data.balance_id, + }; + }, + + async updateOperation(operation: Operation): Promise { + const response = await fetch(`${API_URL}/operations/${operation.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: operation.name, + description: operation.description, + group: operation.group, + amount: operation.amount, + type: operation.type, + date: operation.date, + balance_id: operation.balanceId + }), + }); + if (!response.ok) { + throw new Error('Failed to update operation'); + } + const data = await response.json(); + return { + ...data, + balanceId: data.balance_id, + }; + }, + + async deleteOperation(id: string): Promise { + const response = await fetch(`${API_URL}/operations/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete operation'); + } + }, + + async addBalance(name: string, initialAmount: number, associationId: string): Promise { + const response = await fetch(`${API_URL}/balances_add`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, initialAmount, association_id: associationId }), + }); + if (!response.ok) { + throw new Error('Failed to add balance'); + } + return response.json(); + }, + + async updateBalance(balance: Balance): Promise { + const response = await fetch(`${API_URL}/balances/${balance.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: balance.name, + initialAmount: balance.initialAmount, + position: balance.position, + }), + }); + if (!response.ok) { + throw new Error('Failed to update balance'); + } + return response.json(); + }, + + async deleteBalance(balanceId: string): Promise { + const response = await fetch(`${API_URL}/balances/${balanceId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete balance'); + } + } +}; diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..e603683 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1 @@ +DATABASE_URL=mysql+pymysql://user:password@host:port/database_name diff --git a/backend/cli.py b/backend/cli.py new file mode 100644 index 0000000..7bd7cba --- /dev/null +++ b/backend/cli.py @@ -0,0 +1,49 @@ +import typer +import uvicorn +from sqlmodel import SQLModel +from database import engine +from models import Association, Balance, Operation +from rich.console import Console +from rich.panel import Panel + +app = typer.Typer() +console = Console() + +@app.command() +def start( + host: str = "127.0.0.1", + port: int = 8000, + reload: bool = True +): + """ + Start the FastAPI server. + """ + console.print(Panel(f"Starting Abacus Backend on http://{host}:{port}", title="Abacus", style="bold green")) + uvicorn.run("main:app", host=host, port=port, reload=reload) + +@app.command() +def setup_db(): + """ + Create database tables. + """ + console.print("[bold yellow]Creating tables...[/bold yellow]") + SQLModel.metadata.create_all(engine) + console.print("[bold green]Tables created successfully.[/bold green]") + +@app.command() +def reset_db(): + """ + Drop and recreate database tables. + """ + confirm = typer.confirm("Are you sure you want to drop all tables?") + if not confirm: + console.print("[bold red]Aborted.[/bold red]") + raise typer.Abort() + + console.print("[bold red]Dropping all tables...[/bold red]") + SQLModel.metadata.drop_all(engine) + console.print("[bold green]Tables dropped.[/bold green]") + setup_db() + +if __name__ == "__main__": + app() diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..e034fb1 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,16 @@ +from sqlmodel import SQLModel, create_engine, Session +from dotenv import load_dotenv +import os + +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL") + +if not DATABASE_URL: + raise ValueError("DATABASE_URL environment variable is not set") + +engine = create_engine(DATABASE_URL, echo=True) + +def get_session(): + with Session(engine) as session: + yield session diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..daa0637 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,250 @@ +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from sqlmodel import Session, select +from typing import List +from datetime import datetime +import os +from passlib.context import CryptContext + +from database import get_session +from models import Association, Balance, Operation, OperationType, AssociationRead, BalanceRead + +app = FastAPI() + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +origins = [ + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:9873", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +from pydantic import BaseModel + +class BalanceCreate(BaseModel): + name: str + amount: str + +class SignupRequest(BaseModel): + name: str + password: str + balances: List[BalanceCreate] + +class LoginRequest(BaseModel): + name: str + password: str + +class OperationCreate(BaseModel): + name: str + description: str + group: str + amount: float + type: OperationType + date: datetime + balance_id: str + +def association_to_read(association: Association) -> AssociationRead: + all_operations = [] + balance_reads = [] + for balance in association.balances: + ops = balance.operations + all_operations.extend(ops) + balance_reads.append(BalanceRead( + id=balance.id, + name=balance.name, + initialAmount=balance.initialAmount, + position=balance.position, + operations=ops + )) + + return AssociationRead( + id=association.id, + name=association.name, + balances=balance_reads, + operations=all_operations + ) + +@app.post("/api/signup", response_model=AssociationRead) +def signup(request: SignupRequest, session: Session = Depends(get_session)): + statement = select(Association).where(Association.name == request.name) + existing = session.exec(statement).first() + if existing: + raise HTTPException(status_code=400, detail="Association already exists") + + hashed_password = get_password_hash(request.password) + association = Association(name=request.name, password=hashed_password) + session.add(association) + session.commit() + session.refresh(association) + + for b in request.balances: + balance = Balance( + name=b.name, + initialAmount=float(b.amount), + association_id=association.id, + position=0 + ) + session.add(balance) + + session.commit() + session.refresh(association) + return association_to_read(association) + +@app.post("/api/login", response_model=AssociationRead) +def login(request: LoginRequest, session: Session = Depends(get_session)): + statement = select(Association).where(Association.name == request.name) + association = session.exec(statement).first() + if not association or not verify_password(request.password, association.password): + raise HTTPException(status_code=401, detail="Invalid credentials") + + return association_to_read(association) + +@app.get("/api/associations/{association_id}", response_model=AssociationRead) +def get_association(association_id: str, session: Session = Depends(get_session)): + from sqlalchemy.orm import selectinload + statement = select(Association).where(Association.id == association_id).options( + selectinload(Association.balances).selectinload(Balance.operations) + ) + association = session.exec(statement).first() + + if not association: + raise HTTPException(status_code=404, detail="Association not found") + + return association_to_read(association) + +@app.post("/api/operations") +def create_operation(op: OperationCreate, session: Session = Depends(get_session)): + balance = session.get(Balance, op.balance_id) + if not balance: + raise HTTPException(status_code=404, detail="Balance not found") + + operation = Operation( + name=op.name, + description=op.description, + group=op.group, + amount=op.amount, + type=op.type, + date=op.date, + balance_id=op.balance_id + ) + session.add(operation) + session.commit() + session.refresh(operation) + return operation + +@app.delete("/api/operations/{operation_id}") +def delete_operation(operation_id: str, session: Session = Depends(get_session)): + operation = session.get(Operation, operation_id) + if not operation: + raise HTTPException(status_code=404, detail="Operation not found") + session.delete(operation) + session.commit() + session.commit() + return {"ok": True} + +class OperationUpdate(BaseModel): + name: str + description: str + group: str + amount: float + type: OperationType + date: datetime + balance_id: str + +@app.put("/api/operations/{operation_id}") +def update_operation(operation_id: str, op: OperationUpdate, session: Session = Depends(get_session)): + operation = session.get(Operation, operation_id) + if not operation: + raise HTTPException(status_code=404, detail="Operation not found") + + operation.name = op.name + operation.description = op.description + operation.group = op.group + operation.amount = op.amount + operation.type = op.type + operation.date = op.date + operation.balance_id = op.balance_id + + session.add(operation) + session.commit() + session.refresh(operation) + return operation + +@app.post("/api/balances") +def create_balance(balance: BalanceCreate, association_id: str, session: Session = Depends(get_session)): + pass + +class BalanceAddRequest(BaseModel): + name: str + initialAmount: float + association_id: str + +@app.post("/api/balances_add") +def add_balance(request: BalanceAddRequest, session: Session = Depends(get_session)): + statement = select(Balance).where(Balance.association_id == request.association_id).order_by(Balance.position.desc()) + last_balance = session.exec(statement).first() + new_position = (last_balance.position + 1) if last_balance else 0 + + balance = Balance( + name=request.name, + initialAmount=request.initialAmount, + association_id=request.association_id, + position=new_position + ) + session.add(balance) + session.commit() + session.refresh(balance) + return balance + +@app.delete("/api/balances/{balance_id}") +def delete_balance(balance_id: str, session: Session = Depends(get_session)): + balance = session.get(Balance, balance_id) + if not balance: + raise HTTPException(status_code=404, detail="Balance not found") + + session.delete(balance) + session.commit() + return {"ok": True} + +class BalanceUpdate(BaseModel): + name: str + initialAmount: float + position: int + +@app.put("/api/balances/{balance_id}") +def update_balance(balance_id: str, data: BalanceUpdate, session: Session = Depends(get_session)): + balance = session.get(Balance, balance_id) + if not balance: + raise HTTPException(status_code=404, detail="Balance not found") + + balance.name = data.name + balance.initialAmount = data.initialAmount + balance.position = data.position + + session.add(balance) + session.commit() + session.refresh(balance) + return balance + +static_dir = os.path.join(os.path.dirname(__file__), "static") +if os.path.exists(static_dir): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") + +@app.get("/health") +def health_check(): + return {"status": "ok"} diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..221b354 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,52 @@ +from typing import List, Optional +from sqlmodel import Field, Relationship, SQLModel +from enum import Enum +import uuid +from datetime import datetime + +class OperationType(str, Enum): + INCOME = 'income' + EXPENSE = 'expense' + +class Association(SQLModel, table=True): + id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True) + name: str + password: str + + balances: List["Balance"] = Relationship(back_populates="association") + +class Balance(SQLModel, table=True): + id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True) + name: str + initialAmount: float + association_id: Optional[str] = Field(default=None, foreign_key="association.id") + + association: Optional[Association] = Relationship(back_populates="balances") + operations: List["Operation"] = Relationship(back_populates="balance") + position: int = Field(default=0) + +class Operation(SQLModel, table=True): + id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True) + name: str + description: str + group: str + amount: float + type: OperationType + date: datetime + invoice: Optional[str] = None + balance_id: Optional[str] = Field(default=None, foreign_key="balance.id") + + balance: Optional[Balance] = Relationship(back_populates="operations") + +class BalanceRead(SQLModel): + id: str + name: str + initialAmount: float + position: int = 0 + operations: List[Operation] = [] + +class AssociationRead(SQLModel): + id: str + name: str + balances: List[BalanceRead] = [] + operations: List[Operation] = [] diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f5db2b6 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi +uvicorn +sqlmodel +pymysql +python-dotenv +typer +rich +passlib[bcrypt] diff --git a/components/AddBalanceModal.tsx b/components/AddBalanceModal.tsx new file mode 100644 index 0000000..1c500ee --- /dev/null +++ b/components/AddBalanceModal.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { Balance } from '../types'; + +interface AddBalanceModalProps { + isOpen: boolean; + onClose: () => void; + onAddBalance: (balance: Omit) => void; + onUpdateBalance?: (balance: Balance) => void; + balanceToEdit?: Balance | null; +} + +const AddBalanceModal: React.FC = ({ isOpen, onClose, onAddBalance, onUpdateBalance, balanceToEdit }) => { + const [name, setName] = useState(''); + const [initialAmount, setInitialAmount] = useState(''); + + React.useEffect(() => { + if (balanceToEdit) { + setName(balanceToEdit.name); + setInitialAmount(balanceToEdit.initialAmount.toString()); + } else { + setName(''); + setInitialAmount(''); + } + }, [balanceToEdit, isOpen]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!name || !initialAmount) { + alert('Please fill all required fields.'); + return; + } + + if (balanceToEdit && onUpdateBalance) { + onUpdateBalance({ + ...balanceToEdit, + name, + initialAmount: parseFloat(initialAmount), + }); + } else { + onAddBalance({ + name, + initialAmount: parseFloat(initialAmount), + }); + } + resetForm(); + }; + + const resetForm = () => { + setName(''); + setInitialAmount(''); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

{balanceToEdit ? 'Edit Balance' : 'Add New Balance'}

+ +
+
+
+ + setName(e.target.value)} + className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" + placeholder="e.g., Main Account, Savings" + /> +
+ +
+ + setInitialAmount(e.target.value)} + className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" + placeholder="0.00" + /> +
+ +
+ +
+
+
+
+ ); +}; + +export default AddBalanceModal; diff --git a/components/AddOperationModal.tsx b/components/AddOperationModal.tsx new file mode 100644 index 0000000..ba82016 --- /dev/null +++ b/components/AddOperationModal.tsx @@ -0,0 +1,161 @@ + +import React, { useState, useEffect } from 'react'; +import { Balance, Operation, OperationType } from '../types'; + +interface AddOperationModalProps { + isOpen: boolean; + onClose: () => void; + onAddOperation: (operation: Omit) => void; + onUpdateOperation?: (operation: Operation) => void; + operationToEdit?: Operation | null; + balances: Balance[]; +} + +const AddOperationModal: React.FC = ({ isOpen, onClose, onAddOperation, onUpdateOperation, operationToEdit, balances }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [group, setGroup] = useState(''); + const [amount, setAmount] = useState(''); + const [type, setType] = useState(OperationType.EXPENSE); + const [date, setDate] = useState(new Date().toISOString().split('T')[0]); + const [balanceId, setBalanceId] = useState(''); + const [invoice, setInvoice] = useState(null); + + useEffect(() => { + if (operationToEdit) { + setName(operationToEdit.name); + setDescription(operationToEdit.description); + setGroup(operationToEdit.group); + setAmount(operationToEdit.amount.toString()); + setType(operationToEdit.type); + setDate(new Date(operationToEdit.date).toISOString().split('T')[0]); + setBalanceId(operationToEdit.balanceId); + } else if (balances.length > 0) { + setBalanceId(balances[0].id); + resetFormFields(); + } + }, [balances, operationToEdit, isOpen]); + + const resetFormFields = () => { + setName(''); + setDescription(''); + setGroup(''); + setAmount(''); + setType(OperationType.EXPENSE); + setDate(new Date().toISOString().split('T')[0]); + if (balances.length > 0) setBalanceId(balances[0].id); + setInvoice(null); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!name || !amount || !balanceId || !group) { + alert('Please fill all required fields.'); + return; + } + + const operationData = { + balanceId, + name, + description, + group, + amount: parseFloat(amount), + type, + date: new Date(date).toISOString(), + invoice: invoice ? invoice.name : (operationToEdit?.invoice) + }; + + if (operationToEdit && onUpdateOperation) { + onUpdateOperation({ + ...operationData, + id: operationToEdit.id + }); + } else { + onAddOperation(operationData); + } + resetForm(); + }; + + const resetForm = () => { + resetFormFields(); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

{operationToEdit ? 'Edit Operation' : 'Add New Operation'}

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + setName(e.target.value)} className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" /> +
+ +
+ + setGroup(e.target.value)} className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" /> +
+ +
+
+ + setAmount(e.target.value)} className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" /> +
+
+ + setDate(e.target.value)} className="w-full mt-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" /> +
+
+ +
+ + +
+ + {type === OperationType.EXPENSE && ( +
+ + setInvoice(e.target.files ? e.target.files[0] : null)} className="w-full mt-1 text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200" /> +
+ )} + +
+ + +
+
+
+
+ ); +}; + +export default AddOperationModal; diff --git a/components/BalanceCard.tsx b/components/BalanceCard.tsx new file mode 100644 index 0000000..93b09db --- /dev/null +++ b/components/BalanceCard.tsx @@ -0,0 +1,131 @@ + +import React, { useMemo } from 'react'; +import { Balance, Operation, OperationType } from '../types'; + +interface BalanceCardProps { + balance: Balance; + operations: Operation[]; + isSelected: boolean; + onClick: () => void; + onEdit: (balance: Balance) => void; + onDelete: (balanceId: string) => void; +} + +const BalanceCard: React.FC = ({ balance, operations, isSelected, onClick, onEdit, onDelete }) => { + const { totalIncome, totalExpenses, currentBalance } = useMemo(() => { + const totalIncome = operations + .filter(op => op.type === OperationType.INCOME) + .reduce((sum, op) => sum + op.amount, 0); + + const totalExpenses = operations + .filter(op => op.type === OperationType.EXPENSE) + .reduce((sum, op) => sum + op.amount, 0); + + const currentBalance = balance.initialAmount + totalIncome - totalExpenses; + + return { totalIncome, totalExpenses, currentBalance }; + }, [balance, operations]); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount); + }; + + const [openMenu, setOpenMenu] = React.useState(false); + const menuRef = React.useRef(null); + const [menuPosition, setMenuPosition] = React.useState<{ top: number; left: number } | null>(null); + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpenMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const toggleMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + if (openMenu) { + setOpenMenu(false); + return; + } + + const rect = e.currentTarget.getBoundingClientRect(); + setMenuPosition({ + top: rect.bottom, + left: rect.right - 128, + }); + setOpenMenu(true); + }; + + return ( +
+
+

+ {balance.name} +

+ +
+ +

{formatCurrency(currentBalance)}

+
+
+

Income

+

{formatCurrency(totalIncome)}

+
+
+

Expenses

+

{formatCurrency(totalExpenses)}

+
+
+ + {openMenu && menuPosition && ( +
e.stopPropagation()} + > +
+ + +
+
+ )} +
+ ); +}; + +export default BalanceCard; diff --git a/components/ConfirmationModal.tsx b/components/ConfirmationModal.tsx new file mode 100644 index 0000000..fb6561a --- /dev/null +++ b/components/ConfirmationModal.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +interface ConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + isDanger?: boolean; +} + +const ConfirmationModal: React.FC = ({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + isDanger = false, +}) => { + if (!isOpen) return null; + + return ( +
+
+ + + + +
+
+
+
+ {isDanger ? ( + + ) : ( + + + + )} +
+
+ +
+

+ {message} +

+
+
+
+
+
+ + +
+
+
+
+ ); +}; + +export default ConfirmationModal; diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx new file mode 100644 index 0000000..1e4eb40 --- /dev/null +++ b/components/Dashboard.tsx @@ -0,0 +1,362 @@ + +import React, { useState, useMemo } from 'react'; +import { Association, Balance, Operation, OperationType } from '../types'; +import { api } from '../api'; +import Header from './Header'; +import BalanceCard from './BalanceCard'; +import OperationsChart from './OperationsChart'; +import OperationsTable from './OperationsTable'; +import AddOperationModal from './AddOperationModal'; +import AddBalanceModal from './AddBalanceModal'; +import { format, subDays, startOfMonth, endOfMonth } from 'date-fns'; + +interface DashboardProps { + association: Association; + onLogout: () => void; + onUpdateAssociation: (association: Association) => void; +} + +const Dashboard: React.FC = ({ association, onLogout, onUpdateAssociation }) => { + const today = new Date(); + const [dateRange, setDateRange] = useState<{ start: Date, end: Date }>({ start: startOfMonth(today), end: endOfMonth(today) }); + const [selectedBalanceId, setSelectedBalanceId] = useState(association.balances[0]?.id ?? null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isAddBalanceModalOpen, setIsAddBalanceModalOpen] = useState(false); + + const [balanceToEdit, setBalanceToEdit] = useState(null); + const [draggedBalanceIndex, setDraggedBalanceIndex] = useState(null); + + const [operationToEdit, setOperationToEdit] = useState(null); + const scrollContainerRef = React.useRef(null); + + const scroll = (direction: 'left' | 'right') => { + if (scrollContainerRef.current) { + const scrollAmount = 320; + const newScrollLeft = scrollContainerRef.current.scrollLeft + (direction === 'right' ? scrollAmount : -scrollAmount); + scrollContainerRef.current.scrollTo({ + left: newScrollLeft, + behavior: 'smooth' + }); + } + }; + + const filteredOperations = useMemo(() => { + return association.operations.filter(op => { + const opDate = new Date(op.date); + return opDate >= dateRange.start && opDate <= dateRange.end; + }); + }, [association.operations, dateRange]); + + const selectedBalance = useMemo(() => { + return association.balances.find(b => b.id === selectedBalanceId) ?? null; + }, [association.balances, selectedBalanceId]); + + const operationsForSelectedBalance = useMemo(() => { + return filteredOperations.filter(op => op.balanceId === selectedBalanceId); + }, [filteredOperations, selectedBalanceId]); + + const incomesForSelectedBalance = operationsForSelectedBalance.filter(op => op.type === OperationType.INCOME); + const expensesForSelectedBalance = operationsForSelectedBalance.filter(op => op.type === OperationType.EXPENSE); + + const handleAddOperation = async (newOperationData: Omit) => { + try { + const newOperation = await api.createOperation({ + ...newOperationData, + balance_id: newOperationData.balanceId, + date: newOperationData.date, + }); + + const updatedAssociation = { + ...association, + operations: [...association.operations, newOperation] + }; + onUpdateAssociation(updatedAssociation); + setIsModalOpen(false); + } catch (error) { + console.error("Failed to add operation", error); + alert("Failed to add operation"); + } + }; + + const handleUpdateOperation = async (updatedOperation: Operation) => { + try { + const result = await api.updateOperation({ + ...updatedOperation, + balanceId: updatedOperation.balanceId, + }); + + const updatedAssociation = { + ...association, + operations: association.operations.map(op => op.id === result.id ? result : op) + }; + onUpdateAssociation(updatedAssociation); + setIsModalOpen(false); + setOperationToEdit(null); + } catch (error) { + console.error("Failed to update operation", error); + alert("Failed to update operation"); + } + }; + + const handleDeleteOperation = async (operationId: string) => { + if (window.confirm('Are you sure you want to delete this operation?')) { + try { + await api.deleteOperation(operationId); + const updatedAssociation = { + ...association, + operations: association.operations.filter(op => op.id !== operationId) + }; + onUpdateAssociation(updatedAssociation); + } catch (error) { + console.error("Failed to delete operation", error); + alert("Failed to delete operation"); + } + } + }; + + const handleEditOperation = (operation: Operation) => { + setOperationToEdit(operation); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setOperationToEdit(null); + }; + + const handleAddBalance = async (newBalanceData: Omit) => { + try { + const newBalance = await api.addBalance(newBalanceData.name, newBalanceData.initialAmount, association.id); + const updatedAssociation = { + ...association, + balances: [...association.balances, newBalance] + }; + onUpdateAssociation(updatedAssociation); + setIsAddBalanceModalOpen(false); + setSelectedBalanceId(newBalance.id); + } catch (error) { + console.error("Failed to add balance", error); + alert("Failed to add balance"); + } + }; + + const handleEditBalance = (balance: Balance) => { + setBalanceToEdit(balance); + setIsAddBalanceModalOpen(true); + }; + + const handleDeleteBalance = async (balanceId: string) => { + if (window.confirm('Are you sure you want to delete this balance? All associated operations will be lost.')) { + try { + await api.deleteBalance(balanceId); + const updatedAssociation = { + ...association, + balances: association.balances.filter(b => b.id !== balanceId) + }; + onUpdateAssociation(updatedAssociation); + if (selectedBalanceId === balanceId) { + setSelectedBalanceId(updatedAssociation.balances[0]?.id ?? null); + } + } catch (error) { + console.error("Failed to delete balance", error); + alert("Failed to delete balance"); + } + } + }; + + const handleUpdateBalance = async (updatedBalance: Balance) => { + try { + const result = await api.updateBalance(updatedBalance); + const updatedAssociation = { + ...association, + balances: association.balances.map(b => b.id === result.id ? result : b) + }; + onUpdateAssociation(updatedAssociation); + setIsAddBalanceModalOpen(false); + setBalanceToEdit(null); + } catch (error) { + console.error("Failed to update balance", error); + alert("Failed to update balance"); + } + }; + + const handleDragStart = (index: number) => { + setDraggedBalanceIndex(index); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = async (dropIndex: number) => { + if (draggedBalanceIndex === null || draggedBalanceIndex === dropIndex) return; + + const newBalances = [...association.balances]; + const [draggedBalance] = newBalances.splice(draggedBalanceIndex, 1); + newBalances.splice(dropIndex, 0, draggedBalance); + + const updatedBalances = newBalances.map((b, index) => ({ + ...b, + position: index + })); + + onUpdateAssociation({ + ...association, + balances: updatedBalances + }); + + try { + await Promise.all(updatedBalances.map(b => api.updateBalance(b))); + } catch (error) { + console.error("Failed to update balance positions", error); + alert("Failed to save new order"); + } + setDraggedBalanceIndex(null); + }; + + return ( +
+ {/* ... Header ... */} +
+
+
+

Balances

+
+ + +
+
+ +
+ + +
+ {association.balances + .sort((a, b) => (a.position - b.position)) + .map((balance, index) => ( +
handleDragStart(index)} + onDragOver={handleDragOver} + onDrop={() => handleDrop(index)} + > + op.balanceId === balance.id)} + isSelected={selectedBalanceId === balance.id} + onClick={() => setSelectedBalanceId(balance.id)} + onEdit={handleEditBalance} + onDelete={handleDeleteBalance} + /> +
+ ))} +
+ + +
+ +
+
+

Balances Variation

+ +
+ + {selectedBalance && ( + <> +
+

+ Operations for {selectedBalance.name} +

+
+ + +
+
+ + )} +
+ +
+ + { + setIsAddBalanceModalOpen(false); + setBalanceToEdit(null); + }} + onAddBalance={handleAddBalance} + onUpdateBalance={handleUpdateBalance} + balanceToEdit={balanceToEdit} + /> +
+ ); +}; + +export default Dashboard; diff --git a/components/ExportButton.tsx b/components/ExportButton.tsx new file mode 100644 index 0000000..e116f1b --- /dev/null +++ b/components/ExportButton.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { PDFDownloadLink } from '@react-pdf/renderer'; +import PDFDocument from './PDFDocument'; +import { Operation, Balance } from '../types'; +import { format } from 'date-fns'; + +interface ExportButtonProps { + operations: Operation[]; + balances: Balance[]; + dateRange: { start: Date; end: Date }; + associationName: string; +} + +const ExportButton: React.FC = ({ operations, balances, dateRange, associationName }) => { + return ( + + } + fileName={`abacus_export_${format(dateRange.start, 'yyyy-MM-dd')}_${format(dateRange.end, 'yyyy-MM-dd')}.pdf`} + className="text-sm font-medium text-gray-600 hover:text-gray-900 transition flex items-center space-x-1" + > + {({ blob, url, loading, error }) => ( + <> + + + + {loading ? 'Loading...' : 'Export PDF'} + + )} + + ); +}; + +export default ExportButton; diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..d9e32d0 --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,70 @@ + +import React from 'react'; +import { format, parseISO } from 'date-fns'; + +import { Operation, Balance } from '../types'; +import ExportButton from './ExportButton'; + +interface HeaderProps { + associationName: string; + onLogout: () => void; + dateRange: { start: Date, end: Date }; + setDateRange: (range: { start: Date, end: Date }) => void; + operations: Operation[]; + balances: Balance[]; +} + +const Header: React.FC = ({ associationName, onLogout, dateRange, setDateRange, operations, balances }) => { + + const handleDateChange = (field: 'start' | 'end', value: string) => { + const newDate = parseISO(value); + if (field === 'start' && newDate < dateRange.end) { + setDateRange({ ...dateRange, start: newDate }); + } + if (field === 'end' && newDate > dateRange.start) { + setDateRange({ ...dateRange, end: newDate }); + } + }; + + return ( +
+
+
+
+ Abacus + | +

{associationName}

+
+
+
+ handleDateChange('start', e.target.value)} + className="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-gray-800" + /> + - + handleDateChange('end', e.target.value)} + className="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-gray-800" + /> +
+ + +
+
+
+
+ ); +}; + +export default Header; diff --git a/components/LoginScreen.tsx b/components/LoginScreen.tsx new file mode 100644 index 0000000..96a8539 --- /dev/null +++ b/components/LoginScreen.tsx @@ -0,0 +1,157 @@ + +import React, { useState } from 'react'; +import { Association, Balance } from '../types'; +import { api } from '../api'; + +interface LoginScreenProps { + onLogin: (association: Association) => void; +} + +const LoginScreen: React.FC = ({ onLogin }) => { + const [isLoginView, setIsLoginView] = useState(true); + const [associationName, setAssociationName] = useState(''); + const [password, setPassword] = useState(''); + const [initialBalances, setInitialBalances] = useState<{ name: string; amount: string }[]>([{ name: '', amount: '' }]); + const [error, setError] = useState(''); + + const handleAddBalance = () => { + setInitialBalances([...initialBalances, { name: '', amount: '' }]); + }; + + const handleRemoveBalance = (index: number) => { + setInitialBalances(initialBalances.filter((_, i) => i !== index)); + }; + + const handleBalanceChange = (index: number, field: 'name' | 'amount', value: string) => { + const newBalances = [...initialBalances]; + newBalances[index][field] = value; + setInitialBalances(newBalances); + }; + + const validateSignup = () => { + if (!associationName.trim() || !password.trim()) { + setError('Association name and password are required.'); + return false; + } + if (initialBalances.some(b => !b.name.trim() || !b.amount.trim() || isNaN(parseFloat(b.amount)))) { + setError('All balance fields must be filled correctly.'); + return false; + } + return true; + }; + + const handleSignup = async () => { + setError(''); + if (!validateSignup()) return; + + try { + const newAssociation = await api.signup(associationName.trim(), password, initialBalances.map(b => ({ name: b.name.trim(), amount: b.amount }))); + onLogin(newAssociation); + } catch (err: any) { + setError(err.message || 'Signup failed'); + } + }; + + const handleLogin = async () => { + setError(''); + if (!associationName.trim() || !password.trim()) { + setError('Please enter association name and password.'); + return; + } + + try { + const association = await api.login(associationName.trim(), password); + onLogin(association); + } catch (err: any) { + setError(err.message || 'Login failed'); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isLoginView) { + handleLogin(); + } else { + handleSignup(); + } + } + + return ( +
+
+

Abacus

+

Simplified accounting for your association.

+
+

{isLoginView ? 'Login' : 'Create Account'}

+ +
+ setAssociationName(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" + /> + setPassword(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-800 transition" + /> + + {!isLoginView && ( +
+

Initial Balances

+
+ {initialBalances.map((balance, index) => ( +
+ handleBalanceChange(index, 'name', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" + /> + handleBalanceChange(index, 'amount', e.target.value)} + className="w-40 px-3 py-2 border border-gray-300 rounded-lg text-sm" + /> + {initialBalances.length > 1 && ( + + )} +
+ ))} +
+ +
+ )} + + {error &&

{error}

} + + +
+ +

+ {isLoginView ? "Don't have an account?" : 'Already have an account?'} + +

+
+
+
+ ); +}; + +export default LoginScreen; diff --git a/components/OperationsChart.tsx b/components/OperationsChart.tsx new file mode 100644 index 0000000..c6fc9de --- /dev/null +++ b/components/OperationsChart.tsx @@ -0,0 +1,85 @@ + +import React, { useMemo } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { Balance, Operation } from '../types'; +import { format, eachDayOfInterval, isBefore, isEqual } from 'date-fns'; + +interface OperationsChartProps { + balances: Balance[]; + allOperations: Operation[]; + dateRange: { start: Date, end: Date }; +} + +const OperationsChart: React.FC = ({ balances, allOperations, dateRange }) => { + + const chartData = useMemo(() => { + const days = eachDayOfInterval(dateRange); + + const balanceColors = ['#1f2937', '#6b7280', '#ef4444', '#10b981', '#3b82f6', '#a855f7']; + + const data = days.map(day => { + const entry: { date: string, [key: string]: number | string } = { + date: format(day, 'MMM dd'), + }; + + balances.forEach(balance => { + const balanceAtStartOfRange = balance.initialAmount + allOperations + .filter(op => op.balanceId === balance.id && isBefore(new Date(op.date), dateRange.start)) + .reduce((acc, op) => acc + (op.type === 'income' ? op.amount : -op.amount), 0); + + const dailyTotal = allOperations + .filter(op => op.balanceId === balance.id && isBefore(new Date(op.date), day) && isEqual(new Date(op.date), day)) + .reduce((acc, op) => acc + (op.type === 'income' ? op.amount : -op.amount), balanceAtStartOfRange); + + const operationsInPeriodUntilDay = allOperations + .filter(op => { + const opDate = new Date(op.date); + return op.balanceId === balance.id && + opDate >= dateRange.start && + opDate <= day; + }) + .reduce((acc, op) => acc + (op.type === 'income' ? op.amount : -op.amount), 0); + + entry[balance.name] = balanceAtStartOfRange + operationsInPeriodUntilDay; + }); + + return entry; + }); + + return { data, colors: balanceColors }; + }, [balances, allOperations, dateRange]); + + + if (!balances.length) { + return

No balances to display.

+ } + + return ( +
+ + + + + new Intl.NumberFormat('fr-FR', { notation: 'compact', compactDisplay: 'short' }).format(value as number)}/> + new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value as number)} + /> + + {balances.map((balance, index) => ( + + ))} + + +
+ ); +}; + +export default OperationsChart; diff --git a/components/OperationsTable.tsx b/components/OperationsTable.tsx new file mode 100644 index 0000000..9d04b73 --- /dev/null +++ b/components/OperationsTable.tsx @@ -0,0 +1,207 @@ +import React, { useMemo, useState, useRef, useEffect } from 'react'; +import { Operation } from '../types'; +import { format } from 'date-fns'; +import ConfirmationModal from './ConfirmationModal'; + +interface OperationsTableProps { + title: 'Income' | 'Expenses'; + operations: Operation[]; + onEdit: (operation: Operation) => void; + onDelete: (operationId: string) => void; +} + +const OperationsTable: React.FC = ({ title, operations, onEdit, onDelete }) => { + const [openMenuId, setOpenMenuId] = useState(null); + const menuRef = useRef(null); + const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); + const [operationToDelete, setOperationToDelete] = useState(null); + const [collapsedGroups, setCollapsedGroups] = useState>({}); + + const toggleGroup = (group: string) => { + setCollapsedGroups(prev => ({ + ...prev, + [group]: !prev[group] + })); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpenMenuId(null); + setMenuPosition(null); + } + }; + const handleScroll = () => { + if (openMenuId) { + setOpenMenuId(null); + setMenuPosition(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); + }; + }, [openMenuId]); + + const toggleMenu = (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + if (openMenuId === id) { + setOpenMenuId(null); + setMenuPosition(null); + } else { + const rect = e.currentTarget.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const menuHeight = 100; + + let top = rect.bottom; + if (spaceBelow < menuHeight) { + top = rect.top - menuHeight; + } + + setMenuPosition({ + top: top, + left: rect.right - 128, + }); + setOpenMenuId(id); + } + }; + + const handleDeleteClick = (op: Operation) => { + setOperationToDelete(op); + setOpenMenuId(null); + setMenuPosition(null); + }; + + const confirmDelete = () => { + if (operationToDelete) { + onDelete(operationToDelete.id); + setOperationToDelete(null); + } + }; + + const groupedOperations = useMemo(() => { + return operations.reduce((acc, op) => { + (acc[op.group] = acc[op.group] || []).push(op); + return acc; + }, {} as Record); + }, [operations]); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount); + }; + + const titleColor = title === 'Income' ? 'text-green-600' : 'text-red-600'; + + return ( +
+

{title}

+
+ {Object.keys(groupedOperations).length > 0 ? ( + Object.keys(groupedOperations).map((group) => { + const ops = groupedOperations[group]; + return ( +
+
toggleGroup(group)} + > +
{group}
+ + + +
+ {!collapsedGroups[group] && ( +
+ {ops.map(op => ( +
+
+
+

{op.name}

+

{format(new Date(op.date), 'MMM dd, yyyy')}

+
+

{op.description}

+
+ +
+
+ {formatCurrency(op.amount)} +
+
+ +
+
+
+ ))} +
+ )} +
+ ); + }) + ) : ( +
+

No {title.toLowerCase()} for this period.

+
+ )} +
+ + {/* Fixed Menu Portal */} + {openMenuId && menuPosition && ( +
+
+ + +
+
+ )} + + setOperationToDelete(null)} + onConfirm={confirmDelete} + title="Delete Operation" + message={`Are you sure you want to delete the operation "${operationToDelete?.name}"? This action cannot be undone.`} + confirmText="Delete" + isDanger={true} + /> +
+ ); +}; + +export default OperationsTable; diff --git a/components/PDFDocument.tsx b/components/PDFDocument.tsx new file mode 100644 index 0000000..fe0c9dc --- /dev/null +++ b/components/PDFDocument.tsx @@ -0,0 +1,291 @@ +import React from 'react'; +import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer'; +import { format } from 'date-fns'; +import { Operation, Balance, OperationType } from '../types'; + +const styles = StyleSheet.create({ + page: { + flexDirection: 'column', + backgroundColor: '#F9FAFB', + padding: 35, + fontFamily: 'Helvetica', + }, + header: { + marginBottom: 25, + paddingBottom: 15, + paddingTop: 20, + paddingLeft: 25, + paddingRight: 25, + backgroundColor: '#1F2937', + borderRadius: 8, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 8, + letterSpacing: 0.5, + }, + subtitle: { + fontSize: 12, + color: '#D1D5DB', + fontWeight: 'normal', + }, + balanceSection: { + marginBottom: 20, + }, + balanceTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#1F2937', + marginTop: 15, + marginBottom: 12, + paddingBottom: 8, + paddingLeft: 10, + borderLeftWidth: 4, + borderLeftColor: '#3B82F6', + backgroundColor: '#EFF6FF', + paddingTop: 8, + paddingRight: 10, + }, + table: { + display: 'flex', + width: 'auto', + borderRadius: 6, + overflow: 'hidden', + borderStyle: 'solid', + borderWidth: 1, + borderColor: '#D1D5DB', + backgroundColor: '#FFFFFF', + }, + tableRow: { + margin: 'auto', + flexDirection: 'row', + }, + tableColHeader: { + width: '25%', + borderStyle: 'solid', + borderWidth: 1, + borderLeftWidth: 0, + borderTopWidth: 0, + borderColor: '#E5E7EB', + backgroundColor: '#F3F4F6', + padding: 8, + }, + tableCol: { + width: '25%', + borderStyle: 'solid', + borderWidth: 1, + borderLeftWidth: 0, + borderTopWidth: 0, + borderColor: '#E5E7EB', + padding: 8, + backgroundColor: '#FFFFFF', + }, + tableCellHeader: { + margin: 'auto', + fontSize: 10, + fontWeight: 'bold', + color: '#1F2937', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + tableCell: { + margin: 'auto', + fontSize: 10, + color: '#4B5563', + }, + tableCellIncome: { + margin: 'auto', + fontSize: 10, + color: '#059669', + fontWeight: 'bold', + }, + tableCellExpense: { + margin: 'auto', + fontSize: 10, + color: '#DC2626', + fontWeight: 'bold', + }, + tableCellBalance: { + margin: 'auto', + fontSize: 10, + color: '#1F2937', + fontWeight: 'bold', + }, + totalRow: { + flexDirection: 'row', + marginTop: 12, + justifyContent: 'flex-end', + backgroundColor: '#EFF6FF', + padding: 10, + borderRadius: 6, + }, + totalText: { + fontSize: 13, + fontWeight: 'bold', + color: '#1F2937', + }, + emptyMessage: { + fontSize: 11, + color: '#9CA3AF', + fontStyle: 'italic', + padding: 20, + textAlign: 'center', + backgroundColor: '#F9FAFB', + borderRadius: 6, + }, + summaryCard: { + backgroundColor: '#FFFFFF', + padding: 15, + borderRadius: 8, + marginBottom: 15, + borderLeftWidth: 4, + borderLeftColor: '#3B82F6', + } +}); + +interface PDFDocumentProps { + operations: Operation[]; + balances: Balance[]; + dateRange: { start: Date; end: Date }; + associationName: string; +} + +const PDFDocument: React.FC = ({ operations, balances, dateRange, associationName }) => { + + const summaryData = balances.map(balance => { + const balanceOps = operations.filter(op => op.balanceId === balance.id); + + const previousOps = balanceOps.filter(op => new Date(op.date) < dateRange.start); + const initialIncome = previousOps.filter(op => op.type === OperationType.INCOME).reduce((sum, op) => sum + op.amount, 0); + const initialExpense = previousOps.filter(op => op.type === OperationType.EXPENSE).reduce((sum, op) => sum + op.amount, 0); + const startBalance = balance.initialAmount + initialIncome - initialExpense; + + const periodOps = balanceOps.filter(op => { + const opDate = new Date(op.date); + return opDate >= dateRange.start && opDate <= dateRange.end; + }); + + const periodIncome = periodOps.filter(op => op.type === OperationType.INCOME).reduce((sum, op) => sum + op.amount, 0); + const periodExpense = periodOps.filter(op => op.type === OperationType.EXPENSE).reduce((sum, op) => sum + op.amount, 0); + const endBalance = startBalance + periodIncome - periodExpense; + + return { + balance, + startBalance, + endBalance, + periodIncome, + periodExpense, + periodOps: periodOps.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + }; + }); + + return ( + + {/* Summary Page */} + + + {associationName} + + Report Period: {format(dateRange.start, 'MMM dd, yyyy')} - {format(dateRange.end, 'MMM dd, yyyy')} + + + + Summary + + + + Balance + + + Start + + + Income + + + Expense + + + End + + + {summaryData.map((data) => ( + + + {data.balance.name} + + + {data.startBalance.toFixed(2)} € + + + +{data.periodIncome.toFixed(2)} € + + + -{data.periodExpense.toFixed(2)} € + + + {data.endBalance.toFixed(2)} € + + + ))} + + + + {/* Detailed Pages */} + {summaryData.map((data) => ( + + + {data.balance.name} - Details + + {data.periodOps.length > 0 ? ( + + + + Date + + + Name + + + Type + + + Amount + + + {data.periodOps.map((op) => ( + + + {format(new Date(op.date), 'MMM dd, yyyy')} + + + {op.name} + + + {op.type} + + + + {op.type === OperationType.EXPENSE ? '-' : '+'}{op.amount.toFixed(2)} € + + + + ))} + + ) : ( + No operations for this period. + )} + + + End Balance: {data.endBalance.toFixed(2)} € + + + + ))} + + ); +}; + +export default PDFDocument; diff --git a/index.html b/index.html new file mode 100644 index 0000000..47f4bd9 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + + + + Abacus - Coodlab + + + + + + +
+ + + + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..aaa0c6e --- /dev/null +++ b/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8017588 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2759 @@ +{ + "name": "abacus", + "version": "2025.11.22", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "abacus", + "version": "2025.11.22", + "dependencies": { + "@react-pdf/renderer": "^4.3.1", + "date-fns": "^4.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^3.3.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@react-pdf/fns": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz", + "integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.3.tgz", + "integrity": "sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^4.0.4", + "@react-pdf/types": "^2.9.1", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz", + "integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/png-js": "^3.0.0", + "jay-peg": "^1.1.1" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.1.tgz", + "integrity": "sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/image": "^3.0.3", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.1", + "@react-pdf/textkit": "^6.0.0", + "@react-pdf/types": "^2.9.1", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.4.tgz", + "integrity": "sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/png-js": "^3.0.0", + "browserify-zlib": "^0.2.0", + "crypto-js": "^4.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "linebreak": "^1.1.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/png-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz", + "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==", + "license": "MIT", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz", + "integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz", + "integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/reconciler/node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/@react-pdf/render": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.1.tgz", + "integrity": "sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/textkit": "^6.0.0", + "@react-pdf/types": "^2.9.1", + "abs-svg-path": "^0.1.1", + "color-string": "^1.9.1", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.1.tgz", + "integrity": "sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/font": "^4.0.3", + "@react-pdf/layout": "^4.4.1", + "@react-pdf/pdfkit": "^4.0.4", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/reconciler": "^1.1.4", + "@react-pdf/render": "^4.3.1", + "@react-pdf/types": "^2.9.1", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.1.tgz", + "integrity": "sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/types": "^2.9.1", + "color-string": "^1.9.1", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz", + "integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.1.tgz", + "integrity": "sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.3", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.1" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/baseline-browser-mapping": { + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "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/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/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, + "node_modules/hyphen": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz", + "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==", + "license": "ISC" + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "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==", + "dev": true, + "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-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.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/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "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/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "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/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/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c783690 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "abacus", + "private": false, + "version": "2025.11.22", + "description": "Abacus is a web application for association accounting.", + "author": "Coodlab, Mallevaey Lino", + "license": "CC BY-NC-SA 4.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@react-pdf/renderer": "^4.3.1", + "date-fns": "^4.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^3.3.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} \ No newline at end of file diff --git a/public/abacus.svg b/public/abacus.svg new file mode 100644 index 0000000..6b93224 --- /dev/null +++ b/public/abacus.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..19b9584 Binary files /dev/null and b/public/icon.png differ diff --git a/run_prod.py b/run_prod.py new file mode 100644 index 0000000..8cb371b --- /dev/null +++ b/run_prod.py @@ -0,0 +1,42 @@ +import subprocess +import shutil +import os +import sys + +def run_command(command, cwd=None): + print(f"Running: {command}") + result = subprocess.run(command, shell=True, cwd=cwd) + if result.returncode != 0: + print(f"Error running command: {command}") + sys.exit(1) + +def main(): + root_dir = os.path.dirname(os.path.abspath(__file__)) + backend_dir = os.path.join(root_dir, "backend") + static_dir = os.path.join(backend_dir, "static") + + print("Building Frontend...") + run_command("npm run build", cwd=root_dir) + + print("Copying build artifacts to backend/static...") + if os.path.exists(static_dir): + shutil.rmtree(static_dir) + + dist_dir = os.path.join(root_dir, "dist") + if not os.path.exists(dist_dir): + print("Error: dist directory not found. Build failed?") + sys.exit(1) + + shutil.copytree(dist_dir, static_dir) + print("Artifacts copied.") + + print("Starting Production Server...") + os.environ["PYTHONPATH"] = backend_dir + run_command("python cli.py start --host 0.0.0.0 --port 9874 --no-reload", cwd=backend_dir) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nShutting down...") + sys.exit(0) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..1171cbb --- /dev/null +++ b/types.ts @@ -0,0 +1,32 @@ + +export enum OperationType { + INCOME = 'income', + EXPENSE = 'expense', +} + +export interface Operation { + id: string; + balanceId: string; + name: string; + description: string; + group: string; + amount: number; + type: OperationType; + date: string; + invoice?: string; +} + +export interface Balance { + id: string; + name: string; + initialAmount: number; + position: number; +} + +export interface Association { + id: string; + name: string; + password: string; + balances: Balance[]; + operations: Operation[]; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f7a49f9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,29 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + server: { + port: 9873, + host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + } + } + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +});