commit ab3c6131653b1da4f137ea0aae324ca665c54b54 Author: George Date: Mon Aug 17 10:34:12 2026 -0500 Initial commit: mini-erp (backend FastAPI + frontend React/Vite) Base para el flujo de ramas por cliente. Co-Authored-By: Claude Sonnet 5 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ffcf710 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Variables para docker-compose.yml (no confundir con mini-backend/.env, +# que tiene las credenciales reales de conexión a SAP Service Layer). +# +# Copiar este archivo a ".env" en esta misma carpeta antes de levantar +# docker-compose, y ajustar VITE_API_URL a la IP o hostname real del +# servidor donde correrá el backend dentro de la red de la empresa. +# NO usar "localhost": eso resolvería a la máquina de cada usuario, no +# al servidor. + +VITE_API_URL=http://192.168.1.50:8000 + +# Zona horaria de los contenedores (tzdata). Por defecto Perú; usar otra +# zona de America/* si el despliegue es para un cliente en otro país. +TZ=America/Lima diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..988d151 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Sistema +.DS_Store + +# --- Backend (Python/FastAPI) --- +mini-backend/backend-venv/ +mini-backend/__pycache__/ +mini-backend/**/__pycache__/ +mini-backend/*.pyc +mini-backend/logs/ +mini-backend/.env +mini-backend/.env.* +mini-backend/certs/ + +# --- Frontend (React/Vite) --- +mini-frontend/node_modules/ +mini-frontend/dist/ +mini-frontend/dist-ssr/ +mini-frontend/*.local +mini-frontend/.env +mini-frontend/.env.* +mini-frontend/certs/ + +# Editor +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Logs genéricos +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Root env +.env +.env.* +!*.env.example +!.env.example diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6f9e203 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + backend: + build: + context: ./mini-backend + args: + TZ: ${TZ:-America/Lima} + ports: + - "8000:8000" + env_file: + - ./mini-backend/.env + volumes: + # Se lee del disco en cada request (routers/permissions.py), no se + # cachea en memoria: editar este archivo en el host y guardarlo ya + # aplica el cambio, sin reiniciar el contenedor. + - ./mini-backend/menu_permissions.json:/app/menu_permissions.json:ro + - ./mini-backend/logs:/app/logs + restart: always + + frontend: + build: + context: ./mini-frontend + args: + VITE_API_URL: ${VITE_API_URL:-http://localhost:8000} + TZ: ${TZ:-America/Lima} + ports: + - "80:80" + depends_on: + - backend + restart: always diff --git a/mini-backend/.dockerignore b/mini-backend/.dockerignore new file mode 100644 index 0000000..0719c41 --- /dev/null +++ b/mini-backend/.dockerignore @@ -0,0 +1,9 @@ +.env +.env.aberio +backend-env/ +__pycache__/ +*.pyc +logs/ +test/ +docs/ +.DS_Store diff --git a/mini-backend/Dockerfile b/mini-backend/Dockerfile new file mode 100644 index 0000000..12a6b42 --- /dev/null +++ b/mini-backend/Dockerfile @@ -0,0 +1,48 @@ +FROM python:3.12-slim + +# Zona horaria del contenedor. Por defecto Perú (America/Lima), a juego con +# los datos de SAP que usa esta app (Country "PE", SUNAT, soles); cambiar el +# build arg si se despliega para un cliente en otro país de Latinoamérica. +ARG TZ=America/Lima +ENV TZ=${TZ} +RUN apt-get update \ + && apt-get install -y --no-install-recommends tzdata \ + && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo ${TZ} > /etc/timezone \ + && rm -rf /var/lib/apt/lists/* + +# Librerías nativas que necesita WeasyPrint (routers/reports.py) para +# renderizar HTML/CSS a PDF: no vienen incluidas en python:3.12-slim. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libpangoft2-1.0-0 \ + libcairo2 \ + libgdk-pixbuf-2.0-0 \ + shared-mime-info \ + fonts-liberation \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Certificado extra de la empresa (VPN, proxy o antivirus corporativo que +# intercepta HTTPS, ej. Zscaler/Netskope). Si mini-backend/certs/ tiene un +# archivo .pem/.crt, pip confía en él para poder salir a internet durante el +# build. Si la carpeta está vacía (caso normal), este paso no hace nada. +COPY certs/ /usr/local/share/extra-ca/ +COPY requirements.txt . +RUN CERT_FILE=$(find /usr/local/share/extra-ca -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" -o -name "*.cer" \) 2>/dev/null | head -n1); \ + if [ -n "$CERT_FILE" ]; then export PIP_CERT="$CERT_FILE"; fi; \ + pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +# Un solo worker (por diseño, no un límite temporal): la sesión de SAP y las +# sesiones de empleado viven en diccionarios en memoria del proceso +# (core/sap.py, core/session.py). Con --workers > 1 cada proceso tendría su +# propia copia y los usuarios verían "sesión expirada" de forma intermitente +# según a qué worker los enrutara Docker/el proxy. +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/mini-backend/context.py b/mini-backend/context.py new file mode 100644 index 0000000..443c678 --- /dev/null +++ b/mini-backend/context.py @@ -0,0 +1,40 @@ +from contextvars import ContextVar + +# Variable de contexto única por cada Request de FastAPI +request_id_ctx: ContextVar[str] = ContextVar("request_id", default="-") + +# Variable de contexto para el empleado conectado que originó la petición +current_user_ctx: ContextVar[dict] = ContextVar( + "current_user", default={"webUser": "-", "emp_id": "-", "full_name": "-"} +) + + +# ============================================================================ +def get_request_id() -> str: + """ + Obtiene el ID de la petición del contexto actual. + """ + return request_id_ctx.get() + + +# ============================================================================ +def set_request_id(request_id: str): + """ + Establece el ID de la petición en el contexto actual. + """ + request_id_ctx.set(request_id) +# ============================================================================ +def get_current_user() -> dict: + """ + Obtiene el empleado conectado del contexto actual. + """ + return current_user_ctx.get() + + +# ============================================================================ +def set_current_user(webUser: str, full_name: str = "", emp_id="-"): + """ + Establece el empleado conectado en el contexto actual. + """ + current_user_ctx.set({"webUser": webUser, "emp_id": emp_id, "full_name": full_name}) +# ============================================================================ \ No newline at end of file diff --git a/mini-backend/core/__init__.py b/mini-backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mini-backend/core/api_gateway.py b/mini-backend/core/api_gateway.py new file mode 100644 index 0000000..232fde5 --- /dev/null +++ b/mini-backend/core/api_gateway.py @@ -0,0 +1,130 @@ +from dotenv import load_dotenv +import logging +import requests +from requests import Session +from typing import Optional, Dict, Any +import os +from fastapi import HTTPException +import urllib3 + +load_dotenv(dotenv_path=".env") + +APIGATEWAY_URL = os.getenv("APIGATEWAY_URL", "") +APIGATEWAY_USER = os.getenv("APIGATEWAY_USER", "") +APIGATEWAY_PASSWORD = os.getenv("APIGATEWAY_PASSWORD", "") +APIGATEWAY_COMPANYDB = os.getenv("APIGATEWAY_COMPANYDB", "") +APIGATEWAY_DBINSTANCE = os.getenv("APIGATEWAY_DBINSTANCE", "") +APIGATEWAY_TIMEOUT = float(os.getenv("APIGATEWAY_TIMEOUT", 30.0)) + +logger = logging.getLogger(__name__) + +api_gateway_session: Optional[Session] = None + + +def ensure_api_gateway_session(force_refresh: bool = False) -> Session: + global api_gateway_session + + if api_gateway_session and not force_refresh: + return api_gateway_session + + logger.info("Iniciando sesión en APIGateway...") + login_url = f"{APIGATEWAY_URL}/login" + payload = { + "CompanyDB": APIGATEWAY_COMPANYDB, + "UserName": APIGATEWAY_USER, + "Password": APIGATEWAY_PASSWORD, + "DBInstance": APIGATEWAY_DBINSTANCE, + } + + try: + session = Session() + session.verify = False + + response = session.post( + login_url, + json=payload, + timeout=APIGATEWAY_TIMEOUT, + ) + + if response.status_code != 200: + logger.error(f"Error login APIGateway. Status: {response.status_code}") + raise HTTPException( + status_code=response.status_code, + detail="Error de autenticación con APIGateway" + ) + + api_gateway_session = session + logger.info(f"Sesión APIGateway establecida. Cookies: {dict(session.cookies)}") + return session + + except requests.RequestException as exc: + logger.error(f"Error de conexión con APIGateway: {exc}") + api_gateway_session = None + raise HTTPException( + status_code=500, + detail=f"No se pudo conectar con APIGateway: {exc}" + ) + + +def forward_to_api_gateway( + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + json_data: Optional[Any] = None, +) -> requests.Response: + if not method or not endpoint: + raise ValueError("method y endpoint son requeridos") + + method = method.upper() + session = ensure_api_gateway_session() + + url_destino = f"{APIGATEWAY_URL}/{endpoint}" + + log_extra = {"http_method": method, "endpoint": endpoint} + logger.info(f"[APIGateway] [{method}] {endpoint}", extra=log_extra) + + try: + response = session.request( + method=method, + url=url_destino, + params=params, + json=json_data, + timeout=APIGATEWAY_TIMEOUT, + ) + except requests.exceptions.Timeout: + logger.error(f"Timeout ({APIGATEWAY_TIMEOUT}s) en APIGateway {endpoint}", extra=log_extra) + raise ConnectionError(f"Timeout al conectar con APIGateway: {endpoint}") + except requests.RequestException as exc: + logger.error(f"Error en solicitud APIGateway: {exc}", extra=log_extra) + raise ConnectionError(f"Error en solicitud APIGateway: {exc}") + + if response.status_code == 401: + logger.warning("Sesión APIGateway expirada (401). Reautenticando...", extra=log_extra) + session = ensure_api_gateway_session(force_refresh=True) + try: + response = session.request( + method=method, + url=url_destino, + params=params, + json=json_data, + timeout=APIGATEWAY_TIMEOUT, + ) + except requests.RequestException as exc: + raise ConnectionError(f"Error en reintento APIGateway: {exc}") + + if response.status_code >= 400: + error_msg = response.text[:500] + logger.error( + f"[{response.status_code}] APIGateway {endpoint}: {error_msg}", + extra={**log_extra, "status_code": response.status_code} + ) + raise HTTPException( + status_code=response.status_code, + detail=f"Error en APIGateway: {error_msg}" + ) + + logger.info( + f"[{response.status_code}] APIGateway {endpoint}", + extra={**log_extra, "status_code": response.status_code} + ) + return response diff --git a/mini-backend/core/crud_router.py b/mini-backend/core/crud_router.py new file mode 100644 index 0000000..01290cb --- /dev/null +++ b/mini-backend/core/crud_router.py @@ -0,0 +1,123 @@ +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Request + +from core.sap import forward_to_sapb1 + + +def _build_sap_params( + skip: int, + top: int, + fields: Optional[str], + filter: Optional[str], + orderby: Optional[str] = None, +) -> Dict[str, Any]: + sap_params: Dict[str, Any] = { + "$skip": skip, + "$top": top, + "$inlinecount": "allpages", + } + if fields: + sap_params["$select"] = fields.replace(" ", "") + if filter: + sap_params["$filter"] = filter + if orderby: + sap_params["$orderby"] = orderby + return sap_params + + +def _build_list_headers(top: int, filter: Optional[str]) -> Dict[str, str]: + headers = {"B1S-PageSize": str(top)} + if filter: + headers["B1S-CaseInsensitive"] = "true" + return headers + + +def make_crud_router( + resource: str, + *, + tags: List[str], + key_field: Optional[str] = None, + key_quoted: bool = False, + supports_orderby: bool = False, + supports_get_by_key: bool = True, + supports_create: bool = True, + supports_patch: bool = True, + patch_headers: Optional[Dict[str, str]] = None, +) -> APIRouter: + """ + Genera el router list/get/create/update repetido antes en cada módulo de + routers/ (mismo bloque de $skip/$top/$filter y de forward_to_sapb1, solo + cambiaba el nombre del recurso OData). Centralizarlo evita que un + copy-paste entre recursos deje, por ejemplo, un PATCH apuntando al + endpoint_path de otro recurso. + """ + router = APIRouter(tags=tags) + + def key_expr(key: str) -> str: + return f"{resource}('{key}')" if key_quoted else f"{resource}({key})" + + if supports_orderby: + @router.get(f"/api/{resource}", name=f"list_{resource}") + async def list_resource( + skip: int = 0, + top: int = 50, + fields: Optional[str] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + ): + sap_params = _build_sap_params(skip, top, fields, filter, orderby) + return forward_to_sapb1( + method="GET", + endpoint_path=resource, + params=sap_params, + extra_headers=_build_list_headers(top, filter), + ).json() + else: + @router.get(f"/api/{resource}", name=f"list_{resource}") + async def list_resource( + skip: int = 0, + top: int = 50, + fields: Optional[str] = None, + filter: Optional[str] = None, + ): + sap_params = _build_sap_params(skip, top, fields, filter) + return forward_to_sapb1( + method="GET", + endpoint_path=resource, + params=sap_params, + extra_headers=_build_list_headers(top, filter), + ).json() + + if supports_get_by_key: + @router.get(f"/api/{resource}/{{key}}", name=f"get_{resource}_by_key") + async def get_by_key(key: str): + return forward_to_sapb1( + method="GET", + endpoint_path=key_expr(key), + ).json() + + if supports_create: + @router.post(f"/api/{resource}", status_code=201, name=f"create_{resource}") + async def create_resource(request: Request): + data = await request.json() + resp = forward_to_sapb1( + method="POST", + endpoint_path=resource, + json_data=data, + ) + created = resp.json() + return {key_field: created[key_field]} + + if supports_patch: + @router.patch(f"/api/{resource}/{{key}}", status_code=204, name=f"update_{resource}") + async def update_resource(key: str, request: Request): + data = await request.json() + forward_to_sapb1( + method="PATCH", + endpoint_path=key_expr(key), + json_data=data, + extra_headers=patch_headers, + ) + + return router diff --git a/mini-backend/core/hana.py b/mini-backend/core/hana.py new file mode 100644 index 0000000..d21bbbc --- /dev/null +++ b/mini-backend/core/hana.py @@ -0,0 +1,44 @@ +import os +import logging +from contextlib import contextmanager +from typing import Iterator + +from dotenv import load_dotenv +from fastapi import HTTPException +from hdbcli import dbapi + +# ============================================================================ +load_dotenv(dotenv_path=".env") + +HANA_HOST = os.getenv("HANA_HOST", "") +HANA_PORT = int(os.getenv("HANA_PORT", 30015)) +HANA_USER = os.getenv("HANA_USER", "") +HANA_PASSWORD = os.getenv("HANA_PASSWORD", "") +HANA_SCHEMA = os.getenv("HANA_SCHEMA", "") + +logger = logging.getLogger(__name__) + +# ============================================================================ +@contextmanager +def hana_connection() -> Iterator[dbapi.Connection]: + """ + Abre una conexión a SAP HANA para un query puntual (reportes Jinja/HTML) + y la cierra al salir del bloque `with`. + """ + try: + conn = dbapi.connect( + address=HANA_HOST, + port=HANA_PORT, + user=HANA_USER, + password=HANA_PASSWORD, + currentSchema=HANA_SCHEMA, + ) + except Exception as exc: + logger.error(f"Error al conectar con HANA: {exc}") + raise HTTPException(status_code=500, detail=f"No se pudo conectar con la base de datos: {exc}") + + try: + yield conn + finally: + conn.close() +# ============================================================================ diff --git a/mini-backend/core/sap.py b/mini-backend/core/sap.py new file mode 100644 index 0000000..92107f4 --- /dev/null +++ b/mini-backend/core/sap.py @@ -0,0 +1,200 @@ + +from dotenv import load_dotenv +import logging +import requests +from typing import Optional, Dict, Any +import os +from fastapi import FastAPI, HTTPException, Request +import json +import urllib3 + + +# ============================================================================ +# CONFIGURACIÓN +# ============================================================================ + +# Cargar variables de entorno desde .env +load_dotenv(dotenv_path=".env") + +SAPB1_SL_URL = os.getenv("SAPB1_SL_URL", "") +SAPB1_SL_USER = os.getenv("SAPB1_SL_USER", "") +SAPB1_SL_PASSWORD = os.getenv("SAPB1_SL_PASSWORD", "") +SAPB1_SL_COMPANYDB = os.getenv("SAPB1_SL_COMPANYDB", "") +# Tiempo de espera y verificación SSL +SAPB1_SL_TIMEOUT = float(os.getenv("SAPB1_SL_TIMEOUT", 15.0)) +SAPB1_SL_VERIFY_SSL = os.getenv("SAPB1_SL_VERIFY_SSL", "False").lower() == "true" + +# ============================================================================ +# Obtiene el logger específico de este archivo +logger = logging.getLogger(__name__) + +# ============================================================================ +# 1. Diccionario global para persistir las cabeceras de autenticación de SAP +sap_session_store: dict[str, Optional[dict[str, str]]] = { + "headers": None +} + +# ============================================================================ +def _log_response( + method: str, + endpoint: str, + response: requests.Response, + request_data: Optional[Dict] = None +) -> None: + """Registra detalles de la respuesta""" + extra = {"http_method": method, "endpoint": endpoint, "status_code": response.status_code} + if response.status_code >= 400: + try: + error_detail = response.json() + respuesta = json.dumps(error_detail, indent=2)[:500] + except Exception: + respuesta = response.text[:500] + logger.error( + f"[{response.status_code}] {method} {endpoint} - " + f"Datos enviados: {json.dumps(request_data, indent=2, default=str)[:500]} - " + f"Respuesta: {respuesta}", + extra=extra + ) + else: + logger.info(f"[{response.status_code}] {method} {endpoint}", extra=extra) + +# ============================================================================ +def ensure_sap_session(force_refresh: bool = False) -> dict: + """ + Verifica si existe una sesión global de SAP B1 (B1SESSION). + Si no existe o si force_refresh es True, realiza el Login con la información del .env. + """ + if sap_session_store["headers"] and not force_refresh: + return sap_session_store["headers"] + + logger.info("Iniciando sesión global en SAP como Manager...") + auth_url = f"{SAPB1_SL_URL}/Login" + sap_auth_payload = { + "CompanyDB": SAPB1_SL_COMPANYDB, + "UserName": SAPB1_SL_USER, + "Password": SAPB1_SL_PASSWORD + } + + try: + response = requests.post( + auth_url, + json=sap_auth_payload, + timeout=SAPB1_SL_TIMEOUT, + verify=SAPB1_SL_VERIFY_SSL + ) + + if response.status_code != 200: + logger.error(f"Error crítico: No se pudo conectar el usuario Manager a SAP. Status: {response.status_code}") + sap_session_store["headers"] = None + raise HTTPException(status_code=response.status_code, detail="Conexión fallida con el servidor SAP") + + sap_session = response.json() + b1_session_id = sap_session.get("SessionId") + route_id = response.cookies.get("ROUTEID") + + headers = { + "Cookie": f"B1SESSION={b1_session_id}; ROUTEID={route_id}" + } + sap_session_store["headers"] = headers + logger.info(f"Sesión global establecida. B1SESSION: {b1_session_id[:10]}...") + return headers + except requests.RequestException as exc: + logger.error(f"Error de conexión al autenticar contra SAP: {exc}") + sap_session_store["headers"] = None + raise HTTPException(status_code=500, detail=f"Error al conectar con SAP Service Layer: {exc}") + +# ============================================================================ +# FUNCIÓN PROXY CENTRALIZADA +# ============================================================================ +def forward_to_sapb1( + method: str, + endpoint_path: str, + json_data: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + extra_headers: Optional[Dict[str, str]] = None +) -> requests.Response: + """ + Función proxy centralizada para todas las llamadas a SAP B1 Service Layer. + Maneja autenticación transparentemente con sesión única y reintento. + """ + if not method or not endpoint_path: + raise ValueError("method y endpoint_path son requeridos") + + method = method.upper() + + # Obtener cabeceras con B1SESSION + headers = ensure_sap_session() + + # Preparar URL + url_destino = f"{SAPB1_SL_URL}/{endpoint_path}" + + # Preparar cabeceras de la petición + req_headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + req_headers.update(headers) + if extra_headers: + req_headers.update(extra_headers) + + log_extra = {"http_method": method, "endpoint": endpoint_path} + logger.info(f"[{method}] {endpoint_path}", extra=log_extra) + + # Realizar solicitud + try: + response = requests.request( + method=method, + url=url_destino, + json=json_data, + params=params, + headers=req_headers, + timeout=SAPB1_SL_TIMEOUT, + verify=SAPB1_SL_VERIFY_SSL + ) + except requests.exceptions.Timeout: + logger.error(f"Timeout ({SAPB1_SL_TIMEOUT}s) en {endpoint_path}", extra=log_extra) + raise ConnectionError(f"Timeout al conectar con {endpoint_path}") + except requests.exceptions.ConnectionError as exc: + logger.error(f"Error de conexión: {exc}", extra=log_extra) + raise ConnectionError(f"No se pudo conectar con SAP B1: {exc}") + except requests.RequestException as exc: + logger.error(f"Error en solicitud: {exc}", extra=log_extra) + raise ConnectionError(f"Error en solicitud HTTP: {exc}") + + # Manejo de 401 - Sesión expirada: reintentar UNA sola vez + if response.status_code == 401: + logger.warning("Sesión expirada (401). Reautenticando e intentando una sola vez...", extra=log_extra) + try: + # Forzar actualización de sesión + headers = ensure_sap_session(force_refresh=True) + req_headers.update(headers) + + logger.info("Reintentando solicitud [RETRY]", extra=log_extra) + response = requests.request( + method=method, + url=url_destino, + json=json_data, + params=params, + headers=req_headers, + timeout=SAPB1_SL_TIMEOUT, + verify=SAPB1_SL_VERIFY_SSL + ) + except Exception as exc: + logger.error(f"Error en reintento: {exc}", extra=log_extra) + raise ConnectionError(f"Error en reintento tras reautenticación: {exc}") + + # Logging de respuesta + _log_response(method, endpoint_path, response, json_data) + + # Extraer error SAP y propagar al frontend + if response.status_code >= 400: + error_msg = response.text + try: + err = response.json() + error_msg = err.get("error", {}).get("message", {}).get("value", error_msg) + except Exception: + pass + raise HTTPException(status_code=response.status_code, detail=error_msg) + + return response +# ============================================================================ \ No newline at end of file diff --git a/mini-backend/core/session.py b/mini-backend/core/session.py new file mode 100644 index 0000000..858a5d9 --- /dev/null +++ b/mini-backend/core/session.py @@ -0,0 +1,74 @@ +import secrets +import logging +from datetime import datetime, timedelta + +# ============================================================================ +SESSION_TIMEOUT_MINUTES = 30 + +employee_sessions: dict[str, dict] = {} + +active_tokens: dict[str, str] = {} + +logger = logging.getLogger(__name__) + +# ============================================================================ +def create_employee_session(webUser: str, emp_id, first_name: str, last_name: str) -> str: + for token, wu in list(active_tokens.items()): + if wu == webUser: + del active_tokens[token] + logger.info(f"Sesión anterior removida para el usuario {webUser} (nuevo login)") + break + + token = secrets.token_urlsafe(32) + active_tokens[token] = webUser + employee_sessions[webUser] = { + "last_activity": datetime.now(), + "emp_id": emp_id, + "first_name": first_name, + "last_name": last_name + } + logger.info(f"Sesión creada para el usuario {webUser} - {first_name} {last_name}") + return token + +# ============================================================================ +def validate_and_refresh_session(token: str) -> str | None: + webUser = active_tokens.get(token) + if not webUser: + return None + + session = employee_sessions.get(webUser) + if not session: + active_tokens.pop(token, None) + return None + + now = datetime.now() + if now - session["last_activity"] > timedelta(minutes=SESSION_TIMEOUT_MINUTES): + logger.info(f"Sesión expirada para el usuario {webUser} (inactivo > {SESSION_TIMEOUT_MINUTES} min)") + active_tokens.pop(token, None) + employee_sessions.pop(webUser, None) + return None + + session["last_activity"] = now + return webUser + +# ============================================================================ +def remove_employee_session_by_token(token: str): + webUser = active_tokens.pop(token, None) + if webUser: + employee_sessions.pop(webUser, None) + logger.info(f"Sesión cerrada para el usuario {webUser}") + +# ============================================================================ +def get_employee_name(webUser: str) -> str: + session = employee_sessions.get(webUser) + if not session: + return "-" + return f"{session.get('first_name', '')} {session.get('last_name', '')}".strip() + +# ============================================================================ +def get_emp_id(webUser: str): + session = employee_sessions.get(webUser) + if not session: + return "-" + return session.get("emp_id", "-") +# ============================================================================ \ No newline at end of file diff --git a/mini-backend/docs/api-reference.md b/mini-backend/docs/api-reference.md new file mode 100644 index 0000000..ed0b5f7 --- /dev/null +++ b/mini-backend/docs/api-reference.md @@ -0,0 +1,526 @@ +# API Reference + +> **Base URL:** `http://:8000` +> **API Version:** 1.1.0 +> **Content-Type:** `application/json` + +--- + +## Headers Globales + +| Header | Requerido | Descripción | +|--------|-----------|-------------| +| `Content-Type` | Sí | `application/json` | +| `X-Session-Token` | Sí (excepto `/api/Login`) | Token obtenido en el login | +| `X-Request-ID` | Opcional | UUID para trazabilidad; si no se envía, el servidor lo genera | + +--- + +## Formato de Errores + +Todas las respuestas de error siguen el mismo formato: + +```json +{ + "detail": "Mensaje de error de SAP o del servidor" +} +``` + +| Código | Significado | +|--------|-------------| +| 400 | Error de negocio SAP (ej: código duplicado, validación fallida) | +| 401 | Token inválido/expirado, o credenciales incorrectas | +| 404 | Recurso no encontrado en SAP | +| 500 | Error de conexión con SAP o error interno | + +--- + +## Autenticación + +### `POST /api/Login` + +Inicia sesión de empleado. Valida contra SAP EmployeesInfo usando el apellido como contraseña. + +**Request body:** + +```json +{ + "Username": "123", + "Password": "Garcia" +} +``` + +**Response 200:** + +```json +{ + "message": "Login exitoso", + "token": "aB3x...token_urlsafe_32_chars...", + "username": "Juan" +} +``` + +**Response 401:** + +```json +{ + "detail": "Usuario o contraseña incorrectos" +} +``` + +--- + +### `POST /api/Logout` + +Cierra la sesión del empleado. + +**Headers:** `X-Session-Token: ` + +**Response 200:** + +```json +{ + "message": "Logout exitoso" +} +``` + +--- + +## Socios de Negocio + +### `GET /api/BusinessPartners` + +Lista paginada de socios de negocio. + +**Query parameters:** + +| Parámetro | Tipo | Default | Descripción | +|-----------|------|---------|-------------| +| `skip` | int | 0 | Registros a saltar (`$skip`) | +| `top` | int | 50 | Registros por página (`$top`) | +| `fields` | string | — | Campos a retornar, separados por coma (`$select`) | +| `filter` | string | — | Filtro OData (`$filter`). Activa `B1S-CaseInsensitive` | + +**Response 200:** + +```json +{ + "odata.metadata": "https://.../$metadata#BusinessPartners", + "value": [ + { + "CardCode": "C001", + "CardName": "Cliente Demo", + "CardType": "C" + } + ], + "odata.count": 150 +} +``` + +--- + +### `GET /api/BusinessPartners/{bp_code}` + +Obtiene un socio de negocio por `CardCode`. + +**Response 200:** + +```json +{ + "CardCode": "C001", + "CardName": "Cliente Demo", + "CardType": "C", + "Phone1": "123456789", + "EmailAddress": "cliente@demo.com" +} +``` + +--- + +### `POST /api/BusinessPartners` + +Crea un nuevo socio de negocio. + +**Request body:** + +```json +{ + "CardCode": "C001", + "CardName": "Cliente Demo", + "CardType": "C" +} +``` + +**Response 201:** + +```json +{ + "CardCode": "C001" +} +``` + +--- + +### `PATCH /api/BusinessPartners/{bp_code}` + +Actualiza un socio de negocio existente. + +**Request body:** + +```json +{ + "CardName": "Nuevo Nombre", + "Phone1": "987654321" +} +``` + +**Response 204:** Sin contenido. + +--- + +## Artículos + +### `GET /api/Items` + +Lista paginada de artículos. + +**Query parameters:** + +| Parámetro | Tipo | Default | Descripción | +|-----------|------|---------|-------------| +| `skip` | int | 0 | Registros a saltar | +| `top` | int | 50 | Registros por página | +| `fields` | string | — | Campos a retornar (`$select`) | +| `filter` | string | — | Filtro OData (`$filter`) | + +**Response 200:** + +```json +{ + "odata.metadata": "https://.../$metadata#Items", + "value": [ + { + "ItemCode": "A001", + "ItemName": "Artículo Demo", + "InventoryUOM": "UN" + } + ], + "odata.count": 250 +} +``` + +--- + +### `GET /api/Items/{item_code}` + +Obtiene un artículo por `ItemCode`. + +**Response 200:** + +```json +{ + "ItemCode": "A001", + "ItemName": "Artículo Demo", + "InventoryUOM": "UN", + "SalesPrice": 100.50 +} +``` + +--- + +### `POST /api/Items` + +Crea un nuevo artículo. + +**Request body:** + +```json +{ + "ItemCode": "A001", + "ItemName": "Artículo Demo", + "InventoryUOM": "UN" +} +``` + +**Response 201:** + +```json +{ + "ItemCode": "A001" +} +``` + +--- + +### `PATCH /api/Items/{item_code}` + +Actualiza un artículo existente. + +**Request body:** + +```json +{ + "ItemName": "Nuevo Nombre", + "SalesPrice": 120.00 +} +``` + +**Response 204:** Sin contenido. + +--- + +## Ventas + +### Facturas (A/R Invoices) + +#### `GET /api/Invoices` + +Lista paginada de facturas de venta. + +**Query parameters:** + +| Parámetro | Tipo | Default | Descripción | +|-----------|------|---------|-------------| +| `skip` | int | 0 | Registros a saltar | +| `top` | int | 50 | Registros por página | +| `fields` | string | — | Campos a retornar | +| `filter` | string | — | Filtro OData | +| `orderby` | string | — | Ordenamiento (`$orderby`) | + +**Response 200:** + +```json +{ + "odata.metadata": "https://.../$metadata#Invoices", + "value": [ + { + "DocEntry": 1001, + "CardCode": "C001", + "DocTotal": 1500.00, + "DocDate": "2026-07-06" + } + ], + "odata.count": 45 +} +``` + +--- + +#### `POST /api/Invoices` + +Crea una factura de venta. + +**Response 201:** + +```json +{ + "DocEntry": 1001 +} +``` + +--- + +#### `GET /api/Invoices/{invoice_key}` + +Obtiene una factura por `DocEntry`. + +--- + +#### `PATCH /api/Invoices/{invoice_key}` + +Actualiza una factura. Usa header `B1S-ReplaceCollectionsOnPatch: true`. + +**Response 204:** Sin contenido. + +--- + +### Pedidos (Sales Orders) + +#### `GET /api/Orders` + +Lista paginada de pedidos de venta. Mismos parámetros que Invoices. + +#### `POST /api/Orders` + +Crea un pedido de venta. + +**Response 201:** + +```json +{ + "DocEntry": 2001 +} +``` + +#### `GET /api/Orders/{order_key}` + +Obtiene un pedido por `DocEntry`. + +#### `PATCH /api/Orders/{order_key}` + +Actualiza un pedido. Usa header `B1S-ReplaceCollectionsOnPatch: true`. + +**Response 204:** Sin contenido. + +--- + +## Compras + +### Órdenes de Compra + +#### `GET /api/PurchaseOrders` + +Lista paginada. Mismos parámetros que Invoices/Orders. + +#### `GET /api/PurchaseOrders/{key}` + +Obtiene una orden de compra por `DocEntry`. + +#### `PATCH /api/PurchaseOrders/{key}` + +Actualiza una orden de compra. Usa header `B1S-ReplaceCollectionsOnPatch: true`. + +**Response 204:** Sin contenido. + +--- + +### Facturas de Compra (A/P Invoices) + +#### `GET /api/PurchaseInvoices` + +Lista paginada. Mismos parámetros que Invoices/Orders. + +#### `GET /api/PurchaseInvoices/{key}` + +Obtiene una factura de compra por `DocEntry`. + +#### `PATCH /api/PurchaseInvoices/{key}` + +Actualiza una factura de compra. Usa header `B1S-ReplaceCollectionsOnPatch: true`. + +**Response 204:** Sin contenido. + +--- + +## Catálogos + +### `GET /api/Currencies` + +Lista de monedas. + +**Response 200:** + +```json +{ + "value": [ + { "Code": "USD", "Name": "US Dollar" }, + { "Code": "EUR", "Name": "Euro" } + ] +} +``` + +--- + +### `GET /api/BusinessPartnerGroups` + +Lista de grupos de socios de negocio. + +**Response 200:** + +```json +{ + "value": [ + { "Number": 1, "GroupName": "Clientes Nacionales" } + ] +} +``` + +--- + +### `GET /api/Warehouses` + +Lista paginada de almacenes. Acepta `skip`, `top`, `fields`, `filter`. + +--- + +### `GET /api/ItemGroups` + +Lista de grupos de artículos. + +**Response 200:** + +```json +{ + "value": [ + { "Number": 1, "GroupName": "Materia Prima" } + ] +} +``` + +--- + +### `GET /api/SalesTaxCodes` + +Lista paginada de códigos de IVA. Acepta `skip`, `top`, `fields`, `filter`. + +--- + +## Lotes + +### `GET /api/BatchNumberDetails` + +Lista paginada de lotes/números de serie. Acepta `skip`, `top`, `fields`, `filter`, `orderby`. + +--- + +### `GET /api/BatchNumberDetails/{batch_id}` + +Obtiene un lote por ID. + +--- + +### `PATCH /api/BatchNumberDetails/{batch_id}` + +Actualiza un lote. + +**Response 204:** Sin contenido. + +--- + +## Permisos + +### `GET /api/user/menu` + +Retorna las rutas de menú permitidas para el empleado autenticado. + +**Headers:** `X-Session-Token: ` + +**Response 200:** + +```json +{ + "allowedPaths": [ + "/inventario/*", + "/ventas/*", + "/compras/*" + ] +} +``` + +**Response 401:** + +```json +{ + "detail": "Usuario no autenticado" +} +``` + +--- + +## Resumen de Códigos de Respuesta + +| Método | Éxito | Cuerpo en éxito | Error SAP | Error de conexión | +|--------|-------|----------------|-----------|-------------------| +| GET | 200 | JSON del recurso | 4xx con `detail` | 500 | +| POST | 201 | `{"CardCode"/"ItemCode"/"DocEntry": valor}` | 4xx con `detail` | 500 | +| PATCH | 204 | Sin cuerpo | 4xx con `detail` | 500 | diff --git a/mini-backend/docs/architecture.md b/mini-backend/docs/architecture.md new file mode 100644 index 0000000..c85dcbc --- /dev/null +++ b/mini-backend/docs/architecture.md @@ -0,0 +1,183 @@ +# Arquitectura General + +## Diagrama de Componentes + +```mermaid +graph TB + subgraph "FastAPI Application" + M[main.py\nEntry Point] + C[context.py\nRequest ID Context] + L[logger_config.py\nStructured Logging] + + subgraph "Core" + SAP[core/sap.py\nSAP Session & Proxy] + SES[core/session.py\nEmployee Sessions] + end + + subgraph "Routers" + A[auth.py\nLogin / Logout] + BP[business_partners.py\nSocios de Negocio] + IT[items.py\nArtículos] + SL[sales.py\nFacturas / Pedidos] + PR[purchases.py\nÓrdenes / Facturas Compra] + CT[catalogs.py\nCatálogos] + BT[batch.py\nLotes] + PM[permissions.py\nPermisos de Menú] + end + + MP[menu_permissions.json] + end + + subgraph "External" + SAP_SL[SAP B1 Service Layer] + end + + M --> C + M --> L + C --> L + + M --> A + M --> BP + M --> IT + M --> SL + M --> PR + M --> CT + M --> BT + M --> PM + + A --> SAP + A --> SES + BP --> SAP + IT --> SAP + SL --> SAP + PR --> SAP + CT --> SAP + BT --> SAP + PM --> MP + + SAP --> SAP_SL + SES --> SAP +``` + +## Pipeline de Middleware + +Las peticiones atraviesan los siguientes middleware en orden: + +```mermaid +flowchart LR + REQ[Request] --> CORS[CORSMiddleware\nallow_origins=*] + CORS --> CID[correlation_id_middleware\nX-Request-ID] + CID --> SES_MW[session_middleware\nValida X-Session-Token] + SES_MW --> ROUTER{Router} + ROUTER -->|/api/Login| A[auth.py] + ROUTER -->|Otros /api/*| SAP_CORE[core/sap.py] +``` + +### Detalle de Middleware + +| Middleware | Función | +| -------------------------- | ----------------------------------------------------------- | +| `CORSMiddleware` | Permite peticiones desde cualquier origen | +| `correlation_id_middleware` | Genera o extrae `X-Request-ID` para trazabilidad en logs | +| `session_middleware` | Valida token de sesión, adjunta `emp_id` a `request.state` | + +## Arquitectura de Sesiones + +Existen **dos capas de sesión independientes**: + +```mermaid +flowchart LR + subgraph Employee["Employee Session"] + T["Token aleatorio → emp_id"] + E["employee_sessions dict"] + TK["active_tokens dict"] + T --> TK + TK --> E + end + + subgraph SAP["SAP B1 Session"] + S["sap_session_store"] + M["Manager Credentials (.env)"] + S -.->|Renueva en 401| M + end +``` + +| Capa | Almacenamiento | Ámbito | Timeout | Renovación | +| ------------- | ----------------- | -------- | ------------- | -------------------- | +| SAP B1 | `sap_session_store` | Global | Gestionado SAP | Automática al recibir 401 | +| Empleado | `active_tokens` | Por empleado | 30 min inactividad | Login manual | + +## Estructura de Rutas + +| Método | Ruta | Router | Descripción | +| -------- | --------------------------------- | -------------------- | ------------------------ | +| `POST` | `/api/Login` | `auth.py` | Autenticar empleado | +| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión | +| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios | +| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio | +| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio | +| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio | +| `GET` | `/api/Items` | `items.py` | Listar artículos | +| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo | +| `POST` | `/api/Items` | `items.py` | Crear artículo | +| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo | +| `GET` | `/api/Invoices` | `sales.py` | Listar facturas | +| `GET` | `/api/Invoices/{key}` | `sales.py` | Obtener factura | +| `POST` | `/api/Invoices` | `sales.py` | Crear factura | +| `PATCH` | `/api/Invoices/{key}` | `sales.py` | Actualizar factura | +| `GET` | `/api/Orders` | `sales.py` | Listar pedidos | +| `GET` | `/api/Orders/{key}` | `sales.py` | Obtener pedido | +| `POST` | `/api/Orders` | `sales.py` | Crear pedido | +| `PATCH` | `/api/Orders/{key}` | `sales.py` | Actualizar pedido | +| `GET` | `/api/PurchaseOrders` | `purchases.py` | Listar órdenes de compra | +| `GET` | `/api/PurchaseOrders/{key}` | `purchases.py` | Obtener orden de compra | +| `PATCH` | `/api/PurchaseOrders/{key}` | `purchases.py` | Actualizar orden compra | +| `GET` | `/api/PurchaseInvoices` | `purchases.py` | Listar facturas compra | +| `GET` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Obtener factura compra | +| `PATCH` | `/api/PurchaseInvoices/{key}` | `purchases.py` | Actualizar factura compra| +| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas | +| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios | +| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes | +| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos | +| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA | +| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes | +| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote | +| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote | +| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido | + +## Dependencias del Proyecto + +```mermaid +graph TD + main --> context + main --> logger_config + logger_config --> context + main --> auth + main --> business_partners + main --> items + main --> sales + main --> purchases + main --> catalogs + main --> batch + main --> permissions + auth --> sap_core + auth --> session_core + business_partners --> sap_core + items --> sap_core + sales --> sap_core + purchases --> sap_core + catalogs --> sap_core + batch --> sap_core + permissions --> menu_json[menu_permissions.json] +``` + +## Configuración por Entorno + +La aplicación carga siempre `.env` (un solo archivo, sin sufijos de entorno). +`.env.aberio` se conserva como plantilla de referencia; para desplegar contra +ese entorno, copiar su contenido dentro de `.env`. + +| Archivo | URL SAP | Company DB | +| ------------------ | ------------------------------- | ----------------------- | +| `.env` | `192.168.1.175:50000` | `SBO_PRUEBA_TAGUM_22` | +| `.env.aberio` | `192.168.9.3:50000` | `PRUEBA_20260609` | diff --git a/mini-backend/docs/auth-flow.md b/mini-backend/docs/auth-flow.md new file mode 100644 index 0000000..e51880b --- /dev/null +++ b/mini-backend/docs/auth-flow.md @@ -0,0 +1,108 @@ +# Flujo de Autenticación + +## Login + +```mermaid +sequenceDiagram + participant F as Frontend + participant API as FastAPI + participant S as core/session.py + participant SAP_C as core/sap.py + participant SAP as SAP Service Layer + + F->>API: POST /api/Login\n{Username, Password} + Note over F,API: Username = emp_id\nPassword = LastName + + API->>SAP_C: forward_to_sapb1(GET, "EmployeesInfo({empId})") + SAP_C->>SAP: GET /b1s/v1/EmployeesInfo({empId}) + SAP-->>SAP_C: {FirstName, LastName, ...} + SAP_C-->>API: Employee data + + API->>API: ¿Password == LastName? + alt Coincide + API->>S: create_employee_session(emp_id, first_name, last_name) + S->>S: Genera token_urlsafe(32) + S-->>API: token + + API->>SAP_C: ensure_sap_session() + SAP_C->>SAP: POST /Login (manager creds) + SAP-->>SAP_C: B1SESSION cookie + SAP_C-->>API: OK + + API-->>F: 200 {message, token, username} + else No coincide + API-->>F: 401 Invalid credentials + end +``` + +## Logout + +```mermaid +sequenceDiagram + participant F as Frontend + participant API as FastAPI + participant S as core/session.py + + F->>API: POST /api/Logout\nX-Session-Token: + API->>S: remove_employee_session_by_token(token) + S->>S: Elimina active_tokens[token]\ny employee_sessions[emp_id] + S-->>API: OK + API-->>F: 200 {message: "Sesión cerrada"} +``` + +## Validación de Sesión (Middleware) + +Cada petición a `/api/*` (excepto `/api/Login`) pasa por el `session_middleware`: + +```mermaid +sequenceDiagram + participant F as Frontend + participant MW as session_middleware + participant S as core/session.py + participant R as Router + + F->>MW: Request /api/Items\nX-Session-Token: + + MW->>S: validate_and_refresh_session(token) + S->>S: Busca token en active_tokens + + alt Token válido + S->>S: Actualiza last_activity + S-->>MW: emp_id + MW->>MW: Adjunta emp_id a request.state + MW->>R: Continúa al router + R-->>F: 200 Response + else Token expirado (>30 min) + S->>S: Elimina token y sesión + S-->>MW: None + MW-->>F: 401 Token expirado + else Token no existe + S-->>MW: None + MW-->>F: 401 No autorizado + end +``` + +## Ciclo de Vida de la Sesión de Empleado + +```mermaid +stateDiagram-v2 + [*] --> Activa: Login exitoso\ngenera token + Activa --> Expirada: 30 min sin actividad + Activa --> Cerrada: Logout explícito + Expirada --> [*] + Cerrada --> [*] + + note right of Activa + Se refresca last_activity + en cada petición válida + end note +``` + +## Mecanismo de Renovación + +La sesión del empleado se renueva automáticamente en **cada petición válida**: + +1. El frontend envía `X-Session-Token` en el header +2. `validate_and_refresh_session()` actualiza `last_activity` al timestamp actual +3. El timeout de 30 minutos se mide desde la última actividad +4. Si pasan 30 min sin peticiones, el token se invalida automáticamente diff --git a/mini-backend/docs/index.md b/mini-backend/docs/index.md new file mode 100644 index 0000000..19d2e57 --- /dev/null +++ b/mini-backend/docs/index.md @@ -0,0 +1,45 @@ +# Backend SN — Documentación del Proyecto + +Backend de integración entre una aplicación frontend (Next.js) y **SAP Business One Service Layer**. Actúa como proxy/middleware autenticando empleados, gestionando sesiones y enrutando operaciones CRUD hacia SAP. + +## Stack Tecnológico + +| Componente | Tecnología | +| ------------- | ----------------- | +| Framework | FastAPI (Python) | +| Servidor ASGI | Uvicorn | +| HTTP Client | `requests` | +| Logging | JSON estructurado | +| Sesiones | In-memory | + +## Diagrama de Contexto del Sistema + +```mermaid +graph TB + subgraph "Frontend" + F[App SN - Next.js] + end + + subgraph "Backend SN - FastAPI" + API[API Proxy\n:8000] + end + + subgraph "SAP Business One" + SL[Service Layer\nREST API] + DB[(SAP DB\nSBO_PRUEBA_*)] + end + + F -->|HTTP /api/*| API + API -->|HTTP /b1s/v1| SL + SL --> DB +``` + +## Documentos + +| Documento | Descripción | +| -------------------------------------------- | -------------------------------------------- | +| [Arquitectura](architecture.md) | Componentes, dependencias, middlewares, APIs | +| [Flujo de Autenticación](auth-flow.md) | Login, logout, sesiones, middleware | +| [Proxy SAP](sap-proxy.md) | Enrutamiento a SAP, re-autenticación, OData | +| [API Reference](api-reference.md) | Todos los endpoints, ejemplos request/response | +| [Visualización de logs](logs-viewing.md) | Cómo leer `logs/backend.log` con `jq`/`lnav` | diff --git a/mini-backend/docs/logging-plan.md b/mini-backend/docs/logging-plan.md new file mode 100644 index 0000000..f11dad6 --- /dev/null +++ b/mini-backend/docs/logging-plan.md @@ -0,0 +1,175 @@ +# Plan: logging real por sesión de usuario + +## Problema actual (diagnosticado) + +- `main.py:32` fuerza `setup_logging(environment="debug")` siempre — el modo + `"production"` (JSON estructurado) de `logger_config.py` nunca se activa, + sin importar el entorno real de despliegue. +- `core/sap.py`, `core/api_gateway.py` y `routers/auth.py` usan `print()` en + vez del `logger` real. Esto significa: + - No queda nada en el archivo rotativo `logs/backend.log` (solo en la + consola / `docker compose logs`). + - No tiene formato JSON buscable, ni `request_id` de correlación. +- **Grave**: `routers/auth.py` imprime la contraseña (apellido) en texto + plano dos veces: + - línea ~27: `print(f"Contraseña (Apellido) recibida: {empLastName}")` + - línea ~45: `print(f"Apellido en SAP: {employee_data.get('LastName')}")` + +## Objetivo + +Además de arreglar lo anterior, dejar un log **por sesión de usuario +conectado**: que cada línea de log quede etiquetada con qué empleado +(`emp_id` + nombre) hizo qué acción (`GET /Items`, `POST /BusinessPartners`, +etc.), en JSON, para poder cargarlo después en un monitor/dashboard y buscar +acciones por usuario. + +## Diseño + +`context.py` ya tiene un `ContextVar` para el `request_id` que se inyecta +automáticamente en cada log a través de `logger_config.JsonFormatter`. Se +replica el mismo patrón para el usuario conectado, así **todo log queda +etiquetado automáticamente sin tocar cada llamada individual**. + +### 1. `context.py` + +Agregar un `ContextVar` para el usuario actual, mismo patrón que +`request_id_ctx`: + +```python +current_user_ctx: ContextVar[dict] = ContextVar( + "current_user", default={"emp_id": "-", "username": "-"} +) + +def get_current_user() -> dict: + return current_user_ctx.get() + +def set_current_user(emp_id: str, username: str): + current_user_ctx.set({"emp_id": emp_id, "username": username}) +``` + +### 2. `core/session.py` + +Agregar un helper que devuelva "Nombre Apellido" a partir de `emp_id`, +leyendo el dict `employee_sessions` que ya existe: + +```python +def get_employee_name(emp_id: str) -> str: + session = employee_sessions.get(emp_id) + if not session: + return "-" + return f"{session.get('first_name', '')} {session.get('last_name', '')}".strip() +``` + +### 3. `main.py` + +En `session_middleware`, justo después de validar el token y obtener +`emp_id` (donde hoy se hace `request.state.emp_id = emp_id`), setear el +contexto de usuario: + +```python +from core.session import validate_and_refresh_session, get_employee_name +from context import set_current_user +... +emp_id = validate_and_refresh_session(token) +if not emp_id: + ... +set_current_user(emp_id, get_employee_name(emp_id)) +request.state.emp_id = emp_id +``` + +También en este archivo: dejar de forzar `"debug"` en `setup_logging`, +leerlo de una variable de entorno nueva (por defecto `"production"`, para +que Docker quede bien sin configurar nada extra): + +```python +LOG_ENVIRONMENT = os.getenv("LOG_ENVIRONMENT", "production") +... +@asynccontextmanager +async def lifespan(app: FastAPI): + setup_logging(environment=LOG_ENVIRONMENT) + yield +``` + +### 4. `logger_config.py` + +Extender `JsonFormatter` para incluir `emp_id`/`username` desde el +contexto en cada línea, y soportar campos extra estructurados +(`http_method`, `endpoint`, `status_code`) cuando el log los trae vía +`extra={...}`: + +```python +from context import get_request_id, get_current_user + +class JsonFormatter(logging.Formatter): + EXTRA_FIELDS = ("http_method", "endpoint", "status_code") + + def format(self, record): + user = get_current_user() + log_record = { + "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), + "level": record.levelname, + "request_id": get_request_id(), + "emp_id": user["emp_id"], + "username": user["username"], + "message": record.getMessage(), + "module": record.module, + "function": record.funcName, + "line": record.lineno, + } + for field in self.EXTRA_FIELDS: + if hasattr(record, field): + log_record[field] = getattr(record, field) + if record.exc_info: + log_record["exception"] = self.formatException(record.exc_info) + return json.dumps(log_record) +``` + +Resultado esperado por línea, por ejemplo: + +```json +{"timestamp": "2026-08-14T...", "level": "INFO", "request_id": "...", "emp_id": "11", "username": "Juan Pérez", "http_method": "GET", "endpoint": "Items", "status_code": 200, "message": "Solicitud SAP completada", "module": "sap", "function": "forward_to_sapb1", "line": 137} +``` + +### 5. `core/sap.py` / `core/api_gateway.py` + +Reemplazar cada `print(...)` por `logger.info/warning/error(...)`, +agregando `extra={"http_method": method, "endpoint": endpoint_path, +"status_code": response.status_code}` en las líneas de request/respuesta, +para que queden como campos estructurados y no solo texto libre. Quitar la +variable `I = " "` (indentación para prints, ya no hace falta) y los +emojis (el nivel del log ya indica severidad). + +### 6. `routers/auth.py` + +- Agregar `logger = logging.getLogger(__name__)`. +- Reemplazar todos los `print()` por `logger.info/warning/error(...)`. +- **No loguear nunca `empLastName` ni `employee_data.get('LastName')`** — + solo el `emp_id` y si el login fue exitoso o no. +- En cuanto se confirma el empleado (login exitoso), llamar + `set_current_user(empId, f"{first_name} {last_name}")` para que el + propio log del login quede etiquetado también (el middleware no aplica + a `/api/Login` porque no requiere token todavía). + +### 7. `docker-compose.yml` + +Agregar volumen para que el log persista en el host aunque se reconstruya +el contenedor (si no, se perdería cada `--build`, igual que pasaba con +`.env` antes de agregar su volumen): + +```yaml + backend: + volumes: + - ./mini-backend/menu_permissions.json:/app/menu_permissions.json:ro + - ./mini-backend/logs:/app/logs +``` + +## Verificación al implementar + +1. Levantar el backend (local o Docker). +2. Loguearse con un usuario de prueba y hacer un par de llamadas reales + (GET a algún catálogo, POST/PATCH a algo). +3. Revisar `logs/backend.log`: cada línea debe traer `emp_id`, `username`, + `http_method`, `endpoint`, `status_code` correctos. +4. `grep` de la contraseña de prueba sobre todo `logs/backend.log` (y la + salida de consola) para confirmar que **no aparece en ningún lado**. +5. Confirmar que `docker compose build` sigue funcionando sin errores. diff --git a/mini-backend/docs/logs-viewing.md b/mini-backend/docs/logs-viewing.md new file mode 100644 index 0000000..f1fe90d --- /dev/null +++ b/mini-backend/docs/logs-viewing.md @@ -0,0 +1,56 @@ +# Visualización de logs + +El backend escribe en `logs/backend.log` una línea JSON por evento (ver +[logging-plan.md](logging-plan.md)), con campos como `emp_id`, `username`, +`http_method`, `endpoint` y `status_code`. Es el formato correcto para +buscar/cargar en un dashboard, pero incómodo de leer a simple vista con +`cat`/`tail`. Estas son las dos formas recomendadas de verlo cómodo desde la +terminal. + +## `jq` — filtrar y colorear en vivo + +Instalación: `brew install jq` + +```bash +# seguir el log en vivo, con formato e indentación +tail -f logs/backend.log | jq . + +# solo las líneas de un empleado puntual +tail -f logs/backend.log | jq 'select(.emp_id == 10)' + +# solo errores/warnings +tail -f logs/backend.log | jq 'select(.level == "ERROR" or .level == "WARNING")' + +# solo llamadas a un endpoint específico +tail -f logs/backend.log | jq 'select(.endpoint == "Items")' + +# ver solo unos campos, en una línea por evento +tail -f logs/backend.log | jq -c '{ts: .timestamp, user: .username, ep: .endpoint, status: .status_code}' +``` + +## `lnav` — visor interactivo + +Instalación: `brew install lnav` + +```bash +lnav logs/backend.log +``` + +`lnav` detecta el formato JSON automáticamente y da un visor interactivo con: + +- colores por nivel (`INFO`/`WARNING`/`ERROR`). +- búsqueda de texto con `/`. +- filtros en vivo con `:filter-in` / `:filter-out` (por ejemplo + `:filter-in emp_id.:.10` para quedarte solo con el empleado 10). +- queries tipo SQL sobre los campos del log con `;` (por ejemplo + `;SELECT emp_id, username, endpoint, status_code FROM backend_log WHERE level = 'ERROR'`). + +Es la opción más cómoda para explorar el log completo o investigar un +incidente puntual sin escribir comandos `jq` largos. + +## Dashboard centralizado (a futuro) + +Si más adelante se necesita buscar por usuario desde una UI (no la terminal) +o cruzar logs de varios despliegues, la opción estándar es enviar +`logs/backend.log` a algo como Grafana Loki + Promtail. Es infraestructura +adicional, no necesaria mientras el volumen de logs se maneje bien con `jq`/`lnav`. diff --git a/mini-backend/docs/sap-proxy.md b/mini-backend/docs/sap-proxy.md new file mode 100644 index 0000000..fd79dcd --- /dev/null +++ b/mini-backend/docs/sap-proxy.md @@ -0,0 +1,118 @@ +# Flujo de Proxy SAP + +## Función Central: `forward_to_sapb1` + +Todas las operaciones CRUD pasan por esta función en `core/sap.py`, que actúa como proxy hacia SAP Service Layer. + +```mermaid +flowchart TD + START([Inicio]) --> INPUT["method, endpoint_path,\njson_data, params, headers"] + INPUT --> BUILD["Construye URL:\nSAPB1_SL_URL / endpoint_path"] + BUILD --> HEADERS["Adjunta headers:\nCookie: B1SESSION=...\nContent-Type: application/json"] + HEADERS --> SEND[Envía request a SAP] + + SEND --> CHECK{Respuesta} + CHECK -->|2xx| SUCCESS[Retorna Response al router] + CHECK -->|401| REFRESH["Force refresh:\nensure_sap_session()"] + CHECK -->|Timeout / Error| FAIL[ConnectionError] + + REFRESH --> RETRY{Es reintento?} + RETRY -->|No| SEND2["Reintenta request\ncon nueva sesión"] + RETRY -->|Sí| ERROR["Extrae error.message.value\nde SAP y lanza HTTPException"] + + SEND2 --> CHECK2{Respuesta} + CHECK2 -->|2xx| SUCCESS + CHECK2 -->|4xx/5xx| ERROR + + SUCCESS --> END([Fin]) + FAIL --> END + ERROR --> END + + style ERROR fill:#f96,stroke:#a33 +``` + +## Diagrama de Secuencia — Operación CRUD Típica + +Ejemplo con `GET /api/Items`: + +```mermaid +sequenceDiagram + participant F as Frontend + participant MW as Middleware + participant R as router/items.py + participant C as core/sap.py + participant SAP as SAP Service Layer + + F->>MW: GET /api/Items?$skip=0&$top=20\nX-Session-Token: [token] + MW->>MW: Valida sesión + MW->>R: request.state.emp_id = "123" + + R->>C: forward_to_sapb1(\n GET, "Items?$skip=0&$top=20&$inlinecount=allpages") + + C->>SAP: GET /b1s/v1/Items?$skip=0&$top=20\nCookie: B1SESSION=abc123 + SAP-->>C: 200 - value: [...], odata.count: 150 + + C-->>R: Response + R-->>F: 200 - data: [...], total: 150 +``` + +## Re-autenticación Automática ante 401 + +Cuando SAP rechaza la sesión actual (401), el sistema renueva automáticamente la sesión de SAP y reintenta: + +```mermaid +sequenceDiagram + participant R as Router + participant C as core/sap.py + participant SAP as SAP Service Layer + + R->>C: forward_to_sapb1(GET, "Items?...") + C->>SAP: GET /b1s/v1/Items?...\nCookie: B1SESSION=abc123 + SAP-->>C: 401 Unauthorized + + Note over C: Sesión SAP expirada\n→ Re-autentica y reintenta + + C->>SAP: POST /Login\nUserName: manager, Password: ..., CompanyDB: ... + SAP-->>C: 200 SessionId: xyz789 + C->>C: Actualiza sap_session_store + + C->>SAP: GET /b1s/v1/Items?...\nCookie: B1SESSION=xyz789 + SAP-->>C: 200 - value: [...] + + C-->>R: Response 200 + + Note over R,C: Si el reintento también falla (4xx/5xx)\n→ se extrae error.message.value\n→ se lanza HTTPException +``` + +## Parámetros OData y Headers SAP Soportados + +### Query Parameters (OData) + +| Parámetro | Uso | Ejemplo | +| -------------- | ---------------------------------- | -------------------------------- | +| `$skip` | Paginación — desplazamiento | `$skip=20` | +| `$top` | Paginación — límite por página | `$top=20` | +| `$select` | Selección de campos | `$select=ItemCode,ItemName` | +| `$filter` | Filtrado | `$filter=ItemCode eq 'A001'` | +| `$orderby` | Ordenamiento | `$orderby=ItemCode asc` | +| `$inlinecount` | Contar registros totales | `$inlinecount=allpages` | + +### Headers SAP + +| Header | Función | +| ----------------------------- | ----------------------------------------- | +| `B1S-PageSize` | Tamaño de página personalizado | +| `B1S-CaseInsensitive` | Búsqueda sin distinción mayúsc/minúscula | +| `B1S-ReplaceCollectionsOnPatch` | Reemplazar colecciones completas en PATCH | + +## Manejo de Errores + +| Situación | Comportamiento | +| ----------------------- | ---------------------------------------------------------- | +| SAP devuelve 2xx | Retorna `Response` al router (200 body, 201 body, 204 vacío) | +| SAP devuelve 401 | Re-autentica automáticamente y reintenta 1 vez | +| SAP devuelve 4xx/5xx | Extrae `error.message.value` y lanza `HTTPException(status_code, detail)` | +| Timeout de conexión | Lanza `ConnectionError` | +| Error de red | Lanza `ConnectionError` | +| Reintento también falla | Lanza `HTTPException` con el mensaje de error SAP | +| SAP no disponible | `HTTPException(500)` | diff --git a/mini-backend/logger_config.py b/mini-backend/logger_config.py new file mode 100644 index 0000000..42ea836 --- /dev/null +++ b/mini-backend/logger_config.py @@ -0,0 +1,85 @@ +import os +import logging +import json +import sys +from datetime import datetime, timezone +from context import get_request_id, get_current_user # Importamos las funciones +from logging.handlers import RotatingFileHandler + + +# ============================================================================ +class JsonFormatter(logging.Formatter): + """Formateador personalizado para exportar logs en formato JSON puro.""" + EXTRA_FIELDS = ("http_method", "endpoint", "status_code") + + def format(self, record): + user = get_current_user() + log_record = { + "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), + "level": record.levelname, + "request_id": get_request_id(), # ⚡️ Inyección automática del ID de rastreo + "webUser": user["webUser"], + "emp_id": user["emp_id"], + "full_name": user["full_name"], + "message": record.getMessage(), + "module": record.module, + "function": record.funcName, + "line": record.lineno + } + for field in self.EXTRA_FIELDS: + if hasattr(record, field): + log_record[field] = getattr(record, field) + # Si el log incluye excepciones, las añade al JSON + if record.exc_info: + log_record["exception"] = self.formatException(record.exc_info) + return json.dumps(log_record, ensure_ascii=False) + + +# ============================================================================ +def setup_logging(environment: str = "production"): + """Configura el logger global de la aplicación.""" + root_logger = logging.getLogger() + + # Evita duplicar logs si la función se ejecuta más de una vez + if root_logger.hasHandlers(): + root_logger.handlers.clear() + + # Definir el nivel mínimo de captura (INFO por defecto) + root_logger.setLevel(logging.INFO) + + # Handler para la consola (Standard Output) + console_handler = logging.StreamHandler(sys.stdout) + + if environment == "production": + # En producción usamos JSON estructurado + console_handler.setFormatter(JsonFormatter()) + else: + # En desarrollo usamos texto limpio y con colores opcionales + readable_formatter = logging.Formatter( + fmt="%(asctime)s [%(levelname)s] %(module)s.%(funcName)s:%(lineno)d - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + console_handler.setFormatter(readable_formatter) + + root_logger.addHandler(console_handler) + + # 2. CREAR CARPETA Y ARCHIVO FÍSICO EN DISCO + log_dir = "logs" + if not os.path.exists(log_dir): + os.makedirs(log_dir) # Crea la carpeta 'logs/' si no existe + + # Usamos 'RotatingFileHandler' para que el archivo no crezca infinitamente. + # Cuando llegue a 5MB, lo cierra, lo renombra a backend.log.1 y crea uno nuevo. + file_handler = RotatingFileHandler( + filename=os.path.join(log_dir, "backend.log"), + maxBytes=5 * 1024 * 1024, # 5 Megabytes + backupCount=3, # Mantener máximo 3 archivos viejos de respaldo + encoding="utf-8" + ) + file_handler.setFormatter(JsonFormatter()) + root_logger.addHandler(file_handler) + + + # Silenciar logs ruidosos de librerías externas (opcional) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) +# ============================================================================ \ No newline at end of file diff --git a/mini-backend/main.py b/mini-backend/main.py new file mode 100644 index 0000000..c00d830 --- /dev/null +++ b/mini-backend/main.py @@ -0,0 +1,108 @@ +import os +import uuid +import uvicorn +import urllib3 +import logging +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from contextlib import asynccontextmanager +from dotenv import load_dotenv +from logger_config import setup_logging +from context import set_request_id, get_request_id, set_current_user +from core.session import validate_and_refresh_session, get_employee_name, get_emp_id + +from routers.auth import router as auth_router +from routers.business_partners import router as bp_router +from routers.items import router as items_router +from routers.sales import router as sales_router +from routers.purchases import router as purchases_router +from routers.catalogs import router as catalogs_router +from routers.batch import router as batch_router +from routers.permissions import router as permissions_router +from routers.apigateway import router as apigateway_router +from routers.reports import router as reports_router + +# ============================================================================ +load_dotenv(dotenv_path=".env") + +logger = logging.getLogger(__name__) + +LOG_ENVIRONMENT = os.getenv("LOG_ENVIRONMENT", "production") + +# ============================================================================ +@asynccontextmanager +async def lifespan(app: FastAPI): + setup_logging(environment=LOG_ENVIRONMENT) + yield + + +app = FastAPI( + title="SAP Business One Integration API", + description="API para integrar Socios de negocios con SAP Business One Service Layer.", + version="1.1.0", + lifespan=lifespan +) + +app.include_router(auth_router) +app.include_router(bp_router) +app.include_router(items_router) +app.include_router(sales_router) +app.include_router(purchases_router) +app.include_router(catalogs_router) +app.include_router(batch_router) +app.include_router(permissions_router) +app.include_router(apigateway_router) +app.include_router(reports_router) + +# ============================================================================ +@app.middleware("http") +async def correlation_id_middleware(request: Request, call_next): + req_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) + set_request_id(req_id) + response = await call_next(request) + response.headers["X-Request-ID"] = get_request_id() + return response + +# ============================================================================ +@app.middleware("http") +async def session_middleware(request: Request, call_next): + if request.url.path == "/api/Login": + return await call_next(request) + + if request.url.path.startswith("/api/"): + token = request.headers.get("X-Session-Token") + if not token: + logger.warning(f"Request sin token: {request.method} {request.url.path}") + return JSONResponse( + status_code=401, + content={"detail": "Se requiere autenticación. Inicie sesión primero."} + ) + + webUser = validate_and_refresh_session(token) + if not webUser: + logger.warning(f"Token inválido o expirado en: {request.method} {request.url.path}") + return JSONResponse( + status_code=401, + content={"detail": "Sesión expirada. Debe iniciar sesión nuevamente."} + ) + + set_current_user(webUser, get_employee_name(webUser), get_emp_id(webUser)) + request.state.webUser = webUser + + return await call_next(request) + +# ============================================================================ +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +urllib3.disable_warnings() + +# ============================================================================ +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug", access_log=True) diff --git a/mini-backend/menu_permissions.json b/mini-backend/menu_permissions.json new file mode 100644 index 0000000..b4d6968 --- /dev/null +++ b/mini-backend/menu_permissions.json @@ -0,0 +1,22 @@ +{ + "renato.ortega@tagumedica.com": [ + "*" + ], + "rodrigo.catari@tagumedica.com": [ + "/inventario/*" + ], + "jose.huayta@tagumedica.com": [ + "/inventario/articulos", + "/ventas/*", + "/compras/*" + ], + "ricardo.merino@tagumedica.com": [ + "/socios/*" + ], + "renato.bustamante@tagumedica.com": [ + "/inventario/articulos", + "/inventario/lotes", + "/ventas/*", + "/compras/*" + ] +} \ No newline at end of file diff --git a/mini-backend/requirements.txt b/mini-backend/requirements.txt new file mode 100644 index 0000000..a442cec --- /dev/null +++ b/mini-backend/requirements.txt @@ -0,0 +1,35 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.13.0 +brotli==1.2.0 +certifi==2026.5.20 +cffi==2.1.1 +charset-normalizer==3.4.7 +click==8.4.1 +cssselect2==0.9.0 +dotenv==0.9.9 +fastapi==0.136.3 +fonttools==4.63.0 +h11==0.16.0 +hdbcli==2.29.25 +idna==3.17 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +pillow==12.3.0 +pycparser==3.0 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydyf==0.12.1 +pyphen==0.18.1 +python-dotenv==1.2.2 +requests==2.34.2 +starlette==1.2.0 +tinycss2==1.5.1 +tinyhtml5==2.1.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.7.0 +uvicorn==0.48.0 +weasyprint==69.0 +webencodings==0.5.1 +zopfli==0.4.3 diff --git a/mini-backend/routers/__init__.py b/mini-backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mini-backend/routers/apigateway.py b/mini-backend/routers/apigateway.py new file mode 100644 index 0000000..5da6a60 --- /dev/null +++ b/mini-backend/routers/apigateway.py @@ -0,0 +1,52 @@ +import base64 +from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse +from core.api_gateway import forward_to_api_gateway +from typing import Any, List +from io import BytesIO + +router = APIRouter( + tags=["APIGateway - Crystal Reports"] +) + + +@router.get("/api/apigateway/reports") +async def list_reports(): + resp = forward_to_api_gateway( + method="GET", + endpoint="rs/v1/LoadAuthorizedCRList" + ) + data = resp.json() + if isinstance(data, list): + return data + if isinstance(data, dict): + return data.get("value", data.get("AuthorizedCRList", [data])) + return [] + + +@router.get("/api/apigateway/reports/{docCode}/parameters") +async def get_report_parameters(docCode: str): + resp = forward_to_api_gateway( + method="GET", + endpoint=f"rs/v1/LoadCR", + params={"DocCode": docCode} + ) + return resp.json() + + +@router.post("/api/apigateway/reports/{docCode}/export") +async def export_report_pdf(docCode: str, body: List[Any]): + resp = forward_to_api_gateway( + method="POST", + endpoint="rs/v1/ExportPDFData", + params={"DocCode": docCode}, + json_data=body, + ) + pdf_bytes = base64.b64decode(resp.text) + return StreamingResponse( + BytesIO(pdf_bytes), + media_type="application/pdf", + headers={ + "Content-Disposition": f'inline; filename="{docCode}.pdf"', + } + ) diff --git a/mini-backend/routers/auth.py b/mini-backend/routers/auth.py new file mode 100644 index 0000000..eca7078 --- /dev/null +++ b/mini-backend/routers/auth.py @@ -0,0 +1,90 @@ +import os +import logging +from fastapi import APIRouter, Request, HTTPException +from core.sap import forward_to_sapb1, ensure_sap_session +from core.session import create_employee_session, remove_employee_session_by_token +from context import set_current_user +from dotenv import load_dotenv + + + +# Cargar variables de entorno desde .env +load_dotenv(dotenv_path=".env") +SAPB1_SL_COMPANYDB = os.getenv("SAPB1_SL_COMPANYDB", "") + +logger = logging.getLogger(__name__) + +router = APIRouter( + tags=["Autenticación"] +) + + +@router.post("/api/Login") +async def login(request: Request): + data = await request.json() + webUser = data.get("Username") + password = data.get("Password") + + logger.info(f"Intento de login recibido para el usuario web {webUser}") + + webUser_escaped = (webUser or "").replace("'", "''") + + try: + employee_response = forward_to_sapb1( + method="GET", + endpoint_path="EmployeesInfo", + params={ + "$select": "EmployeeID,FirstName,LastName,Active,U_WEB_USER,U_WEB_PASS", + "$filter": f"U_WEB_USER eq '{webUser_escaped}'" + } + ) + except HTTPException: + raise HTTPException(status_code=401, detail="Usuario o contraseña incorrectos") + except Exception as exc: + logger.error(f"Error al consultar empleados en SAP a través de proxy: {exc}") + raise HTTPException(status_code=500, detail=f"Error al consultar empleados en SAP: {exc}") + + employees = employee_response.json().get("value", []) + employee_data = employees[0] if employees else None + + if employee_data is None: + logger.warning(f"Login fallido: no se encontró ningún empleado con U_WEB_USER {webUser}") + raise HTTPException(status_code=401, detail="Usuario no registrado") + + empId = employee_data.get("EmployeeID") + + if employee_data.get("Active") != "tYES": + logger.warning(f"Login fallido: el empleado {empId} está inactivo") + raise HTTPException(status_code=401, detail="Usuario inactivo") + + if employee_data.get("U_WEB_PASS") != password: + logger.warning(f"Login fallido: la contraseña no coincide para el empleado {empId}") + raise HTTPException(status_code=401, detail="Usuario o contraseña incorrectos") + + full_name = f"{employee_data.get('FirstName', '')} {employee_data.get('LastName', '')}".strip() + set_current_user(webUser, full_name, empId) + logger.info(f"Login exitoso para el usuario {webUser} (empleado SAP {empId})") + + token = create_employee_session( + webUser=webUser, + emp_id=empId, + first_name=employee_data.get('FirstName', ''), + last_name=employee_data.get('LastName', '') + ) + + ensure_sap_session() + + return { + "message": "Login exitoso", + "token": token, + "username": employee_data.get('FirstName'), + "company": SAPB1_SL_COMPANYDB + } + + +@router.post("/api/Logout") +async def logout(request: Request): + token = request.headers.get("X-Session-Token") + if token: + remove_employee_session_by_token(token) + return {"message": "Logout exitoso"} \ No newline at end of file diff --git a/mini-backend/routers/batch.py b/mini-backend/routers/batch.py new file mode 100644 index 0000000..0ab8cce --- /dev/null +++ b/mini-backend/routers/batch.py @@ -0,0 +1,8 @@ +from core.crud_router import make_crud_router + +router = make_crud_router( + "BatchNumberDetails", + tags=["Lotes"], + supports_orderby=True, + supports_create=False, +) diff --git a/mini-backend/routers/business_partners.py b/mini-backend/routers/business_partners.py new file mode 100644 index 0000000..5260008 --- /dev/null +++ b/mini-backend/routers/business_partners.py @@ -0,0 +1,8 @@ +from core.crud_router import make_crud_router + +router = make_crud_router( + "BusinessPartners", + tags=["Socios de Negocio"], + key_field="CardCode", + key_quoted=True, +) diff --git a/mini-backend/routers/catalogs.py b/mini-backend/routers/catalogs.py new file mode 100644 index 0000000..4dab655 --- /dev/null +++ b/mini-backend/routers/catalogs.py @@ -0,0 +1,79 @@ +from fastapi import APIRouter +from core.sap import forward_to_sapb1 +from core.crud_router import make_crud_router + + +router = APIRouter( + tags=["Catálogos"] +) + +for _resource in ("Warehouses", "SalesTaxCodes", "FactoringIndicators"): + router.include_router( + make_crud_router( + _resource, + tags=["Catálogos"], + supports_get_by_key=False, + supports_create=False, + supports_patch=False, + ) + ) + + +@router.get("/api/Currencies") +async def get_currencies(): + return forward_to_sapb1( + method="GET", + endpoint_path="Currencies", + params={"$select": "Code,Name", "$top": 200} + ).json() + + +@router.get("/api/BusinessPartnerGroups") +async def get_business_partner_groups(): + return forward_to_sapb1( + method="GET", + endpoint_path="BusinessPartnerGroups", + params={"$select": "Number,GroupName"} + ).json() + + +@router.get("/api/Sunat/BusinessPartners/DocumentType") +async def get_sunat_businesspartners_document_type(): + + sap_params = { + "$skip": 0, + "$top": 200, + "$inlinecount": "allpages" + } + + sap_params["$filter"] = "TableName eq 'OCRD' and Name eq 'EXX_TIPODOCU'" + + + base_headers = {"B1S-PageSize": "200"} + + return forward_to_sapb1( + method="GET", + endpoint_path="UserFieldsMD", + params=sap_params, + extra_headers=base_headers + ).json() + +@router.get("/api/ItemGroups") +async def get_item_groups(): + + sap_params = { + "$skip": 0, + "$top": 200, + "$inlinecount": "allpages" + } + sap_params["$select"] = "Number,GroupName" + + base_headers = {"B1S-PageSize": "200"} + + return forward_to_sapb1( + method="GET", + endpoint_path="ItemGroups", + params=sap_params, + extra_headers=base_headers + ).json() +# diff --git a/mini-backend/routers/items.py b/mini-backend/routers/items.py new file mode 100644 index 0000000..30c8b0f --- /dev/null +++ b/mini-backend/routers/items.py @@ -0,0 +1,8 @@ +from core.crud_router import make_crud_router + +router = make_crud_router( + "Items", + tags=["Artículos"], + key_field="ItemCode", + key_quoted=True, +) diff --git a/mini-backend/routers/permissions.py b/mini-backend/routers/permissions.py new file mode 100644 index 0000000..b05173b --- /dev/null +++ b/mini-backend/routers/permissions.py @@ -0,0 +1,26 @@ +import json +import os +from fastapi import APIRouter, HTTPException, Request + +MENU_PERMISSIONS_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "menu_permissions.json") + +router = APIRouter(tags=["Permisos"]) + + +def load_permissions() -> dict: + if not os.path.exists(MENU_PERMISSIONS_PATH): + return {} + with open(MENU_PERMISSIONS_PATH, "r") as f: + return json.load(f) + + +@router.get("/api/user/menu") +async def get_user_menu(request: Request): + webUser = str(getattr(request.state, "webUser", "")) + if not webUser: + raise HTTPException(status_code=401, detail="Usuario no autenticado") + + permissions = load_permissions() + allowed_paths = permissions.get(webUser, []) + + return {"allowedPaths": allowed_paths} diff --git a/mini-backend/routers/purchases.py b/mini-backend/routers/purchases.py new file mode 100644 index 0000000..1dc8228 --- /dev/null +++ b/mini-backend/routers/purchases.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from core.crud_router import make_crud_router + +router = APIRouter() + +_MARKETING_DOC_PATCH_HEADERS = {"B1S-ReplaceCollectionsOnPatch": "true"} + +for _resource in ("PurchaseOrders", "PurchaseDeliveryNotes", "PurchaseInvoices"): + router.include_router( + make_crud_router( + _resource, + tags=["Compras"], + key_field="DocEntry", + supports_orderby=True, + patch_headers=_MARKETING_DOC_PATCH_HEADERS, + ) + ) diff --git a/mini-backend/routers/reports.py b/mini-backend/routers/reports.py new file mode 100644 index 0000000..255da15 --- /dev/null +++ b/mini-backend/routers/reports.py @@ -0,0 +1,172 @@ +import logging +from io import BytesIO +from pathlib import Path +from typing import Literal + +from fastapi import APIRouter, HTTPException +from fastapi.responses import HTMLResponse, StreamingResponse +from jinja2 import Environment, FileSystemLoader +from pydantic import BaseModel +from weasyprint import HTML + +from hdbcli import dbapi + +from core.hana import hana_connection, HANA_SCHEMA + +router = APIRouter( + tags=["Reportes (Jinja/HTML)"] +) + +TEMPLATES_REPORTS_DIR = Path(__file__).resolve().parent.parent / "templates" / "reports" + +templates_env = Environment(loader=FileSystemLoader("templates/reports")) + +logger = logging.getLogger(__name__) + +# Tamaño de papel que usa cada reporte al renderizar el PDF (CSS @page.size). +PAPER_SIZES = {"A4": "A4", "Carta": "letter"} + +# Alto útil de la página en orientación horizontal (tamaño de papel menos los +# 0.3in de margen superior e inferior definidos en el @page de stickers.html). +PAGINA_ALTO_LANDSCAPE = {"A4": "7.67in", "Carta": "7.9in"} + +# Query original de Crystal Reports para DLN20015, adaptada: el placeholder +# de Crystal `{?Dockey@}` se reemplaza por un bind param `?` de hdbcli, y cada +# tabla queda calificada con el schema de la compañía SAP B1 (HANA_SCHEMA). +STICKERS_QUERY = f""" + SELECT TOP 1 + T0."DocEntry", + T0."DocNum", + CASE + WHEN IFNULL(T0."NumAtCard",'') <> '' THEN T0."NumAtCard" + ELSE T0."FolioPref"||'-'||T0."FolioNum" + END AS "GUIA_REMISION", + T0."U_SIS_ORDCOM" AS "ORDEN_COMPRA", + CASE + WHEN T0."Indicator" = 'AN' THEN 'ANULADO' + END AS "ESTADO", + T0."CardCode", + T0."CardName", + T4."Phone1" AS "TELEFONO", + T7."StreetS" AS "DIRECCION", + T7."CountyS" AS "PROVINCIA", + T14."Name" AS "DEPARTAMENTO", + T7."ZipCodeS" AS "DISTRITO", + T6."IndDesc" AS "GRUPO_CLIENTE", + CASE + WHEN T4."GroupCode" = '100' THEN 'VENTA LOCAL' + WHEN T4."GroupCode" = '102' THEN 'VENTA EXTERIOR' + END AS "TIPO_VENTA", + T0."DocDate" AS "FECHA", + T1."ItemCode", + T2."ItemName", + T3."Name" AS "LINEA", + T10."BatchNum" AS "LOTE", + T1."unitMsr" AS "UNIDADES", + T10."Quantity" AS "CANTIDAD", + T0."Indicator" AS "DOCUMENTO", + T0."U_SIS_FECENT" AS "FECHA_ENTREGA", + T8."SlpName" AS "REPRESENTANTE", + UPPER(T9."U_NAME") AS "ASISTENTE_VENTA", + T0."Comments" AS "COMENTARIOS", + CASE T0."U_SIS_TIPDESP" + WHEN 'N' THEN 'LOCAL' + WHEN 'A' THEN 'PROVINCIA' + WHEN 'U' THEN 'LOCAL - URGENTE' + WHEN 'O' THEN 'OFICINA' + WHEN 'V' THEN '-' + WHEN 'J' THEN 'PROVINCIA -URGENTE' + WHEN 'R' THEN 'REGULARIZACION' + ELSE 'LOCAL' + END AS "TIPO_DESPACHO", + T11."ExpDate" AS "FECHA VENCIMIENTO", + CASE T0."U_SIS_HORREC" + WHEN '100' THEN 'SIN HORARIO' + WHEN '101' THEN '08 AM - 10 AM' + WHEN '102' THEN '10 AM - 01 PM' + WHEN '103' THEN '01 PM - 03 PM' + WHEN '104' THEN '03 PM - 06 PM' + ELSE 'SIN HORARIO' + END AS "HORARIO RECOJO", + T11."LotNumber" AS "UBICACION", + T1."WhsCode" AS "ALMACEN", + T1."VisOrder", + T0.U_EXX_NOMTRANS AS "TRANSPORTISTA", + ROW_NUMBER() OVER (PARTITION BY T1."VisOrder" ORDER BY T1."VisOrder" DESC) AS row_num + FROM "{HANA_SCHEMA}".ODLN T0 + INNER JOIN "{HANA_SCHEMA}".DLN1 T1 ON T0."DocEntry" = T1."DocEntry" + INNER JOIN "{HANA_SCHEMA}".OITM T2 ON T1."ItemCode" = T2."ItemCode" + LEFT JOIN "{HANA_SCHEMA}"."@SIS_LINART" T3 ON T2.U_SIS_LINART = T3."Code" + INNER JOIN "{HANA_SCHEMA}".OCRD T4 ON T0."CardCode" = T4."CardCode" + INNER JOIN "{HANA_SCHEMA}".NNM1 T5 ON T0."Series"=T5."Series" + LEFT JOIN "{HANA_SCHEMA}".OOND T6 ON T4."IndustryC" = T6."IndCode" + LEFT JOIN "{HANA_SCHEMA}".DLN12 T7 ON T0."DocEntry" = T7."DocEntry" + LEFT JOIN "{HANA_SCHEMA}".OCST T14 ON T14."Code" = T7."StateS" AND T14."Country" = T7."CountryS" + LEFT JOIN "{HANA_SCHEMA}".OSLP T8 ON T0."SlpCode" = T8."SlpCode" + LEFT JOIN "{HANA_SCHEMA}".OUSR T9 ON T0."UserSign" = T9."USERID" + LEFT JOIN "{HANA_SCHEMA}".IBT1 T10 ON T1."ItemCode"=T10."ItemCode" AND T0."DocEntry"=T10."BaseEntry" AND T1."WhsCode"=T10."WhsCode" AND T0."ObjType"=T10."BaseType" AND T0."DocNum"=T10."BaseNum" AND T1."LineNum" = T10."BaseLinNum" AND T10."Direction"='1' + LEFT JOIN "{HANA_SCHEMA}".OBTN T11 ON T11."ItemCode" = T1."ItemCode" AND T10."BatchNum" = T11."DistNumber" + WHERE T0."DocEntry" = ? +""" + + +# ============================================================================ +class StickersParams(BaseModel): + doc_entry: int + cantidad: int + formato: Literal["pdf", "html"] = "pdf" + papel: Literal["A4", "Carta"] = "Carta" + + +# ============================================================================ +@router.post("/api/reports/stickers") +async def generar_stickers(params: StickersParams): + log_extra = {"http_method": "POST", "endpoint": "reports/stickers"} + logger.info(f"Generando reporte de stickers para DocEntry {params.doc_entry}", extra=log_extra) + + try: + with hana_connection() as conn: + cursor = conn.cursor() + cursor.execute(STICKERS_QUERY, (params.doc_entry,)) + row = cursor.fetchone() + columns = [d[0] for d in cursor.description] + except dbapi.Error as exc: + logger.error(f"Error al consultar la guía de remisión {params.doc_entry}: {exc}", extra=log_extra) + raise HTTPException(status_code=500, detail=f"Error al consultar el reporte: {exc}") + + if not row: + logger.warning(f"DocEntry {params.doc_entry} no encontrado", extra=log_extra) + raise HTTPException(status_code=404, detail=f"No se encontró el documento {params.doc_entry}") + + fila = dict(zip(columns, row)) + lugar = " - ".join( + p for p in (fila.get("DEPARTAMENTO"), fila.get("PROVINCIA"), fila.get("DISTRITO")) if p + ) + datos = { + "cliente": fila.get("CardName"), + "direccion": fila.get("DIRECCION"), + "lugar": lugar, + "documento": fila.get("GUIA_REMISION"), + } + stickers = [{"numero": i, "total": params.cantidad} for i in range(1, params.cantidad + 1)] + paginas = [stickers[i:i + 4] for i in range(0, len(stickers), 4)] + + template = templates_env.get_template("stickers.html") + html_renderizado = template.render( + datos=datos, + paginas=paginas, + paper_size=PAPER_SIZES[params.papel], + pagina_alto=PAGINA_ALTO_LANDSCAPE[params.papel], + ) + + if params.formato == "html": + logger.info(f"Reporte de stickers generado en HTML para DocEntry {params.doc_entry}", extra={**log_extra, "status_code": 200}) + return HTMLResponse(content=html_renderizado) + + pdf_bytes = HTML(string=html_renderizado, base_url=str(TEMPLATES_REPORTS_DIR)).write_pdf() + logger.info(f"Reporte de stickers generado en PDF para DocEntry {params.doc_entry}", extra={**log_extra, "status_code": 200}) + return StreamingResponse( + BytesIO(pdf_bytes), + media_type="application/pdf", + headers={"Content-Disposition": f'inline; filename="stickers_{params.doc_entry}.pdf"'}, + ) diff --git a/mini-backend/routers/sales.py b/mini-backend/routers/sales.py new file mode 100644 index 0000000..458f9b1 --- /dev/null +++ b/mini-backend/routers/sales.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from core.crud_router import make_crud_router + +router = APIRouter() + +_MARKETING_DOC_PATCH_HEADERS = {"B1S-ReplaceCollectionsOnPatch": "true"} + +for _resource in ("Invoices", "Orders", "DeliveryNotes"): + router.include_router( + make_crud_router( + _resource, + tags=["Ventas"], + key_field="DocEntry", + supports_orderby=True, + patch_headers=_MARKETING_DOC_PATCH_HEADERS, + ) + ) diff --git a/mini-backend/templates/reports/fonts/Orbitron-Bold.ttf b/mini-backend/templates/reports/fonts/Orbitron-Bold.ttf new file mode 100644 index 0000000..1172f55 Binary files /dev/null and b/mini-backend/templates/reports/fonts/Orbitron-Bold.ttf differ diff --git a/mini-backend/templates/reports/stickers.html b/mini-backend/templates/reports/stickers.html new file mode 100644 index 0000000..c17b788 --- /dev/null +++ b/mini-backend/templates/reports/stickers.html @@ -0,0 +1,127 @@ + + + + + + + + {% for pagina in paginas %} +
+ {% for sticker in pagina %} +
+
TAGUMEDICA
+ +
+
DATOS DEL CLIENTE:
+
{{ datos.cliente }}
+
+ +
+
DIRECCIÓN:
+
{{ datos.direccion }}
+
+ +
+
LUGAR:
+
{{ datos.lugar }}
+
+ +
+
+
DOCUMENTO:
+
{{ datos.documento }}
+
+
{{ sticker.numero }}/{{ sticker.total }}
+
+
+ {% endfor %} +
+ {% endfor %} + + + diff --git a/mini-backend/test/ApiGateway/ExportPDFData.yml b/mini-backend/test/ApiGateway/ExportPDFData.yml new file mode 100644 index 0000000..e65d504 --- /dev/null +++ b/mini-backend/test/ApiGateway/ExportPDFData.yml @@ -0,0 +1,42 @@ +info: + name: ExportPDFData + type: http + seq: 1 + +http: + method: POST + url: https://192.168.1.175:60000/rs/v1/ExportPDFData?DocCode=DLN20015 + params: + - name: DocCode + value: DLN20015 + type: query + body: + type: json + data: |- + [ + { + "name": "Dockey@", + "type": "xsd:decimal", + "value": [ + [ + "137138" + ] + ] + }, + { + "name": "CANTIDAD", + "type": "xsd:decimal", + "value": [ + [ + "5" + ] + ] + } + ] + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/ApiGateway/LoadAuthorizedCRList.yml b/mini-backend/test/ApiGateway/LoadAuthorizedCRList.yml new file mode 100644 index 0000000..ad0e9d2 --- /dev/null +++ b/mini-backend/test/ApiGateway/LoadAuthorizedCRList.yml @@ -0,0 +1,15 @@ +info: + name: LoadAuthorizedCRList + type: http + +http: + method: GET + url: https://192.168.1.175:60000/rs/v1/LoadAuthorizedCRList + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 +​ \ No newline at end of file diff --git a/mini-backend/test/ApiGateway/LoadCR.yml b/mini-backend/test/ApiGateway/LoadCR.yml new file mode 100644 index 0000000..ee0f641 --- /dev/null +++ b/mini-backend/test/ApiGateway/LoadCR.yml @@ -0,0 +1,18 @@ +info: + name: LoadCR + type: http + +http: + method: GET + url: https://192.168.1.175:60000/rs/v1/LoadCR?DocCode=DLN20015 + params: + - name: DocCode + value: DLN20015 + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/ApiGateway/Login.yml b/mini-backend/test/ApiGateway/Login.yml new file mode 100644 index 0000000..96c6a9b --- /dev/null +++ b/mini-backend/test/ApiGateway/Login.yml @@ -0,0 +1,23 @@ +info: + name: Login + type: http + +http: + method: POST + url: https://192.168.1.175:60000/login + body: + type: json + data: |- + { + "CompanyDB":"SBO_TAGUMEDICA_21", + "UserName": "manager", + "Password": "m1r1", + "DBInstance":"NDB@192.168.1.175:30013" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/ApiGateway/opencollection.yml b/mini-backend/test/ApiGateway/opencollection.yml new file mode 100644 index 0000000..cd1f343 --- /dev/null +++ b/mini-backend/test/ApiGateway/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: ApiGateway +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/mini-backend/test/apiSN/Login.yml b/mini-backend/test/apiSN/Login.yml new file mode 100644 index 0000000..6add259 --- /dev/null +++ b/mini-backend/test/apiSN/Login.yml @@ -0,0 +1,22 @@ +info: + name: Login + type: http + seq: 1 + +http: + method: POST + url: http://127.0.0.1:8000/api/Login + body: + type: json + data: |- + { + "UserName":56, + "Password":"mngr" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/apiSN/opencollection.yml b/mini-backend/test/apiSN/opencollection.yml new file mode 100644 index 0000000..f9d7713 --- /dev/null +++ b/mini-backend/test/apiSN/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: apiSN +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/mini-backend/test/backend/Login.yml b/mini-backend/test/backend/Login.yml new file mode 100644 index 0000000..c9abc77 --- /dev/null +++ b/mini-backend/test/backend/Login.yml @@ -0,0 +1,21 @@ +info: + name: Login + type: http + +http: + method: POST + url: http://127.0.0.1:8000/api/Login + body: + type: json + data: |- + { + "Username":10, + "Password":"COMENA AGUIRRE" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/backend/Logout.yml b/mini-backend/test/backend/Logout.yml new file mode 100644 index 0000000..7a14c7d --- /dev/null +++ b/mini-backend/test/backend/Logout.yml @@ -0,0 +1,20 @@ +info: + name: Logout + type: http + +http: + method: POST + url: http://127.0.0.1:8000/api/Logout + body: + type: json + data: |- + { + "SessionId":"0427de1e-68c2-11f1-c000-005056a55f84-139742384043904-16982" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/backend/opencollection.yml b/mini-backend/test/backend/opencollection.yml new file mode 100644 index 0000000..93294cb --- /dev/null +++ b/mini-backend/test/backend/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: backend +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/mini-backend/test/sap-aberio/BusinessPartners.yml b/mini-backend/test/sap-aberio/BusinessPartners.yml new file mode 100644 index 0000000..a794746 --- /dev/null +++ b/mini-backend/test/sap-aberio/BusinessPartners.yml @@ -0,0 +1,21 @@ +info: + name: BusinessPartners + type: http + +http: + method: GET + url: https://192.168.9.3:50000/b1s/v1/BusinessPartners?$select=CardCode, CardName,FederalTaxID&$filter=contains(CardName, 'FERRETERIA') and CardType eq 'cCustomer' + params: + - name: $select + value: CardCode, CardName,FederalTaxID + type: query + - name: $filter + value: contains(CardName, 'FERRETERIA') and CardType eq 'cCustomer' + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/DeliveryNotes.yml b/mini-backend/test/sap-aberio/DeliveryNotes.yml new file mode 100644 index 0000000..c0ed0f4 --- /dev/null +++ b/mini-backend/test/sap-aberio/DeliveryNotes.yml @@ -0,0 +1,19 @@ +info: + name: DeliveryNotes + type: http + seq: 1 + +http: + method: GET + url: https://192.168.9.3:50000/b1s/v1/DeliveryNotes?$filter=Cancelled eq 'tNO' and U_BPP_MDSD eq 'T001' and U_BPP_MDTD eq '09' and contains(U_BPP_MDCD,'128452') + params: + - name: $filter + value: Cancelled eq 'tNO' and U_BPP_MDSD eq 'T001' and U_BPP_MDTD eq '09' and contains(U_BPP_MDCD,'128452') + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/Invoices.yml b/mini-backend/test/sap-aberio/Invoices.yml new file mode 100644 index 0000000..a7dda6e --- /dev/null +++ b/mini-backend/test/sap-aberio/Invoices.yml @@ -0,0 +1,25 @@ +info: + name: Invoices + type: http + seq: 1 + +http: + method: GET + url: https://192.168.9.3:50000/b1s/v1/Invoices?$select=DocEntry,DocNum,FederalTaxID,CardCode,CardName,NumAtCard,DocCurrency,DocTotal,DocTotalFc,FolioPrefixString,FolioNumber,DocumentStatus,DocumentSubType&$filter=DocumentStatus eq 'bost_Open'&$orderby=DocEntry desc + params: + - name: $select + value: DocEntry,DocNum,FederalTaxID,CardCode,CardName,NumAtCard,DocCurrency,DocTotal,DocTotalFc,FolioPrefixString,FolioNumber,DocumentStatus,DocumentSubType + type: query + - name: $filter + value: DocumentStatus eq 'bost_Open' + type: query + - name: $orderby + value: DocEntry desc + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/Items.yml b/mini-backend/test/sap-aberio/Items.yml new file mode 100644 index 0000000..5974801 --- /dev/null +++ b/mini-backend/test/sap-aberio/Items.yml @@ -0,0 +1,14 @@ +info: + name: Items + type: http + +http: + method: GET + url: https://192.168.9.3:50000/b1s/v1/Items + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/Login.yml b/mini-backend/test/sap-aberio/Login.yml new file mode 100644 index 0000000..fd8f93e --- /dev/null +++ b/mini-backend/test/sap-aberio/Login.yml @@ -0,0 +1,23 @@ +info: + name: Login + type: http + seq: 1 + +http: + method: POST + url: https://192.168.9.3:50000/b1s/v1/Login + body: + type: json + data: |- + { + "CompanyDB":"PRUEBA_20260701", + "UserName":"manager", + "Password":"s1st3m4s" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/PurchaseInvoices.yml b/mini-backend/test/sap-aberio/PurchaseInvoices.yml new file mode 100644 index 0000000..440e818 --- /dev/null +++ b/mini-backend/test/sap-aberio/PurchaseInvoices.yml @@ -0,0 +1,25 @@ +info: + name: PurchaseInvoices + type: http + seq: 1 + +http: + method: GET + url: https://192.168.9.3:50000/b1s/v1/PurchaseInvoices?$select=DocEntry,DocNum,FederalTaxID,CardCode,CardName,NumAtCard,DocCurrency,DocTotal,DocTotalFc,FolioPrefixString,FolioNumber,DocumentStatus,DocumentSubType&$filter=DocumentStatus eq 'bost_Open'&$orderby=DocEntry desc + params: + - name: $select + value: DocEntry,DocNum,FederalTaxID,CardCode,CardName,NumAtCard,DocCurrency,DocTotal,DocTotalFc,FolioPrefixString,FolioNumber,DocumentStatus,DocumentSubType + type: query + - name: $filter + value: DocumentStatus eq 'bost_Open' + type: query + - name: $orderby + value: DocEntry desc + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/mini-backend/test/sap-aberio/opencollection.yml b/mini-backend/test/sap-aberio/opencollection.yml new file mode 100644 index 0000000..a9b94a0 --- /dev/null +++ b/mini-backend/test/sap-aberio/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: sap-aberio +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/mini-backend/test/sap-tagumedica/sl.http b/mini-backend/test/sap-tagumedica/sl.http new file mode 100644 index 0000000..a27bd50 --- /dev/null +++ b/mini-backend/test/sap-tagumedica/sl.http @@ -0,0 +1,82 @@ +@url=https://192.168.1.175:50000/b1s/v2 + +### +# @noRejectUnauthorized +POST {{url}}/Login HTTP/1.1 +Content-Type: application/json + +{ + "CompanyDB": "SBO_TAGUMEDICA_21", + "UserName": "manager", + "Password": "m1r1" +} +### +# @noRejectUnauthorized +GET {{url}}/$metadata +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/EmployeesInfo?$select=EmployeeID,FirstName,LastName,Active,U_WEB_USER,U_WEB_PASS&$filter=U_WEB_USER eq 'renato.ortega@tagumedica.com' +Content-Type: application/json + + +### +# @noRejectUnauthorized +GET {{url}}/Items?$top=20&$orderby=ItemCode desc +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/DeliveryNotes(29360) +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/UserFieldsMD?$filter=TableName eq 'OCRD' and Name eq 'EXX_TIPODOCU' +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/Warehouses +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/PurchaseInvoices(135235) +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/Orders?$filter=DocumentStatus eq 'bost_Open' +Content-Type: application/json + +### +# @noRejectUnauthorized +POST {{url}}/MessagesService_GetInbox HTTP/1.1 +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/Messages(173810) +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/ItemGroups?$select=Number,GroupName +Content-Type: application/json + +### +# @noRejectUnauthorized +POST {{url}}/FactoringIndicators?$select=IndicatorCode,IndicatorName&$filter=U_EXX_REGVEN eq 'Y' +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/Currencies +Content-Type: application/json + +### +# @noRejectUnauthorized +GET {{url}}/BatchNumberDetails +Content-Type: application/json \ No newline at end of file diff --git a/mini-frontend/.dockerignore b/mini-frontend/.dockerignore new file mode 100644 index 0000000..3bdd52e --- /dev/null +++ b/mini-frontend/.dockerignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.DS_Store diff --git a/mini-frontend/.gitignore b/mini-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/mini-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/mini-frontend/Dockerfile b/mini-frontend/Dockerfile new file mode 100644 index 0000000..1af88d1 --- /dev/null +++ b/mini-frontend/Dockerfile @@ -0,0 +1,40 @@ +FROM node:20-alpine AS build + +WORKDIR /app + +# Certificado extra de la empresa (VPN, proxy o antivirus corporativo que +# intercepta HTTPS, ej. Zscaler/Netskope). Si mini-frontend/certs/ tiene un +# archivo .pem/.crt, npm confía en él para poder salir a internet durante el +# build. Si la carpeta está vacía (caso normal), este paso no hace nada. +COPY certs/ /usr/local/share/extra-ca/ +COPY package.json package-lock.json ./ +RUN CERT_FILE=$(find /usr/local/share/extra-ca -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" -o -name "*.cer" \) 2>/dev/null | head -n1); \ + if [ -n "$CERT_FILE" ]; then export NODE_EXTRA_CA_CERTS="$CERT_FILE"; fi; \ + npm ci + +COPY . . + +# Vite incrusta las variables VITE_* en el bundle en tiempo de build, no en +# tiempo de ejecución — por eso se pasa como build arg y no como env var del +# contenedor final. Debe ser la IP/hostname real del servidor donde corre el +# backend dentro de la red de la empresa (no "localhost": eso resolvería a la +# máquina de cada usuario, no al servidor). +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +RUN npm run build + +FROM nginx:alpine + +# Zona horaria del contenedor. Por defecto Perú (America/Lima); cambiar el +# build arg si se despliega para un cliente en otro país de Latinoamérica. +ARG TZ=America/Lima +ENV TZ=${TZ} +RUN apk add --no-cache tzdata \ + && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo ${TZ} > /etc/timezone + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/mini-frontend/README.md b/mini-frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/mini-frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/mini-frontend/docs/flujo-lotes.md b/mini-frontend/docs/flujo-lotes.md new file mode 100644 index 0000000..91d63ea --- /dev/null +++ b/mini-frontend/docs/flujo-lotes.md @@ -0,0 +1,160 @@ +# Flujo de Procesamiento — Detalle de Lotes + +## Arquitectura General + +```mermaid +graph TB + subgraph "Frontend React (sap-frontend)" + RT[Router App.tsx] + VISTA[BatchNumberDetails.tsx] + FORM[BatchForm.tsx] + LIST[BatchNumberDetailsList.tsx] + LIST_TMPL[ListViewTemplate.tsx] + UI_FF[FormattedField.tsx] + UI_SEL[SelectField.tsx] + UI_TABLE[DenseTable.tsx] + API[api.ts] + ENUMS[enums.ts] + TYPES[types/sapBatch.ts] + end + + subgraph "Backend FastAPI (backend-sn)" + ROUTER[routers/batch.py] + CORE[core/sap.py
forward_to_sapb1] + end + + subgraph "SAP Business One" + SL[Service Layer
BatchNumberDetails] + end + + RT --> VISTA + RT --> LIST + VISTA --> FORM + LIST --> LIST_TMPL + FORM --> UI_FF + FORM --> UI_SEL + FORM --> API + FORM --> ENUMS + FORM --> TYPES + LIST_TMPL --> UI_TABLE + LIST_TMPL --> API + + API -->|GET/PATCH| ROUTER + ROUTER -->|proxy| CORE + CORE -->|B1SESSION| SL +``` + +## Flujo Vista de Lista → Editar Lote + +```mermaid +sequenceDiagram + actor User as Usuario + participant Router as App.tsx + participant ListView as BatchNumberDetailsList + participant LVT as ListViewTemplate + participant DTable as DenseTable + participant DetailView as BatchNumberDetails + participant Form as BatchForm + participant API as api.ts + participant Backend as backend batch.py + participant SAP as SAP Service Layer + + User->>Router: /inventario/lotes + Router->>ListView: render + ListView->>LVT: + LVT->>API: GET /api/BatchNumberDetails?skip=0&top=50&fields=... + API->>Backend: fetch con X-Session-Token + Backend->>SAP: GET BatchNumberDetails (con B1SESSION) + SAP-->>Backend: JSON con datos + Backend-->>API: JSON response + API-->>LVT: json.value[] + LVT->>DTable: render rows + DTable-->>User: tabla con lotes + + User->>DTable: clic en editar (lote 2032586) + DTable->>Router: navigate("/inventario/lotes/mantenimiento?code=2032586") + Router->>DetailView: render con searchParams.code + DetailView->>Form: + Form->>API: GET /api/BatchNumberDetails/2032586 + API->>Backend: fetch + Backend->>SAP: GET BatchNumberDetails(2032586) + SAP-->>Backend: {Batch, ItemCode, U_SIS_ESTLOT, U_SIS_ESTLIB, ...} + Backend-->>API: JSON raw SAP + API-->>Form: data + Form->>Form: mapSapToFriendly(data) + Note over Form: U_SIS_ESTLOT → EstadoAprobacion
U_SIS_ESTLIB → EstadoLiberacion
U_SIS_FECAPROB → FechaAprobacion + + Form->>UI_SEL: + Form->>UI_FF: + Form-->>User: formulario con valores cargados + + User->>Form: edita campos y clic "Actualizar Lote" + Form->>API: PATCH /api/BatchNumberDetails/2032586 + Note over Form,API: body: {U_SIS_ESTLOT: "102", U_SIS_OBSER: "...", ...} + API->>Backend: fetch PATCH + Backend->>SAP: PATCH BatchNumberDetails(2032586) + SAP-->>Backend: 204 No Content + Backend-->>API: 204 + API-->>Form: ok + Form-->>User: "Lote actualizado exitosamente" +``` + +## Árbol de Componentes + +``` +App.tsx (BrowserRouter) +├── /login +│ └── SapLogin.tsx +└── / (ProtectedRoute → MainLayout) + └── /inventario + ├── /lotes + │ └── BatchNumberDetailsList.tsx + │ └── ListViewTemplate.tsx + │ ├── SearchInput.tsx + │ ├── PrimaryButton.tsx + │ ├── DenseTable.tsx + │ │ └── formatValue() [formatters.ts] + │ └── PaginationBar.tsx + └── /lotes/mantenimiento + └── BatchNumberDetails.tsx + └── BatchForm.tsx + ├── FormattedField.tsx (x10) + └── SelectField.tsx (x2) +``` + +## Mapeo de Nombres SAP → Friendly + +| Campo SAP | Nombre Amigable | Tipo | Componente | +|---|---|---|---| +| `Batch` | `Batch` | display | FormattedField | +| `ItemCode` | `ItemCode` | display | FormattedField | +| `ItemDescription` | `ItemDescription` | display | FormattedField | +| `DocEntry` | `DocEntry` | display | FormattedField | +| `BatchAttribute1` | `BatchAttribute1` | input text | FormattedField | +| `BatchAttribute2` | `BatchAttribute2` | input text | FormattedField | +| `AdmissionDate` | `AdmissionDate` | display date | FormattedField | +| `ManufacturingDate` | `ManufacturingDate` | display date | FormattedField | +| `ExpirationDate` | `ExpirationDate` | input date | FormattedField | +| `U_SIS_ESTLOT` | `EstadoAprobacion` | select | SelectField | +| `U_SIS_FECAPROB` | `FechaAprobacion` | input date | FormattedField | +| `U_SIS_RESPAPROB` | `ResponsableAprobacion` | input text | FormattedField | +| `U_SIS_OBSER` | `Observaciones` | input text | FormattedField | +| `U_SIS_ESTLIB` | `EstadoLiberacion` | select | SelectField | +| `U_SIS_FECLIB` | `FechaLiberacion` | input date | FormattedField | +| `U_SIS_HORAPROB` | `HoraAprobacion` | input text | FormattedField | + +## Archivos Clave + +| Archivo | Ruta | Propósito | +|---|---|---| +| `BatchNumberDetails.tsx` | `src/views/inventario/` | Vista detalle, extrae `code` de URL y renderiza `BatchForm` | +| `BatchForm.tsx` | `src/components/` | Formulario edición, fetch + mapeo SAP + PATCH | +| `SelectField.tsx` | `src/components/ui/` | Select reutilizable con `String(value)` para compatibilidad | +| `FormattedField.tsx` | `src/components/ui/` | Campo display/input con formato (fechas, moneda, etc.) | +| `BatchNumberDetailsList.tsx` | `src/views/inventario/` | Lista con columnas SAP directas | +| `ListViewTemplate.tsx` | `src/components/ui/` | Template genérico de listado con paginación/búsqueda | +| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout | +| `enums.ts` | `src/utils/` | Mapeos enum SAP (BATCH_STATUS_APROB, etc.) | +| `sapBatch.ts` | `src/types/` | Interfaz TypeScript BatchNumberDetail | +| `batch.py` | `backend-sn/routers/` | Rutas FastAPI GET/PATCH BatchNumberDetails | +| `sap.py` | `backend-sn/core/` | Proxy universal a SAP Service Layer | diff --git a/mini-frontend/eslint.config.js b/mini-frontend/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/mini-frontend/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/mini-frontend/index.html b/mini-frontend/index.html new file mode 100644 index 0000000..53c1b4a --- /dev/null +++ b/mini-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + sap-frontend + + +
+ + + diff --git a/mini-frontend/nginx.conf b/mini-frontend/nginx.conf new file mode 100644 index 0000000..b18a7a4 --- /dev/null +++ b/mini-frontend/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA: cualquier ruta que no sea un archivo real cae a index.html para + # que React Router la resuelva del lado del cliente. + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/mini-frontend/package-lock.json b/mini-frontend/package-lock.json new file mode 100644 index 0000000..791907a --- /dev/null +++ b/mini-frontend/package-lock.json @@ -0,0 +1,3274 @@ +{ + "name": "sap-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sap-frontend", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.3.1", + "date-fns": "^4.4.0", + "lucide-react": "^1.24.0", + "react": "^19.2.6", + "react-datepicker": "^9.1.0", + "react-dom": "^19.2.6", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-datepicker": "^6.2.0", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "postcss": "^8.5.15", + "tailwindcss": "^4.3.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, + "node_modules/@types/react-datepicker/node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@types/react-datepicker/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "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/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/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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "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==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/lucide-react": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", + "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/mini-frontend/package.json b/mini-frontend/package.json new file mode 100644 index 0000000..174d863 --- /dev/null +++ b/mini-frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "sap-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.1", + "date-fns": "^4.4.0", + "lucide-react": "^1.24.0", + "react": "^19.2.6", + "react-datepicker": "^9.1.0", + "react-dom": "^19.2.6", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-datepicker": "^6.2.0", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "postcss": "^8.5.15", + "tailwindcss": "^4.3.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/mini-frontend/public/favicon.svg b/mini-frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/mini-frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mini-frontend/public/icons.svg b/mini-frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/mini-frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mini-frontend/public/imagen1.png b/mini-frontend/public/imagen1.png new file mode 100644 index 0000000..c486d47 Binary files /dev/null and b/mini-frontend/public/imagen1.png differ diff --git a/mini-frontend/public/imagen2.png b/mini-frontend/public/imagen2.png new file mode 100644 index 0000000..a4c9d7b Binary files /dev/null and b/mini-frontend/public/imagen2.png differ diff --git a/mini-frontend/public/imagen3.png b/mini-frontend/public/imagen3.png new file mode 100644 index 0000000..c454bbf Binary files /dev/null and b/mini-frontend/public/imagen3.png differ diff --git a/mini-frontend/public/login-bg.jpg b/mini-frontend/public/login-bg.jpg new file mode 100644 index 0000000..06a6597 Binary files /dev/null and b/mini-frontend/public/login-bg.jpg differ diff --git a/mini-frontend/public/login-bg.png b/mini-frontend/public/login-bg.png new file mode 100644 index 0000000..53ab1b2 Binary files /dev/null and b/mini-frontend/public/login-bg.png differ diff --git a/mini-frontend/src/App.tsx b/mini-frontend/src/App.tsx new file mode 100644 index 0000000..1d4ab74 --- /dev/null +++ b/mini-frontend/src/App.tsx @@ -0,0 +1,103 @@ +import type { ReactNode } from "react"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import SapLogin from "./components/SapLogin"; +import MainLayout from "./layout/MainLayout"; +import { getToken } from "./utils/api"; +import CustomersList from "./views/socios/CustomersList"; +import SuppliersList from "./views/socios/SuppliersList"; +import Supplier from "./views/socios/Suppliers"; +import Customer from "./views/socios/Customers"; +import InvoicesList from "./views/ventas/InvoicesList"; +import Invoices from "./views/ventas/Invoices"; +import OrdersList from "./views/ventas/OrdersList"; +import Orders from "./views/ventas/Orders"; +import PurchaseOrdersList from "./views/compras/PurchaseOrdersList"; +import PurchaseOrders from "./views/compras/PurchaseOrders"; +import PurchaseInvoicesList from "./views/compras/PurchaseInvoicesList"; +import PurchaseInvoices from "./views/compras/PurchaseInvoices"; +import ItemsList from "./views/inventario/ItemsList"; +import Items from "./views/inventario/Items"; +import BatchNumberDetailsList from "./views/inventario/BatchNumberDetailsList"; +import BatchNumberDetails from "./views/inventario/BatchNumberDetails"; +import Dashboard from "./views/Dashboard"; +import ModuleReportsList from "./views/informes/ModuleReportsList"; +import ReportRunner from "./components/report/ReportRunner"; +import CrystalReportsList from "./views/reportes/CrystalReportsList"; +import CrystalReportRunner from "./components/report/CrystalReportRunner"; +import HtmlReportsList from "./views/reportes/HtmlReportsList"; +import HtmlReportRunner from "./components/report/HtmlReportRunner"; + +const Delivery = () => ( +
+

Entregas / Guías

+
+); + +const Receiving = () => ( +
+

Recepciones de Compra

+
+); + +function ProtectedRoute({ children }: { children: ReactNode }) { + if (!getToken()) { + return ; + } + return <>{children}; +} + +export default function App() { + return ( + + + } /> + + + + + } + > + } + /> + } /> + {/* 1. Inventario */} + } /> + } /> + } /> + } /> + {/* 2. Socios de Negocios */} + } /> + } /> + } /> + } /> + {/* 3. Ventas */} + } /> + } /> + } /> + } /> + } /> + {/* 4. Compras */} + } /> + } /> + } /> + } /> + } /> + {/* 5. Informes */} + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + + ); +} diff --git a/mini-frontend/src/assets/hero.png b/mini-frontend/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/mini-frontend/src/assets/hero.png differ diff --git a/mini-frontend/src/assets/vite.svg b/mini-frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/mini-frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/mini-frontend/src/components/BatchForm.tsx b/mini-frontend/src/components/BatchForm.tsx new file mode 100644 index 0000000..2bafa63 --- /dev/null +++ b/mini-frontend/src/components/BatchForm.tsx @@ -0,0 +1,308 @@ +import { useState, useEffect } from "react"; +import type { ChangeEvent, FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import type { BatchNumberDetail } from "../types/sapBatch"; +import { API_URL, apiFetch } from "../utils/api"; +import { + BATCH_STATUS_APROBACION, + BATCH_STATUS_LIBERACION, +} from "../utils/enums"; +import FormattedField from "./ui/FormattedField"; +import SelectField from "./ui/SelectField"; + +interface BatchFormProps { + batchId: string; + backPath: string; +} + +export default function BatchForm({ batchId, backPath }: BatchFormProps) { + const navigate = useNavigate(); + + const [batch, setBatch] = useState({ Batch: "" }); + const [loading, setLoading] = useState(false); + const [fetchLoading, setFetchLoading] = useState(true); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + const SAP_TO_FRIENDLY: Record = { + U_SIS_ESTLOT: "EstadoAprobacion", + U_SIS_FECAPROB: "FechaAprobacion", + U_SIS_RESPAPROB: "ResponsableAprobacion", + U_SIS_FECLIB: "FechaLiberacion", + U_SIS_OBSER: "Observaciones", + U_SIS_ESTLIB: "EstadoLiberacion", + U_SIS_HORAPROB: "HoraAprobacion", + }; + + function mapSapToFriendly(raw: Record): BatchNumberDetail { + const mapped: Record = {}; + for (const key of Object.keys(raw)) { + const friendly = SAP_TO_FRIENDLY[key] ?? key; + mapped[friendly] = raw[key]; + } + return mapped as BatchNumberDetail; + } + + useEffect(() => { + const fetchBatch = async () => { + setFetchLoading(true); + try { + const response = await apiFetch( + `${API_URL}/api/BatchNumberDetails/${batchId}`, + ); + if (!response.ok) throw new Error("Error al cargar el lote"); + const data = await response.json(); + setBatch(mapSapToFriendly(data)); + } catch (err: any) { + setError(err.message || "Error al cargar el lote"); + } finally { + setFetchLoading(false); + } + }; + fetchBatch(); + }, [batchId]); + + const handleChange = ( + e: ChangeEvent, + ) => { + const { name, value } = e.target; + setBatch((prev) => ({ ...prev, [name]: value })); + }; + + const handleSelectChange = (name: string) => (value: string) => { + setBatch((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + setSuccess(""); + + try { + const response = await apiFetch( + `${API_URL}/api/BatchNumberDetails/${batchId}`, + { + method: "PATCH", + body: JSON.stringify({ + BatchAttribute1: batch.BatchAttribute1, + BatchAttribute2: batch.BatchAttribute2, + Status: batch.Status, + Details: batch.Details, + ExpirationDate: batch.ExpirationDate, + U_SIS_ESTLOT: batch.EstadoAprobacion, + U_SIS_FECAPROB: batch.FechaAprobacion, + U_SIS_RESPAPROB: batch.ResponsableAprobacion, + U_SIS_FECLIB: batch.FechaLiberacion, + U_SIS_OBSER: batch.Observaciones, + U_SIS_ESTLIB: batch.EstadoLiberacion, + U_SIS_HORAPROB: batch.HoraAprobacion, + }), + }, + ); + + if (!response.ok) throw new Error("Error al actualizar el lote"); + + setSuccess("Lote actualizado exitosamente"); + } catch (err: any) { + setError(err.message || "Error de conexión"); + } finally { + setLoading(false); + } + }; + + if (fetchLoading) { + return ( +
+
+ Cargando lote... +
+
+ ); + } + + return ( +
+
+
+

+ Detalle de Lote +

+

+ Mantenimiento de lotes - SAP Business One +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + {success && ( +
+ {success} +
+ )} + +
+
+ + + + + + + + + + + + + + + + + +
+ +
+ +