diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bbfc411 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_SUPABASE_URL="https://dbit.digitalcompass.agency" +VITE_SUPABASE_ANON_KEY="TU_ANON_PUBLIC_KEY" +VITE_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-proyecto" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..026d544 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +.output +.vinxi +.tanstack/** +.nitro +*.local + +# Environment variables — never commit secrets +.env +.env.local +.env.development +.env.production + +# Wrangler / Cloudflare +.wrangler/ +.dev.vars + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..be49b26 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +dist +.output +.vinxi +pnpm-lock.yaml +package-lock.json +bun.lock +routeTree.gen.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..90abee2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bffac08 --- /dev/null +++ b/README.md @@ -0,0 +1,635 @@ +# Tablero CDC — Project Management + +> Aplicación interna de GomezLee Marketing para centralizar la gestión de proyectos creativos del CDC, sus aprobaciones, enlaces, estimación interna y tarifarios en una sola interfaz web. + +--- + +## INFORMACIÓN GENERAL + +| Campo | Detalle | +|---|---| +| **Proyecto** | Tablero CDC / CDC Project Management | +| **Área** | Creatividad y Diseño — CDC | +| **Developer Principal** | Isaac Aracena | +| **IT Manager** | Luis Matos | + +--- + +## OBJETIVO + +### Problema que resuelve + +La gestión de proyectos del CDC requiere centralizar información que de otro modo queda distribuida entre hojas de cálculo, enlaces, comunicaciones y seguimientos manuales. Esto dificulta consultar rápidamente el estado de un proyecto, sus responsables, propuestas, artes finales, costos internos y actividad reciente. + +### Solución implementada + +El Tablero CDC ofrece una aplicación web donde los usuarios autorizados pueden: + +- crear, consultar, editar y cerrar proyectos; +- filtrar y buscar proyectos; +- manejar links de brief, propuestas y artes finales; +- visualizar actividad e información clave de cada proyecto; +- calcular y guardar el costo interno mediante tarifarios; +- usar tarifarios generales o tarifarios especiales por cliente; +- administrar el catálogo de tarifarios cuando el usuario tiene permiso; +- consultar el total tarifado global o filtrado según permisos; +- trabajar con datos persistidos en Supabase y sincronizados entre usuarios. + +### Usuarios / Beneficiarios + +- Equipo de Creatividad y Diseño / CDC. +- Director Creativo. +- Usuarios internos autorizados de GomezLee Marketing. +- IT, para soporte, mantenimiento y administración técnica. +- Áreas que consumen la información consolidada posteriormente mediante Google Sheets / Power BI. + +--- + +## ARQUITECTURA + +### Diagrama de flujo principal + +```text +Usuario autorizado + | + v +React + TypeScript + Vite + | + +--> Supabase Auth (Google OAuth) + | + +--> tablero_cdc_allowed_users + | | + | +--> permisos funcionales + | + +--> Supabase Postgres + | | + | +--> Proyectos + | +--> Links + | +--> Actividad + | +--> Costos internos + | +--> Listas dinámicas + | +--> Tarifarios + | +--> RPCs de paginación y totales + | + +--> Supabase Realtime + | + +--> n8n Webhook + | + v + Google Sheets + | + v + Power BI +``` + +### Flujo del tarifario + +```text +Google Sheet del tarifario + | + v + n8n + | + v +Supabase tariff catalog + | + +--> Secciones generales + | + +--> Tarifarios por cliente + | + v +Estimador interno del proyecto + | + v +tablero_cdc_project_pricing_items +``` + +Las tarifas administradas directamente desde la aplicación se identifican con `managed_by = 'app'` para protegerlas frente al sincronizador del Sheet. + +### Stack tecnológico + +| Componente | Tecnología | Propósito | +|---|---|---| +| Frontend | React 19 + TypeScript | Interfaz y lógica de la aplicación | +| Build / Dev Server | Vite 6 | Desarrollo y compilación | +| UI | Tailwind CSS 4 + Radix UI | Diseño y componentes | +| Base de datos | Supabase / PostgreSQL | Persistencia, RLS, RPCs y configuración | +| Autenticación | Supabase Auth + Google OAuth | Inicio de sesión corporativo | +| Tiempo real | Supabase Realtime | Sincronización multiusuario | +| Automatización | n8n | Integración con Google Sheets | +| Fuente / salida operativa | Google Sheets | Sincronización con procesos existentes | +| Reporting | Power BI | Consumo posterior de la información | +| Repositorio | Gitea | Control de versiones | + +### Integraciones externas + +| Sistema | Tipo de integración | Datos que fluyen | +|---|---|---| +| Supabase | SDK / REST / RPC / Realtime | Usuarios, proyectos, permisos, listas, links, actividad, tarifas y costos | +| Google OAuth | OAuth 2.0 vía Supabase | Identidad del usuario | +| n8n | Webhook HTTPS | Sincronización de proyectos y catálogo | +| Google Sheets | n8n | Datos operativos y catálogo de tarifarios | +| Power BI | Fuente existente basada en Sheets | Reporting / visualización | + +--- + +## REGLAS DE NEGOCIO + +1. **El acceso no depende solamente del dominio del correo.** El usuario debe autenticarse con Google y existir activo en `public.tablero_cdc_allowed_users`. + +2. **Los permisos especiales se controlan desde Supabase**, principalmente mediante: + - `can_delete_projects` + - `can_manage_internal_pricing` + - `can_control_pricing_summary` + - `can_manage_tariff_catalog` + +3. **No se deben hardcodear administradores nuevos en el frontend.** Los accesos y permisos se administran en Supabase. + +4. **Los tarifarios generales** están disponibles para los clientes que no dependen de un tarifario especial. + +5. **Los tarifarios por cliente** solo aparecen al seleccionar el cliente asociado a esa sección. + +6. **Walmart Connect WMC** utiliza un tarifario especial con 12 piezas de precio fijo y mantiene la posibilidad de agregar un costo manual para propuestas o conceptos no contemplados. + +7. **Las tarifas administradas desde la app** quedan marcadas con `managed_by = 'app'`. El sincronizador del Sheet no debe sobrescribirlas. + +8. **Las tarifas históricas no deben eliminarse como operación normal.** Se deben desactivar para conservar integridad histórica. + +9. **Los costos seleccionados en un proyecto** se guardan en `tablero_cdc_project_pricing_items` y alimentan el total interno del proyecto. + +10. **El Total Tarifado Global** se calcula mediante la RPC `tablero_cdc_get_pricing_summary` y respeta los filtros activos. Los proyectos creados con tarifarios actuales o futuros siguen formando parte del total al guardar su costo interno. + +11. **La visibilidad del Total Tarifado** se controla mediante configuración y permisos de Supabase. + +12. **La estructura del Google Sheet original debe mantenerse estable**, especialmente cuando es consumida por Power BI. Cualquier cambio estructural debe validarse previamente. + +13. **Paginación actual:** + - proyectos: `24` por página; + - secciones del módulo de tarifario: `6` por página; + - tarifas / piezas del módulo: `10` por página. + +14. Al cambiar de sección, buscar, cambiar entre **Generales / Por cliente** o activar **Mostrar inactivas**, el módulo de tarifario reajusta la página automáticamente. + +--- + +## CONFIGURACIÓN Y SETUP + +### Prerrequisitos + +- Git. +- Node.js + npm. +- Acceso al repositorio interno en Gitea. +- Proyecto Supabase configurado. +- Google OAuth configurado en Supabase Auth. +- Acceso a SQL Editor de Supabase para instalaciones o migraciones. +- Acceso al workflow n8n correspondiente si se requiere sincronización con Google Sheets. + +### Variables de entorno + +Crear `.env` a partir de `.env.example`. + +| Variable | Descripción | Dónde se obtiene | +|---|---|---| +| `VITE_SUPABASE_URL` | URL pública del proyecto Supabase | Supabase / infraestructura GLM | +| `VITE_SUPABASE_ANON_KEY` | Anon/Public Key usada por el frontend | Supabase | +| `VITE_WEBHOOK_URL` | Webhook de sincronización de proyectos | n8n | + +Ejemplo: + +```env +VITE_SUPABASE_URL="https://dbit.digitalcompass.agency" +VITE_SUPABASE_ANON_KEY="TU_ANON_PUBLIC_KEY" +VITE_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-proyecto" +``` + +> **Nunca commitear `.env`, `service_role`, secretos OAuth, contraseñas, tokens privados ni credenciales administrativas.** + +La `anon key` del frontend no debe sustituirse por una `service_role`. + +### Esquema de base de datos / SQL incluidos + +El repositorio mantiene scripts SQL separados para las distintas capacidades: + +```text +supabase_tablero_cdc_access_control.sql +supabase_tablero_cdc_alicia_permissions.sql +supabase_project_activity.sql +supabase_project_pricing_items.sql +supabase_realtime_projects.sql +supabase_rpc_projects_paginated.sql +supabase_pricing_summary_visibility.sql +supabase_tariff_catalog_and_safe_links.sql +supabase_walmart_connect_tariff.sql +supabase_tariff_admin_module.sql +``` + +#### Script principal del módulo de tarifario + +Para una instalación que ya tenga la base anterior del Tablero CDC, el archivo: + +```text +supabase_tariff_admin_module.sql +``` + +incorpora de forma idempotente el tarifario Walmart Connect y el módulo de administración del catálogo, incluyendo `can_manage_tariff_catalog`, las secciones dinámicas y sus políticas RLS. + +Después de ejecutarlo, verificar que los usuarios administradores esperados tengan: + +```text +can_manage_tariff_catalog = true +``` + +### Instalación local + +```bash +git clone https://git.digitalcompass.agency/Isaac_Aracena/cdc-project-management.git +cd cdc-project-management +npm install +cp .env.example .env +``` + +Editar `.env` con los valores correctos. + +Ejecutar en desarrollo: + +```bash +npm run dev +``` + +La configuración actual de Vite usa: + +```text +http://localhost:3000/tablero-cdc/ +``` + +### Build + +```bash +npm run build +``` + +El script ejecuta: + +```text +tsc && vite build +``` + +Por lo tanto, un error de TypeScript detiene el build y debe corregirse antes de publicar. + +### Preview + +```bash +npm run preview +``` + +--- + +## INSTALACIÓN / DEPLOY + +La aplicación está compilada para funcionar debajo de: + +```text +/tablero-cdc/ +``` + +Configuración en `vite.config.ts`: + +```ts +base: "/tablero-cdc/"; +``` + +### Deploy manual mediante `dist` + +Este repositorio **sí mantiene el `dist` validado** porque el flujo actual de despliegue utiliza esa carpeta directamente en el servidor. + +El `dist` debe contener como mínimo: + +```text +dist/ +├── assets/ +├── index.html +├── favicon.ico +└── demás archivos públicos generados +``` + +> `dist/assets/` es obligatorio. Sin esa carpeta, `index.html` no podrá cargar correctamente el JavaScript y CSS compilados. + +Flujo recomendado: + +1. Probar la aplicación con `npm run dev`. +2. Generar el build con `npm run build`. +3. Probar **ese mismo `dist`** en XAMPP bajo `/tablero-cdc/`. +4. No regenerar el build después de la validación si se desea desplegar exactamente la versión probada. +5. Subir a Gitea el mismo `dist`, incluyendo `dist/assets/`. +6. Copiar ese `dist` validado al servidor. +7. Probar login, tablero, tarifario y una apertura directa sin recargar la página. + +--- + +## CÓMO FUNCIONA + +### Flujo paso a paso + +1. El usuario entra a la aplicación. +2. Supabase Auth inicia o recupera la sesión Google. +3. La app consulta `tablero_cdc_allowed_users`. +4. Si el usuario no está activo/autorizado, la sesión se rechaza. +5. Si está autorizado, la app carga sus permisos. +6. El tablero consulta los proyectos desde Supabase mediante RPC paginada. +7. Los filtros se aplican desde la consulta y no requieren descargar toda la base. +8. Realtime mantiene sincronizadas las ventanas activas. +9. Al crear o editar un proyecto, la información se persiste en Supabase. +10. Si `VITE_WEBHOOK_URL` está configurado, la aplicación dispara la sincronización correspondiente hacia n8n. +11. Los costos internos se guardan como líneas de pricing por proyecto. +12. El total tarifado se calcula en Supabase mediante RPC. +13. El tarifario administrativo solo aparece a usuarios con `can_manage_tariff_catalog = true`. + +### Tarifario general + +El estimador puede utilizar secciones generales como: + +- **Tarifario Gráfico CDC** +- **Estrategia y Creatividad** + +Cada tarifa puede incluir categoría, servicio, tipo de trabajo, nivel, rango de referencia, notas y orden. + +### Tarifario por cliente + +Las secciones con `scope = 'client'` se muestran únicamente cuando el proyecto tiene seleccionado el cliente asociado. + +Ejemplo actual: + +```text +Walmart Connect WMC +``` + +El usuario puede seleccionar varias piezas y el subtotal se calcula automáticamente. + +### Administración del tarifario + +Los usuarios autorizados pueden: + +- crear secciones generales; +- crear tarifarios por cliente; +- crear tarifas / piezas; +- editar secciones y tarifas; +- desactivar y reactivar; +- buscar; +- mostrar inactivas; +- paginar listas extensas. + +La administración no debe borrar costos ya guardados en proyectos históricos. + +### Schedules / Triggers + +| Trigger | Frecuencia | Descripción | +|---|---|---| +| Interacción del usuario | On demand | Crear, editar, filtrar o cotizar proyectos | +| Webhook de proyecto | On demand | La app envía cambios a n8n cuando `VITE_WEBHOOK_URL` está configurado | +| Realtime Supabase | Evento | Actualiza la app cuando cambian tablas suscritas | +| Sync de tarifario Sheet → Supabase | Según workflow n8n | Mantiene actualizado el catálogo administrado desde Sheet | + +> La frecuencia exacta del sincronizador n8n debe consultarse en el workflow activo; no está definida por el frontend. + +--- + +## TESTING + +### Casos de prueba mínimos + +| Caso | Input / Acción | Output esperado | Estado | +|---|---|---|---| +| Login autorizado | Usuario activo en `tablero_cdc_allowed_users` | Entra al tablero | Revalidar tras deploy | +| Login no autorizado | Usuario sin fila activa | Acceso rechazado | Revalidar tras cambios de acceso | +| Carga del tablero | Abrir Todos / Activos / Cerrados | Proyectos paginados correctamente | Revalidar tras deploy | +| Filtros | País, marca, cliente o CM | Resultado y total corresponden al filtro | Revalidar tras cambios SQL | +| Realtime | Dos ventanas abiertas | Cambios visibles sin recarga manual | Revalidar tras cambios de Realtime | +| Links | Editar propuestas / artes finales | Persisten al reabrir el proyecto | Revalidar tras cambios en store/RPC | +| Tarifario general | Cliente normal | Secciones generales disponibles | Revalidar tras cambios de catálogo | +| Walmart Connect | Cliente `Walmart Connect WMC` | Solo tarifario especial correspondiente + costo manual | Validado funcionalmente | +| WMC multiselección | Elegir varias piezas | Subtotal correcto | Validado funcionalmente | +| Administrar tarifario | Usuario con `can_manage_tariff_catalog = true` | Botón Tarifario visible y módulo accesible | Validado funcionalmente | +| Usuario sin permiso | `can_manage_tariff_catalog = false` | No ve módulo administrativo | Revalidar al modificar permisos | +| Paginación secciones | Más de 6 secciones | Controles de página sin perder selección | Implementado | +| Paginación tarifas | Más de 10 tarifas | Controles de página correctos | Implementado | +| Mostrar inactivas | Activar switch | Se muestran registros desactivados cuando existan | Implementado | +| Total tarifado | Crear/editar costos | Total global/filtrado se actualiza | Revalidar tras cambios de pricing | +| Build | `npm run build` | `tsc && vite build` sin errores | Requerido antes de generar nuevo dist | +| XAMPP | Abrir `dist` en `/tablero-cdc/` | App y módulo Tarifario abren sin recarga | Validado en la versión actual | + +### Prueba de referencia Walmart Connect + +La documentación técnica incluida define este caso: + +- `Uniformes` +- `Photobooth` +- `Arco de entrada` + +Subtotal esperado: + +```text +$480.00 +``` + +Agregando manualmente: + +```text +Propuesta general Walmart Connect = $900.00 +``` + +Total esperado: + +```text +$1,380.00 +``` + +Al guardar, cerrar y volver a abrir el proyecto, las líneas y el total deben persistir. + +--- + +## ERRORES CONOCIDOS Y TROUBLESHOOTING + +| Error / Síntoma | Causa probable | Solución | +|---|---|---| +| Pantalla en blanco al publicar | Falta `dist/assets` o las rutas del build no coinciden | Confirmar `dist/assets/` y `base: "/tablero-cdc/"` | +| Tarifario abre como modal blanco hasta recargar | Build antiguo / híbrido o assets desactualizados | Generar un build limpio desde el código fuente actual y desplegar exactamente el `dist` probado | +| `npm run build` falla en TypeScript | Error de tipos antes de ejecutar Vite | Corregir el error de `tsc`; no publicar un dist nuevo hasta que el build termine correctamente | +| Botón **Tarifario** no aparece | Falta permiso o SQL del módulo | Verificar `can_manage_tariff_catalog = true` y recargar sesión | +| Usuario válido no puede entrar | No existe como activo en `tablero_cdc_allowed_users` | Revisar fila, correo normalizado e `is_active` | +| Error / ausencia de RPC paginada | SQL no aplicado o schema cache desactualizado | Ejecutar `supabase_rpc_projects_paginated.sql` y revisar Supabase | +| Total tarifado no responde como esperado | RPC/configuración de visibilidad no aplicada | Revisar `supabase_pricing_summary_visibility.sql` y `tablero_cdc_get_pricing_summary` | +| Tarifario no carga desde Supabase | Tabla / SQL no aplicado | Revisar `tablero_cdc_tariff_catalog` y `tablero_cdc_tariff_sections` | +| Sync a Sheet no ocurre | `VITE_WEBHOOK_URL` vacío o workflow n8n inactivo | Revisar `.env`, webhook y ejecución de n8n | +| Cambios no aparecen en otra ventana | Realtime no habilitado | Ejecutar / revisar `supabase_realtime_projects.sql` | +| OAuth vuelve a una ruta incorrecta | Redirect URL no autorizada o base incorrecta | Revisar configuración OAuth y `/tablero-cdc/` | + +--- + +## MONITOREO + +- **Supabase:** revisar errores de Auth, RLS, RPC y consultas. +- **n8n Executions:** revisar ejecuciones fallidas del webhook y sincronizadores. +- **Browser DevTools:** revisar `Console` y `Network` ante errores de frontend o `404`. +- **Gitea:** confirmar que el commit de despliegue contiene `dist/index.html` y `dist/assets/`. +- **Prueba funcional:** abrir la app en una sesión limpia después de cada despliegue. + +### Output esperado en operación normal + +- usuarios autorizados ingresan con Google; +- usuarios no autorizados quedan fuera; +- proyectos cargan paginados; +- cambios persisten en Supabase; +- Realtime mantiene sincronización multiusuario; +- tarifarios muestran únicamente las secciones aplicables; +- costos guardados alimentan el total interno y el resumen global; +- el módulo administrativo solo aparece a quienes tienen permiso; +- el build publicado funciona directamente sin requerir recargar la página. + +--- + +## ESTRUCTURA DEL REPOSITORIO + +```text +cdc-project-management/ +├── dist/ # Build probado para despliegue +│ ├── assets/ # JS/CSS compilado — obligatorio +│ └── index.html +├── public/ # Favicons y assets públicos +├── src/ +│ ├── components/ +│ │ ├── board/ # Tarjetas, diálogo y pricing +│ │ ├── tariff/ # Administración del tarifario +│ │ └── ui/ # Componentes de interfaz +│ ├── context/ +│ │ └── AuthContext.tsx # Sesión y permisos +│ ├── data/ # Datos de respaldo +│ ├── hooks/ +│ ├── lib/ +│ │ ├── accessControl.ts +│ │ ├── appLists.ts +│ │ ├── pricingSummaryVisibility.ts +│ │ ├── store.ts +│ │ ├── supabase.ts +│ │ ├── tariffCatalog.ts +│ │ └── tariffSections.ts +│ ├── pages/ +│ │ └── BoardPage.tsx +│ ├── App.tsx +│ ├── main.tsx +│ └── styles.css +├── .env.example +├── package.json +├── package-lock.json +├── vite.config.ts +├── tsconfig.json +├── supabase_*.sql # Migraciones / configuración +├── MODULO_TARIFARIO_ADMIN_IMPLEMENTACION.md +├── PAGINACION_MODULO_TARIFARIO.md +├── WMC_TARIFARIO_IMPLEMENTACION.md +├── VALIDACION_MODULO_TARIFARIO.md +├── CORRECCION_BUILD_Y_MODAL_TARIFARIO.md +└── README.md +``` + +--- + +## SEGURIDAD + +### No commitear + +```text +.env +.env.local +.env.production +service_role keys +tokens privados +secretos OAuth +contraseñas +node_modules/ +logs con información sensible +``` + +### Sí se mantiene en este repositorio + +```text +.env.example +dist/ +dist/assets/ +scripts SQL versionados +documentación técnica +``` + +La inclusión de `dist/` es intencional mientras el procedimiento de producción dependa de desplegar exactamente el build probado en XAMPP. + +--- + +## CHANGELOG + +### 2026-08-07 — Documentación + +- README actualizado al estándar GLM IT. +- Se documenta arquitectura, permisos, setup, deploy, testing y troubleshooting. +- Se deja explícito que `dist/assets/` forma parte obligatoria del build desplegable. + +### 2026-07-28 — Módulo de tarifario administrativo + +- Incorporación del tarifario especial Walmart Connect. +- Administración de secciones generales y por cliente. +- Permiso `can_manage_tariff_catalog`. +- Creación, edición, desactivación y reactivación de tarifas. +- Compatibilidad con tarifas gestionadas por Sheet y por app. +- Corrección del build / renderizado del modal del tarifario. +- Paginación de secciones y tarifas: + - 6 secciones por página. + - 10 tarifas por página. +- `dist` validado mediante XAMPP bajo `/tablero-cdc/`. + +--- + +## DECISIONS LOG + +### DEC-001 — Acceso administrado desde Supabase + +- **Contexto:** evitar listas rígidas de usuarios dentro del frontend. +- **Decisión:** utilizar `tablero_cdc_allowed_users`. +- **Razón:** permite habilitar, deshabilitar y asignar permisos sin recompilar la aplicación. + +### DEC-002 — Permisos granulares + +- **Contexto:** no todos los usuarios deben administrar costos, eliminar proyectos, controlar el resumen o editar tarifarios. +- **Decisión:** usar flags independientes en Supabase. +- **Razón:** mantener privilegio mínimo y separar responsabilidades. + +### DEC-003 — Tarifario híbrido Sheet + App + +- **Contexto:** las tarifas generales existentes se mantienen desde el proceso operativo, mientras nuevos tarifarios especiales pueden administrarse desde la app. +- **Decisión:** utilizar `managed_by` para distinguir origen y proteger registros administrados en la aplicación. +- **Razón:** evitar que el sincronizador del Sheet sobrescriba cambios creados desde el módulo administrativo. + +### DEC-004 — Desactivar en lugar de eliminar tarifas históricas + +- **Contexto:** una tarifa puede estar referenciada por proyectos anteriores. +- **Decisión:** usar `is_active = false` como operación habitual. +- **Razón:** conservar trazabilidad e integridad histórica. + +### DEC-005 — Base de Vite fija en `/tablero-cdc/` + +- **Contexto:** la aplicación se publica dentro de una subcarpeta. +- **Decisión:** configurar `base: "/tablero-cdc/"`. +- **Razón:** generar rutas correctas para JS, CSS y assets en producción. + +### DEC-006 — Publicar exactamente el `dist` validado + +- **Contexto:** el build de producción debe comportarse igual que la versión probada antes del despliegue. +- **Decisión:** validar el `dist` en XAMPP y subir ese mismo build al repositorio / servidor. +- **Razón:** evitar diferencias entre el artefacto probado y el artefacto publicado. + +--- + +## CONTACTOS DEL PROYECTO + +| Rol | Nombre | Contacto | +|---|---|---| +| IT Manager | Luis Matos | `lmatos@gomezleemarketing.com` | +| Developer Principal | Isaac Aracena | `iaracena@gomezleemarketing.com` | + diff --git a/components.json b/components.json new file mode 100644 index 0000000..f0817a8 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "css": "src/styles.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/dist/assets/index-COXhoke_.js b/dist/assets/index-COXhoke_.js new file mode 100644 index 0000000..aa12d95 --- /dev/null +++ b/dist/assets/index-COXhoke_.js @@ -0,0 +1,433 @@ +function FR(e,t){for(var r=0;rs[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const l of i)if(l.type==="childList")for(const u of l.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&s(u)}).observe(document,{childList:!0,subtree:!0});function r(i){const l={};return i.integrity&&(l.integrity=i.integrity),i.referrerPolicy&&(l.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?l.credentials="include":i.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function s(i){if(i.ep)return;i.ep=!0;const l=r(i);fetch(i.href,l)}})();function y_(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Cm={exports:{}},il={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var dx;function KR(){if(dx)return il;dx=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(s,i,l){var u=null;if(l!==void 0&&(u=""+l),i.key!==void 0&&(u=""+i.key),"key"in i){l={};for(var d in i)d!=="key"&&(l[d]=i[d])}else l=i;return i=l.ref,{$$typeof:e,type:s,key:u,ref:i!==void 0?i:null,props:l}}return il.Fragment=t,il.jsx=r,il.jsxs=r,il}var fx;function YR(){return fx||(fx=1,Cm.exports=KR()),Cm.exports}var m=YR(),Tm={exports:{}},Ue={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var hx;function WR(){if(hx)return Ue;hx=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),u=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),b=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),x=Symbol.iterator;function _(k){return k===null||typeof k!="object"?null:(k=x&&k[x]||k["@@iterator"],typeof k=="function"?k:null)}var E={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,T={};function C(k,O,U){this.props=k,this.context=O,this.refs=T,this.updater=U||E}C.prototype.isReactComponent={},C.prototype.setState=function(k,O){if(typeof k!="object"&&typeof k!="function"&&k!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,k,O,"setState")},C.prototype.forceUpdate=function(k){this.updater.enqueueForceUpdate(this,k,"forceUpdate")};function R(){}R.prototype=C.prototype;function A(k,O,U){this.props=k,this.context=O,this.refs=T,this.updater=U||E}var D=A.prototype=new R;D.constructor=A,S(D,C.prototype),D.isPureReactComponent=!0;var B=Array.isArray;function I(){}var L={H:null,A:null,T:null,S:null},H=Object.prototype.hasOwnProperty;function Z(k,O,U){var Y=U.ref;return{$$typeof:e,type:k,key:O,ref:Y!==void 0?Y:null,props:U}}function ue(k,O){return Z(k.type,O,k.props)}function ce(k){return typeof k=="object"&&k!==null&&k.$$typeof===e}function he(k){var O={"=":"=0",":":"=2"};return"$"+k.replace(/[=:]/g,function(U){return O[U]})}var de=/\/+/g;function me(k,O){return typeof k=="object"&&k!==null&&k.key!=null?he(""+k.key):O.toString(36)}function pe(k){switch(k.status){case"fulfilled":return k.value;case"rejected":throw k.reason;default:switch(typeof k.status=="string"?k.then(I,I):(k.status="pending",k.then(function(O){k.status==="pending"&&(k.status="fulfilled",k.value=O)},function(O){k.status==="pending"&&(k.status="rejected",k.reason=O)})),k.status){case"fulfilled":return k.value;case"rejected":throw k.reason}}throw k}function j(k,O,U,Y,$){var re=typeof k;(re==="undefined"||re==="boolean")&&(k=null);var te=!1;if(k===null)te=!0;else switch(re){case"bigint":case"string":case"number":te=!0;break;case"object":switch(k.$$typeof){case e:case t:te=!0;break;case b:return te=k._init,j(te(k._payload),O,U,Y,$)}}if(te)return $=$(k),te=Y===""?"."+me(k,0):Y,B($)?(U="",te!=null&&(U=te.replace(de,"$&/")+"/"),j($,O,U,"",function(Se){return Se})):$!=null&&(ce($)&&($=ue($,U+($.key==null||k&&k.key===$.key?"":(""+$.key).replace(de,"$&/")+"/")+te)),O.push($)),1;te=0;var ee=Y===""?".":Y+":";if(B(k))for(var se=0;se>>1,G=j[J];if(0>>1;Ji(U,W))Yi($,U)?(j[J]=$,j[Y]=W,J=Y):(j[J]=U,j[O]=W,J=O);else if(Yi($,W))j[J]=$,j[Y]=W,J=Y;else break e}}return K}function i(j,K){var W=j.sortIndex-K.sortIndex;return W!==0?W:j.id-K.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;e.unstable_now=function(){return l.now()}}else{var u=Date,d=u.now();e.unstable_now=function(){return u.now()-d}}var f=[],h=[],b=1,g=null,x=3,_=!1,E=!1,S=!1,T=!1,C=typeof setTimeout=="function"?setTimeout:null,R=typeof clearTimeout=="function"?clearTimeout:null,A=typeof setImmediate<"u"?setImmediate:null;function D(j){for(var K=r(h);K!==null;){if(K.callback===null)s(h);else if(K.startTime<=j)s(h),K.sortIndex=K.expirationTime,t(f,K);else break;K=r(h)}}function B(j){if(S=!1,D(j),!E)if(r(f)!==null)E=!0,I||(I=!0,he());else{var K=r(h);K!==null&&pe(B,K.startTime-j)}}var I=!1,L=-1,H=5,Z=-1;function ue(){return T?!0:!(e.unstable_now()-Zj&&ue());){var J=g.callback;if(typeof J=="function"){g.callback=null,x=g.priorityLevel;var G=J(g.expirationTime<=j);if(j=e.unstable_now(),typeof G=="function"){g.callback=G,D(j),K=!0;break t}g===r(f)&&s(f),D(j)}else s(f);g=r(f)}if(g!==null)K=!0;else{var k=r(h);k!==null&&pe(B,k.startTime-j),K=!1}}break e}finally{g=null,x=W,_=!1}K=void 0}}finally{K?he():I=!1}}}var he;if(typeof A=="function")he=function(){A(ce)};else if(typeof MessageChannel<"u"){var de=new MessageChannel,me=de.port2;de.port1.onmessage=ce,he=function(){me.postMessage(null)}}else he=function(){C(ce,0)};function pe(j,K){L=C(function(){j(e.unstable_now())},K)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(j){j.callback=null},e.unstable_forceFrameRate=function(j){0>j||125J?(j.sortIndex=W,t(h,j),r(f)===null&&j===r(h)&&(S?(R(L),L=-1):S=!0,pe(B,W-J))):(j.sortIndex=G,t(f,j),E||_||(E=!0,I||(I=!0,he()))),j},e.unstable_shouldYield=ue,e.unstable_wrapCallback=function(j){var K=x;return function(){var W=x;x=K;try{return j.apply(this,arguments)}finally{x=W}}}})(Rm)),Rm}var gx;function JR(){return gx||(gx=1,Am.exports=XR()),Am.exports}var jm={exports:{}},dn={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var vx;function QR(){if(vx)return dn;vx=1;var e=jd();function t(f){var h="https://react.dev/errors/"+f;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),jm.exports=QR(),jm.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var bx;function ZR(){if(bx)return ol;bx=1;var e=JR(),t=jd(),r=b_();function s(n){var a="https://react.dev/errors/"+n;if(1G||(n.current=J[G],J[G]=null,G--)}function U(n,a){G++,J[G]=n.current,n.current=a}var Y=k(null),$=k(null),re=k(null),te=k(null);function ee(n,a){switch(U(re,a),U($,n),U(Y,null),a.nodeType){case 9:case 11:n=(n=a.documentElement)&&(n=n.namespaceURI)?D0(n):0;break;default:if(n=a.tagName,a=a.namespaceURI)a=D0(a),n=P0(a,n);else switch(n){case"svg":n=1;break;case"math":n=2;break;default:n=0}}O(Y),U(Y,n)}function se(){O(Y),O($),O(re)}function Se(n){n.memoizedState!==null&&U(te,n);var a=Y.current,o=P0(a,n.type);a!==o&&(U($,n),U(Y,o))}function Te(n){$.current===n&&(O(Y),O($)),te.current===n&&(O(te),nl._currentValue=W)}var Re,nt;function Ze(n){if(Re===void 0)try{throw Error()}catch(o){var a=o.stack.trim().match(/\n( *(at )?)/);Re=a&&a[1]||"",nt=-1)":-1p||M[c]!==V[p]){var ne=` +`+M[c].replace(" at new "," at ");return n.displayName&&ne.includes("")&&(ne=ne.replace("",n.displayName)),ne}while(1<=c&&0<=p);break}}}finally{xt=!1,Error.prepareStackTrace=o}return(o=n?n.displayName||n.name:"")?Ze(o):""}function je(n,a){switch(n.tag){case 26:case 27:case 5:return Ze(n.type);case 16:return Ze("Lazy");case 13:return n.child!==a&&a!==null?Ze("Suspense Fallback"):Ze("Suspense");case 19:return Ze("SuspenseList");case 0:case 15:return oe(n.type,!1);case 11:return oe(n.type.render,!1);case 1:return oe(n.type,!0);case 31:return Ze("Activity");default:return""}}function Ve(n){try{var a="",o=null;do a+=je(n,o),o=n,n=n.return;while(n);return a}catch(c){return` +Error generating stack: `+c.message+` +`+c.stack}}var $e=Object.prototype.hasOwnProperty,Nt=e.unstable_scheduleCallback,We=e.unstable_cancelCallback,it=e.unstable_shouldYield,Pt=e.unstable_requestPaint,Ie=e.unstable_now,Q=e.unstable_getCurrentPriorityLevel,Oe=e.unstable_ImmediatePriority,Xe=e.unstable_UserBlockingPriority,Lt=e.unstable_NormalPriority,ln=e.unstable_LowPriority,Kt=e.unstable_IdlePriority,gn=e.log,vn=e.unstable_setDisableYieldValue,yn=null,kt=null;function cn(n){if(typeof gn=="function"&&vn(n),kt&&typeof kt.setStrictMode=="function")try{kt.setStrictMode(yn,n)}catch{}}var Ot=Math.clz32?Math.clz32:le,Ur=Math.log,$r=Math.LN2;function le(n){return n>>>=0,n===0?32:31-(Ur(n)/$r|0)|0}var Me=256,at=262144,ot=4194304;function qt(n){var a=n&42;if(a!==0)return a;switch(n&-n){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return n&261888;case 262144:case 524288:case 1048576:case 2097152:return n&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return n&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return n}}function Pe(n,a,o){var c=n.pendingLanes;if(c===0)return 0;var p=0,v=n.suspendedLanes,w=n.pingedLanes;n=n.warmLanes;var N=c&134217727;return N!==0?(c=N&~v,c!==0?p=qt(c):(w&=N,w!==0?p=qt(w):o||(o=N&~n,o!==0&&(p=qt(o))))):(N=c&~v,N!==0?p=qt(N):w!==0?p=qt(w):o||(o=c&~n,o!==0&&(p=qt(o)))),p===0?0:a!==0&&a!==p&&(a&v)===0&&(v=p&-p,o=a&-a,v>=o||v===32&&(o&4194048)!==0)?a:p}function yt(n,a){return(n.pendingLanes&~(n.suspendedLanes&~n.pingedLanes)&a)===0}function Yt(n,a){switch(n){case 1:case 2:case 4:case 8:case 64:return a+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function un(){var n=ot;return ot<<=1,(ot&62914560)===0&&(ot=4194304),n}function xa(n){for(var a=[],o=0;31>o;o++)a.push(n);return a}function Ct(n,a){n.pendingLanes|=a,a!==268435456&&(n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0)}function bn(n,a,o,c,p,v){var w=n.pendingLanes;n.pendingLanes=o,n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0,n.expiredLanes&=o,n.entangledLanes&=o,n.errorRecoveryDisabledLanes&=o,n.shellSuspendCounter=0;var N=n.entanglements,M=n.expirationTimes,V=n.hiddenUpdates;for(o=w&~o;0"u")return null;try{return n.activeElement||n.body}catch{return n.body}}var $N=/[\n"\\]/g;function Kn(n){return n.replace($N,function(a){return"\\"+a.charCodeAt(0).toString(16)+" "})}function bf(n,a,o,c,p,v,w,N){n.name="",w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"?n.type=w:n.removeAttribute("type"),a!=null?w==="number"?(a===0&&n.value===""||n.value!=a)&&(n.value=""+Fn(a)):n.value!==""+Fn(a)&&(n.value=""+Fn(a)):w!=="submit"&&w!=="reset"||n.removeAttribute("value"),a!=null?xf(n,w,Fn(a)):o!=null?xf(n,w,Fn(o)):c!=null&&n.removeAttribute("value"),p==null&&v!=null&&(n.defaultChecked=!!v),p!=null&&(n.checked=p&&typeof p!="function"&&typeof p!="symbol"),N!=null&&typeof N!="function"&&typeof N!="symbol"&&typeof N!="boolean"?n.name=""+Fn(N):n.removeAttribute("name")}function Av(n,a,o,c,p,v,w,N){if(v!=null&&typeof v!="function"&&typeof v!="symbol"&&typeof v!="boolean"&&(n.type=v),a!=null||o!=null){if(!(v!=="submit"&&v!=="reset"||a!=null)){yf(n);return}o=o!=null?""+Fn(o):"",a=a!=null?""+Fn(a):o,N||a===n.value||(n.value=a),n.defaultValue=a}c=c??p,c=typeof c!="function"&&typeof c!="symbol"&&!!c,n.checked=N?n.checked:!!c,n.defaultChecked=!!c,w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"&&(n.name=w),yf(n)}function xf(n,a,o){a==="number"&&cc(n.ownerDocument)===n||n.defaultValue===""+o||(n.defaultValue=""+o)}function ei(n,a,o,c){if(n=n.options,a){a={};for(var p=0;p"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Cf=!1;if(qr)try{var xo={};Object.defineProperty(xo,"passive",{get:function(){Cf=!0}}),window.addEventListener("test",xo,xo),window.removeEventListener("test",xo,xo)}catch{Cf=!1}var _a=null,Tf=null,dc=null;function Pv(){if(dc)return dc;var n,a=Tf,o=a.length,c,p="value"in _a?_a.value:_a.textContent,v=p.length;for(n=0;n=So),Bv=" ",Hv=!1;function qv(n,a){switch(n){case"keyup":return mA.indexOf(a.keyCode)!==-1;case"keydown":return a.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Vv(n){return n=n.detail,typeof n=="object"&&"data"in n?n.data:null}var ai=!1;function gA(n,a){switch(n){case"compositionend":return Vv(a);case"keypress":return a.which!==32?null:(Hv=!0,Bv);case"textInput":return n=a.data,n===Bv&&Hv?null:n;default:return null}}function vA(n,a){if(ai)return n==="compositionend"||!kf&&qv(n,a)?(n=Pv(),dc=Tf=_a=null,ai=!1,n):null;switch(n){case"paste":return null;case"keypress":if(!(a.ctrlKey||a.altKey||a.metaKey)||a.ctrlKey&&a.altKey){if(a.char&&1=a)return{node:o,offset:a-n};n=c}e:{for(;o;){if(o.nextSibling){o=o.nextSibling;break e}o=o.parentNode}o=void 0}o=Qv(o)}}function ey(n,a){return n&&a?n===a?!0:n&&n.nodeType===3?!1:a&&a.nodeType===3?ey(n,a.parentNode):"contains"in n?n.contains(a):n.compareDocumentPosition?!!(n.compareDocumentPosition(a)&16):!1:!1}function ty(n){n=n!=null&&n.ownerDocument!=null&&n.ownerDocument.defaultView!=null?n.ownerDocument.defaultView:window;for(var a=cc(n.document);a instanceof n.HTMLIFrameElement;){try{var o=typeof a.contentWindow.location.href=="string"}catch{o=!1}if(o)n=a.contentWindow;else break;a=cc(n.document)}return a}function Df(n){var a=n&&n.nodeName&&n.nodeName.toLowerCase();return a&&(a==="input"&&(n.type==="text"||n.type==="search"||n.type==="tel"||n.type==="url"||n.type==="password")||a==="textarea"||n.contentEditable==="true")}var CA=qr&&"documentMode"in document&&11>=document.documentMode,si=null,Pf=null,No=null,Lf=!1;function ny(n,a,o){var c=o.window===o?o.document:o.nodeType===9?o:o.ownerDocument;Lf||si==null||si!==cc(c)||(c=si,"selectionStart"in c&&Df(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset}),No&&To(No,c)||(No=c,c=au(Pf,"onSelect"),0>=w,p-=w,_r=1<<32-Ot(a)+p|o<Ge?(tt=Ce,Ce=null):tt=Ce.sibling;var ct=F(z,Ce,q[Ge],ae);if(ct===null){Ce===null&&(Ce=tt);break}n&&Ce&&ct.alternate===null&&a(z,Ce),P=v(ct,P,Ge),lt===null?ke=ct:lt.sibling=ct,lt=ct,Ce=tt}if(Ge===q.length)return o(z,Ce),rt&&Gr(z,Ge),ke;if(Ce===null){for(;GeGe?(tt=Ce,Ce=null):tt=Ce.sibling;var Va=F(z,Ce,ct.value,ae);if(Va===null){Ce===null&&(Ce=tt);break}n&&Ce&&Va.alternate===null&&a(z,Ce),P=v(Va,P,Ge),lt===null?ke=Va:lt.sibling=Va,lt=Va,Ce=tt}if(ct.done)return o(z,Ce),rt&&Gr(z,Ge),ke;if(Ce===null){for(;!ct.done;Ge++,ct=q.next())ct=ie(z,ct.value,ae),ct!==null&&(P=v(ct,P,Ge),lt===null?ke=ct:lt.sibling=ct,lt=ct);return rt&&Gr(z,Ge),ke}for(Ce=c(Ce);!ct.done;Ge++,ct=q.next())ct=X(Ce,z,Ge,ct.value,ae),ct!==null&&(n&&ct.alternate!==null&&Ce.delete(ct.key===null?Ge:ct.key),P=v(ct,P,Ge),lt===null?ke=ct:lt.sibling=ct,lt=ct);return n&&Ce.forEach(function(GR){return a(z,GR)}),rt&&Gr(z,Ge),ke}function pt(z,P,q,ae){if(typeof q=="object"&&q!==null&&q.type===S&&q.key===null&&(q=q.props.children),typeof q=="object"&&q!==null){switch(q.$$typeof){case _:e:{for(var ke=q.key;P!==null;){if(P.key===ke){if(ke=q.type,ke===S){if(P.tag===7){o(z,P.sibling),ae=p(P,q.props.children),ae.return=z,z=ae;break e}}else if(P.elementType===ke||typeof ke=="object"&&ke!==null&&ke.$$typeof===H&&ws(ke)===P.type){o(z,P.sibling),ae=p(P,q.props),Mo(ae,q),ae.return=z,z=ae;break e}o(z,P);break}else a(z,P);P=P.sibling}q.type===S?(ae=gs(q.props.children,z.mode,ae,q.key),ae.return=z,z=ae):(ae=wc(q.type,q.key,q.props,null,z.mode,ae),Mo(ae,q),ae.return=z,z=ae)}return w(z);case E:e:{for(ke=q.key;P!==null;){if(P.key===ke)if(P.tag===4&&P.stateNode.containerInfo===q.containerInfo&&P.stateNode.implementation===q.implementation){o(z,P.sibling),ae=p(P,q.children||[]),ae.return=z,z=ae;break e}else{o(z,P);break}else a(z,P);P=P.sibling}ae=qf(q,z.mode,ae),ae.return=z,z=ae}return w(z);case H:return q=ws(q),pt(z,P,q,ae)}if(pe(q))return _e(z,P,q,ae);if(he(q)){if(ke=he(q),typeof ke!="function")throw Error(s(150));return q=ke.call(q),De(z,P,q,ae)}if(typeof q.then=="function")return pt(z,P,Ac(q),ae);if(q.$$typeof===A)return pt(z,P,Ec(z,q),ae);Rc(z,q)}return typeof q=="string"&&q!==""||typeof q=="number"||typeof q=="bigint"?(q=""+q,P!==null&&P.tag===6?(o(z,P.sibling),ae=p(P,q),ae.return=z,z=ae):(o(z,P),ae=Hf(q,z.mode,ae),ae.return=z,z=ae),w(z)):o(z,P)}return function(z,P,q,ae){try{Oo=0;var ke=pt(z,P,q,ae);return gi=null,ke}catch(Ce){if(Ce===pi||Ce===Tc)throw Ce;var lt=Ln(29,Ce,null,z.mode);return lt.lanes=ae,lt.return=z,lt}finally{}}}var Ss=Cy(!0),Ty=Cy(!1),Na=!1;function th(n){n.updateQueue={baseState:n.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function nh(n,a){n=n.updateQueue,a.updateQueue===n&&(a.updateQueue={baseState:n.baseState,firstBaseUpdate:n.firstBaseUpdate,lastBaseUpdate:n.lastBaseUpdate,shared:n.shared,callbacks:null})}function Aa(n){return{lane:n,tag:0,payload:null,callback:null,next:null}}function Ra(n,a,o){var c=n.updateQueue;if(c===null)return null;if(c=c.shared,(ut&2)!==0){var p=c.pending;return p===null?a.next=a:(a.next=p.next,p.next=a),c.pending=a,a=xc(n),cy(n,null,o),a}return bc(n,c,a,o),xc(n)}function Do(n,a,o){if(a=a.updateQueue,a!==null&&(a=a.shared,(o&4194048)!==0)){var c=a.lanes;c&=n.pendingLanes,o|=c,a.lanes=o,xn(n,o)}}function rh(n,a){var o=n.updateQueue,c=n.alternate;if(c!==null&&(c=c.updateQueue,o===c)){var p=null,v=null;if(o=o.firstBaseUpdate,o!==null){do{var w={lane:o.lane,tag:o.tag,payload:o.payload,callback:null,next:null};v===null?p=v=w:v=v.next=w,o=o.next}while(o!==null);v===null?p=v=a:v=v.next=a}else p=v=a;o={baseState:c.baseState,firstBaseUpdate:p,lastBaseUpdate:v,shared:c.shared,callbacks:c.callbacks},n.updateQueue=o;return}n=o.lastBaseUpdate,n===null?o.firstBaseUpdate=a:n.next=a,o.lastBaseUpdate=a}var ah=!1;function Po(){if(ah){var n=mi;if(n!==null)throw n}}function Lo(n,a,o,c){ah=!1;var p=n.updateQueue;Na=!1;var v=p.firstBaseUpdate,w=p.lastBaseUpdate,N=p.shared.pending;if(N!==null){p.shared.pending=null;var M=N,V=M.next;M.next=null,w===null?v=V:w.next=V,w=M;var ne=n.alternate;ne!==null&&(ne=ne.updateQueue,N=ne.lastBaseUpdate,N!==w&&(N===null?ne.firstBaseUpdate=V:N.next=V,ne.lastBaseUpdate=M))}if(v!==null){var ie=p.baseState;w=0,ne=V=M=null,N=v;do{var F=N.lane&-536870913,X=F!==N.lane;if(X?(et&F)===F:(c&F)===F){F!==0&&F===hi&&(ah=!0),ne!==null&&(ne=ne.next={lane:0,tag:N.tag,payload:N.payload,callback:null,next:null});e:{var _e=n,De=N;F=a;var pt=o;switch(De.tag){case 1:if(_e=De.payload,typeof _e=="function"){ie=_e.call(pt,ie,F);break e}ie=_e;break e;case 3:_e.flags=_e.flags&-65537|128;case 0:if(_e=De.payload,F=typeof _e=="function"?_e.call(pt,ie,F):_e,F==null)break e;ie=g({},ie,F);break e;case 2:Na=!0}}F=N.callback,F!==null&&(n.flags|=64,X&&(n.flags|=8192),X=p.callbacks,X===null?p.callbacks=[F]:X.push(F))}else X={lane:F,tag:N.tag,payload:N.payload,callback:N.callback,next:null},ne===null?(V=ne=X,M=ie):ne=ne.next=X,w|=F;if(N=N.next,N===null){if(N=p.shared.pending,N===null)break;X=N,N=X.next,X.next=null,p.lastBaseUpdate=X,p.shared.pending=null}}while(!0);ne===null&&(M=ie),p.baseState=M,p.firstBaseUpdate=V,p.lastBaseUpdate=ne,v===null&&(p.shared.lanes=0),Da|=w,n.lanes=w,n.memoizedState=ie}}function Ny(n,a){if(typeof n!="function")throw Error(s(191,n));n.call(a)}function Ay(n,a){var o=n.callbacks;if(o!==null)for(n.callbacks=null,n=0;nv?v:8;var w=j.T,N={};j.T=N,Sh(n,!1,a,o);try{var M=p(),V=j.S;if(V!==null&&V(N,M),M!==null&&typeof M=="object"&&typeof M.then=="function"){var ne=DA(M,c);Uo(n,a,ne,Bn(n))}else Uo(n,a,c,Bn(n))}catch(ie){Uo(n,a,{then:function(){},status:"rejected",reason:ie},Bn())}finally{K.p=v,w!==null&&N.types!==null&&(w.types=N.types),j.T=w}}function $A(){}function wh(n,a,o,c){if(n.tag!==5)throw Error(s(476));var p=ib(n).queue;sb(n,p,a,W,o===null?$A:function(){return ob(n),o(c)})}function ib(n){var a=n.memoizedState;if(a!==null)return a;a={memoizedState:W,baseState:W,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wr,lastRenderedState:W},next:null};var o={};return a.next={memoizedState:o,baseState:o,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wr,lastRenderedState:o},next:null},n.memoizedState=a,n=n.alternate,n!==null&&(n.memoizedState=a),a}function ob(n){var a=ib(n);a.next===null&&(a=n.alternate.memoizedState),Uo(n,a.next.queue,{},Bn())}function _h(){return tn(nl)}function lb(){return Dt().memoizedState}function cb(){return Dt().memoizedState}function BA(n){for(var a=n.return;a!==null;){switch(a.tag){case 24:case 3:var o=Bn();n=Aa(o);var c=Ra(a,n,o);c!==null&&(An(c,a,o),Do(c,a,o)),a={cache:Jf()},n.payload=a;return}a=a.return}}function HA(n,a,o){var c=Bn();o={lane:c,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null},Uc(n)?db(a,o):(o=$f(n,a,o,c),o!==null&&(An(o,n,c),fb(o,a,c)))}function ub(n,a,o){var c=Bn();Uo(n,a,o,c)}function Uo(n,a,o,c){var p={lane:c,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null};if(Uc(n))db(a,p);else{var v=n.alternate;if(n.lanes===0&&(v===null||v.lanes===0)&&(v=a.lastRenderedReducer,v!==null))try{var w=a.lastRenderedState,N=v(w,o);if(p.hasEagerState=!0,p.eagerState=N,Pn(N,w))return bc(n,a,p,0),vt===null&&yc(),!1}catch{}finally{}if(o=$f(n,a,p,c),o!==null)return An(o,n,c),fb(o,a,c),!0}return!1}function Sh(n,a,o,c){if(c={lane:2,revertLane:tm(),gesture:null,action:c,hasEagerState:!1,eagerState:null,next:null},Uc(n)){if(a)throw Error(s(479))}else a=$f(n,o,c,2),a!==null&&An(a,n,2)}function Uc(n){var a=n.alternate;return n===Be||a!==null&&a===Be}function db(n,a){yi=Oc=!0;var o=n.pending;o===null?a.next=a:(a.next=o.next,o.next=a),n.pending=a}function fb(n,a,o){if((o&4194048)!==0){var c=a.lanes;c&=n.pendingLanes,o|=c,a.lanes=o,xn(n,o)}}var $o={readContext:tn,use:Pc,useCallback:At,useContext:At,useEffect:At,useImperativeHandle:At,useLayoutEffect:At,useInsertionEffect:At,useMemo:At,useReducer:At,useRef:At,useState:At,useDebugValue:At,useDeferredValue:At,useTransition:At,useSyncExternalStore:At,useId:At,useHostTransitionStatus:At,useFormState:At,useActionState:At,useOptimistic:At,useMemoCache:At,useCacheRefresh:At};$o.useEffectEvent=At;var hb={readContext:tn,use:Pc,useCallback:function(n,a){return hn().memoizedState=[n,a===void 0?null:a],n},useContext:tn,useEffect:Xy,useImperativeHandle:function(n,a,o){o=o!=null?o.concat([n]):null,zc(4194308,4,eb.bind(null,a,n),o)},useLayoutEffect:function(n,a){return zc(4194308,4,n,a)},useInsertionEffect:function(n,a){zc(4,2,n,a)},useMemo:function(n,a){var o=hn();a=a===void 0?null:a;var c=n();if(Es){cn(!0);try{n()}finally{cn(!1)}}return o.memoizedState=[c,a],c},useReducer:function(n,a,o){var c=hn();if(o!==void 0){var p=o(a);if(Es){cn(!0);try{o(a)}finally{cn(!1)}}}else p=a;return c.memoizedState=c.baseState=p,n={pending:null,lanes:0,dispatch:null,lastRenderedReducer:n,lastRenderedState:p},c.queue=n,n=n.dispatch=HA.bind(null,Be,n),[c.memoizedState,n]},useRef:function(n){var a=hn();return n={current:n},a.memoizedState=n},useState:function(n){n=gh(n);var a=n.queue,o=ub.bind(null,Be,a);return a.dispatch=o,[n.memoizedState,o]},useDebugValue:bh,useDeferredValue:function(n,a){var o=hn();return xh(o,n,a)},useTransition:function(){var n=gh(!1);return n=sb.bind(null,Be,n.queue,!0,!1),hn().memoizedState=n,[!1,n]},useSyncExternalStore:function(n,a,o){var c=Be,p=hn();if(rt){if(o===void 0)throw Error(s(407));o=o()}else{if(o=a(),vt===null)throw Error(s(349));(et&127)!==0||Dy(c,a,o)}p.memoizedState=o;var v={value:o,getSnapshot:a};return p.queue=v,Xy(Ly.bind(null,c,v,n),[n]),c.flags|=2048,xi(9,{destroy:void 0},Py.bind(null,c,v,o,a),null),o},useId:function(){var n=hn(),a=vt.identifierPrefix;if(rt){var o=Sr,c=_r;o=(c&~(1<<32-Ot(c)-1)).toString(32)+o,a="_"+a+"R_"+o,o=Mc++,0<\/script>",v=v.removeChild(v.firstChild);break;case"select":v=typeof c.is=="string"?w.createElement("select",{is:c.is}):w.createElement("select"),c.multiple?v.multiple=!0:c.size&&(v.size=c.size);break;default:v=typeof c.is=="string"?w.createElement(p,{is:c.is}):w.createElement(p)}}v[Zt]=a,v[_n]=c;e:for(w=a.child;w!==null;){if(w.tag===5||w.tag===6)v.appendChild(w.stateNode);else if(w.tag!==4&&w.tag!==27&&w.child!==null){w.child.return=w,w=w.child;continue}if(w===a)break e;for(;w.sibling===null;){if(w.return===null||w.return===a)break e;w=w.return}w.sibling.return=w.return,w=w.sibling}a.stateNode=v;e:switch(rn(v,p,c),p){case"button":case"input":case"select":case"textarea":c=!!c.autoFocus;break e;case"img":c=!0;break e;default:c=!1}c&&Jr(a)}}return _t(a),zh(a,a.type,n===null?null:n.memoizedProps,a.pendingProps,o),null;case 6:if(n&&a.stateNode!=null)n.memoizedProps!==c&&Jr(a);else{if(typeof c!="string"&&a.stateNode===null)throw Error(s(166));if(n=re.current,di(a)){if(n=a.stateNode,o=a.memoizedProps,c=null,p=en,p!==null)switch(p.tag){case 27:case 5:c=p.memoizedProps}n[Zt]=a,n=!!(n.nodeValue===o||c!==null&&c.suppressHydrationWarning===!0||O0(n.nodeValue,o)),n||Ca(a,!0)}else n=su(n).createTextNode(c),n[Zt]=a,a.stateNode=n}return _t(a),null;case 31:if(o=a.memoizedState,n===null||n.memoizedState!==null){if(c=di(a),o!==null){if(n===null){if(!c)throw Error(s(318));if(n=a.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(s(557));n[Zt]=a}else vs(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;_t(a),n=!1}else o=Kf(),n!==null&&n.memoizedState!==null&&(n.memoizedState.hydrationErrors=o),n=!0;if(!n)return a.flags&256?(In(a),a):(In(a),null);if((a.flags&128)!==0)throw Error(s(558))}return _t(a),null;case 13:if(c=a.memoizedState,n===null||n.memoizedState!==null&&n.memoizedState.dehydrated!==null){if(p=di(a),c!==null&&c.dehydrated!==null){if(n===null){if(!p)throw Error(s(318));if(p=a.memoizedState,p=p!==null?p.dehydrated:null,!p)throw Error(s(317));p[Zt]=a}else vs(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;_t(a),p=!1}else p=Kf(),n!==null&&n.memoizedState!==null&&(n.memoizedState.hydrationErrors=p),p=!0;if(!p)return a.flags&256?(In(a),a):(In(a),null)}return In(a),(a.flags&128)!==0?(a.lanes=o,a):(o=c!==null,n=n!==null&&n.memoizedState!==null,o&&(c=a.child,p=null,c.alternate!==null&&c.alternate.memoizedState!==null&&c.alternate.memoizedState.cachePool!==null&&(p=c.alternate.memoizedState.cachePool.pool),v=null,c.memoizedState!==null&&c.memoizedState.cachePool!==null&&(v=c.memoizedState.cachePool.pool),v!==p&&(c.flags|=2048)),o!==n&&o&&(a.child.flags|=8192),Vc(a,a.updateQueue),_t(a),null);case 4:return se(),n===null&&sm(a.stateNode.containerInfo),_t(a),null;case 10:return Kr(a.type),_t(a),null;case 19:if(O(Mt),c=a.memoizedState,c===null)return _t(a),null;if(p=(a.flags&128)!==0,v=c.rendering,v===null)if(p)Ho(c,!1);else{if(Rt!==0||n!==null&&(n.flags&128)!==0)for(n=a.child;n!==null;){if(v=kc(n),v!==null){for(a.flags|=128,Ho(c,!1),n=v.updateQueue,a.updateQueue=n,Vc(a,n),a.subtreeFlags=0,n=o,o=a.child;o!==null;)uy(o,n),o=o.sibling;return U(Mt,Mt.current&1|2),rt&&Gr(a,c.treeForkCount),a.child}n=n.sibling}c.tail!==null&&Ie()>Wc&&(a.flags|=128,p=!0,Ho(c,!1),a.lanes=4194304)}else{if(!p)if(n=kc(v),n!==null){if(a.flags|=128,p=!0,n=n.updateQueue,a.updateQueue=n,Vc(a,n),Ho(c,!0),c.tail===null&&c.tailMode==="hidden"&&!v.alternate&&!rt)return _t(a),null}else 2*Ie()-c.renderingStartTime>Wc&&o!==536870912&&(a.flags|=128,p=!0,Ho(c,!1),a.lanes=4194304);c.isBackwards?(v.sibling=a.child,a.child=v):(n=c.last,n!==null?n.sibling=v:a.child=v,c.last=v)}return c.tail!==null?(n=c.tail,c.rendering=n,c.tail=n.sibling,c.renderingStartTime=Ie(),n.sibling=null,o=Mt.current,U(Mt,p?o&1|2:o&1),rt&&Gr(a,c.treeForkCount),n):(_t(a),null);case 22:case 23:return In(a),ih(),c=a.memoizedState!==null,n!==null?n.memoizedState!==null!==c&&(a.flags|=8192):c&&(a.flags|=8192),c?(o&536870912)!==0&&(a.flags&128)===0&&(_t(a),a.subtreeFlags&6&&(a.flags|=8192)):_t(a),o=a.updateQueue,o!==null&&Vc(a,o.retryQueue),o=null,n!==null&&n.memoizedState!==null&&n.memoizedState.cachePool!==null&&(o=n.memoizedState.cachePool.pool),c=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),c!==o&&(a.flags|=2048),n!==null&&O(xs),null;case 24:return o=null,n!==null&&(o=n.memoizedState.cache),a.memoizedState.cache!==o&&(a.flags|=2048),Kr(zt),_t(a),null;case 25:return null;case 30:return null}throw Error(s(156,a.tag))}function KA(n,a){switch(Gf(a),a.tag){case 1:return n=a.flags,n&65536?(a.flags=n&-65537|128,a):null;case 3:return Kr(zt),se(),n=a.flags,(n&65536)!==0&&(n&128)===0?(a.flags=n&-65537|128,a):null;case 26:case 27:case 5:return Te(a),null;case 31:if(a.memoizedState!==null){if(In(a),a.alternate===null)throw Error(s(340));vs()}return n=a.flags,n&65536?(a.flags=n&-65537|128,a):null;case 13:if(In(a),n=a.memoizedState,n!==null&&n.dehydrated!==null){if(a.alternate===null)throw Error(s(340));vs()}return n=a.flags,n&65536?(a.flags=n&-65537|128,a):null;case 19:return O(Mt),null;case 4:return se(),null;case 10:return Kr(a.type),null;case 22:case 23:return In(a),ih(),n!==null&&O(xs),n=a.flags,n&65536?(a.flags=n&-65537|128,a):null;case 24:return Kr(zt),null;case 25:return null;default:return null}}function zb(n,a){switch(Gf(a),a.tag){case 3:Kr(zt),se();break;case 26:case 27:case 5:Te(a);break;case 4:se();break;case 31:a.memoizedState!==null&&In(a);break;case 13:In(a);break;case 19:O(Mt);break;case 10:Kr(a.type);break;case 22:case 23:In(a),ih(),n!==null&&O(xs);break;case 24:Kr(zt)}}function qo(n,a){try{var o=a.updateQueue,c=o!==null?o.lastEffect:null;if(c!==null){var p=c.next;o=p;do{if((o.tag&n)===n){c=void 0;var v=o.create,w=o.inst;c=v(),w.destroy=c}o=o.next}while(o!==p)}}catch(N){ft(a,a.return,N)}}function Oa(n,a,o){try{var c=a.updateQueue,p=c!==null?c.lastEffect:null;if(p!==null){var v=p.next;c=v;do{if((c.tag&n)===n){var w=c.inst,N=w.destroy;if(N!==void 0){w.destroy=void 0,p=a;var M=o,V=N;try{V()}catch(ne){ft(p,M,ne)}}}c=c.next}while(c!==v)}}catch(ne){ft(a,a.return,ne)}}function Ib(n){var a=n.updateQueue;if(a!==null){var o=n.stateNode;try{Ay(a,o)}catch(c){ft(n,n.return,c)}}}function Ub(n,a,o){o.props=Cs(n.type,n.memoizedProps),o.state=n.memoizedState;try{o.componentWillUnmount()}catch(c){ft(n,a,c)}}function Vo(n,a){try{var o=n.ref;if(o!==null){switch(n.tag){case 26:case 27:case 5:var c=n.stateNode;break;case 30:c=n.stateNode;break;default:c=n.stateNode}typeof o=="function"?n.refCleanup=o(c):o.current=c}}catch(p){ft(n,a,p)}}function Er(n,a){var o=n.ref,c=n.refCleanup;if(o!==null)if(typeof c=="function")try{c()}catch(p){ft(n,a,p)}finally{n.refCleanup=null,n=n.alternate,n!=null&&(n.refCleanup=null)}else if(typeof o=="function")try{o(null)}catch(p){ft(n,a,p)}else o.current=null}function $b(n){var a=n.type,o=n.memoizedProps,c=n.stateNode;try{e:switch(a){case"button":case"input":case"select":case"textarea":o.autoFocus&&c.focus();break e;case"img":o.src?c.src=o.src:o.srcSet&&(c.srcset=o.srcSet)}}catch(p){ft(n,n.return,p)}}function Ih(n,a,o){try{var c=n.stateNode;pR(c,n.type,o,a),c[_n]=a}catch(p){ft(n,n.return,p)}}function Bb(n){return n.tag===5||n.tag===3||n.tag===26||n.tag===27&&Ua(n.type)||n.tag===4}function Uh(n){e:for(;;){for(;n.sibling===null;){if(n.return===null||Bb(n.return))return null;n=n.return}for(n.sibling.return=n.return,n=n.sibling;n.tag!==5&&n.tag!==6&&n.tag!==18;){if(n.tag===27&&Ua(n.type)||n.flags&2||n.child===null||n.tag===4)continue e;n.child.return=n,n=n.child}if(!(n.flags&2))return n.stateNode}}function $h(n,a,o){var c=n.tag;if(c===5||c===6)n=n.stateNode,a?(o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o).insertBefore(n,a):(a=o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o,a.appendChild(n),o=o._reactRootContainer,o!=null||a.onclick!==null||(a.onclick=Hr));else if(c!==4&&(c===27&&Ua(n.type)&&(o=n.stateNode,a=null),n=n.child,n!==null))for($h(n,a,o),n=n.sibling;n!==null;)$h(n,a,o),n=n.sibling}function Gc(n,a,o){var c=n.tag;if(c===5||c===6)n=n.stateNode,a?o.insertBefore(n,a):o.appendChild(n);else if(c!==4&&(c===27&&Ua(n.type)&&(o=n.stateNode),n=n.child,n!==null))for(Gc(n,a,o),n=n.sibling;n!==null;)Gc(n,a,o),n=n.sibling}function Hb(n){var a=n.stateNode,o=n.memoizedProps;try{for(var c=n.type,p=a.attributes;p.length;)a.removeAttributeNode(p[0]);rn(a,c,o),a[Zt]=n,a[_n]=o}catch(v){ft(n,n.return,v)}}var Qr=!1,$t=!1,Bh=!1,qb=typeof WeakSet=="function"?WeakSet:Set,Xt=null;function YA(n,a){if(n=n.containerInfo,lm=fu,n=ty(n),Df(n)){if("selectionStart"in n)var o={start:n.selectionStart,end:n.selectionEnd};else e:{o=(o=n.ownerDocument)&&o.defaultView||window;var c=o.getSelection&&o.getSelection();if(c&&c.rangeCount!==0){o=c.anchorNode;var p=c.anchorOffset,v=c.focusNode;c=c.focusOffset;try{o.nodeType,v.nodeType}catch{o=null;break e}var w=0,N=-1,M=-1,V=0,ne=0,ie=n,F=null;t:for(;;){for(var X;ie!==o||p!==0&&ie.nodeType!==3||(N=w+p),ie!==v||c!==0&&ie.nodeType!==3||(M=w+c),ie.nodeType===3&&(w+=ie.nodeValue.length),(X=ie.firstChild)!==null;)F=ie,ie=X;for(;;){if(ie===n)break t;if(F===o&&++V===p&&(N=w),F===v&&++ne===c&&(M=w),(X=ie.nextSibling)!==null)break;ie=F,F=ie.parentNode}ie=X}o=N===-1||M===-1?null:{start:N,end:M}}else o=null}o=o||{start:0,end:0}}else o=null;for(cm={focusedElem:n,selectionRange:o},fu=!1,Xt=a;Xt!==null;)if(a=Xt,n=a.child,(a.subtreeFlags&1028)!==0&&n!==null)n.return=a,Xt=n;else for(;Xt!==null;){switch(a=Xt,v=a.alternate,n=a.flags,a.tag){case 0:if((n&4)!==0&&(n=a.updateQueue,n=n!==null?n.events:null,n!==null))for(o=0;o title"))),rn(v,c,o),v[Zt]=n,Wt(v),c=v;break e;case"link":var w=W0("link","href",p).get(c+(o.href||""));if(w){for(var N=0;Npt&&(w=pt,pt=De,De=w);var z=Zv(N,De),P=Zv(N,pt);if(z&&P&&(X.rangeCount!==1||X.anchorNode!==z.node||X.anchorOffset!==z.offset||X.focusNode!==P.node||X.focusOffset!==P.offset)){var q=ie.createRange();q.setStart(z.node,z.offset),X.removeAllRanges(),De>pt?(X.addRange(q),X.extend(P.node,P.offset)):(q.setEnd(P.node,P.offset),X.addRange(q))}}}}for(ie=[],X=N;X=X.parentNode;)X.nodeType===1&&ie.push({element:X,left:X.scrollLeft,top:X.scrollTop});for(typeof N.focus=="function"&&N.focus(),N=0;No?32:o,j.T=null,o=Yh,Yh=null;var v=La,w=ra;if(Vt=0,Ci=La=null,ra=0,(ut&6)!==0)throw Error(s(331));var N=ut;if(ut|=4,e0(v.current),Jb(v,v.current,w,o),ut=N,Xo(0,!1),kt&&typeof kt.onPostCommitFiberRoot=="function")try{kt.onPostCommitFiberRoot(yn,v)}catch{}return!0}finally{K.p=p,j.T=c,y0(n,a)}}function x0(n,a,o){a=Wn(o,a),a=Nh(n.stateNode,a,2),n=Ra(n,a,2),n!==null&&(Ct(n,2),Cr(n))}function ft(n,a,o){if(n.tag===3)x0(n,n,o);else for(;a!==null;){if(a.tag===3){x0(a,n,o);break}else if(a.tag===1){var c=a.stateNode;if(typeof a.type.getDerivedStateFromError=="function"||typeof c.componentDidCatch=="function"&&(Pa===null||!Pa.has(c))){n=Wn(o,n),o=wb(2),c=Ra(a,o,2),c!==null&&(_b(o,c,a,n),Ct(c,2),Cr(c));break}}a=a.return}}function Qh(n,a,o){var c=n.pingCache;if(c===null){c=n.pingCache=new JA;var p=new Set;c.set(a,p)}else p=c.get(a),p===void 0&&(p=new Set,c.set(a,p));p.has(o)||(Vh=!0,p.add(o),n=nR.bind(null,n,a,o),a.then(n,n))}function nR(n,a,o){var c=n.pingCache;c!==null&&c.delete(a),n.pingedLanes|=n.suspendedLanes&o,n.warmLanes&=~o,vt===n&&(et&o)===o&&(Rt===4||Rt===3&&(et&62914560)===et&&300>Ie()-Yc?(ut&2)===0&&Ti(n,0):Gh|=o,Ei===et&&(Ei=0)),Cr(n)}function w0(n,a){a===0&&(a=un()),n=ps(n,a),n!==null&&(Ct(n,a),Cr(n))}function rR(n){var a=n.memoizedState,o=0;a!==null&&(o=a.retryLane),w0(n,o)}function aR(n,a){var o=0;switch(n.tag){case 31:case 13:var c=n.stateNode,p=n.memoizedState;p!==null&&(o=p.retryLane);break;case 19:c=n.stateNode;break;case 22:c=n.stateNode._retryCache;break;default:throw Error(s(314))}c!==null&&c.delete(a),w0(n,o)}function sR(n,a){return Nt(n,a)}var tu=null,Ai=null,Zh=!1,nu=!1,em=!1,Ia=0;function Cr(n){n!==Ai&&n.next===null&&(Ai===null?tu=Ai=n:Ai=Ai.next=n),nu=!0,Zh||(Zh=!0,oR())}function Xo(n,a){if(!em&&nu){em=!0;do for(var o=!1,c=tu;c!==null;){if(n!==0){var p=c.pendingLanes;if(p===0)var v=0;else{var w=c.suspendedLanes,N=c.pingedLanes;v=(1<<31-Ot(42|n)+1)-1,v&=p&~(w&~N),v=v&201326741?v&201326741|1:v?v|2:0}v!==0&&(o=!0,C0(c,v))}else v=et,v=Pe(c,c===vt?v:0,c.cancelPendingCommit!==null||c.timeoutHandle!==-1),(v&3)===0||yt(c,v)||(o=!0,C0(c,v));c=c.next}while(o);em=!1}}function iR(){_0()}function _0(){nu=Zh=!1;var n=0;Ia!==0&&vR()&&(n=Ia);for(var a=Ie(),o=null,c=tu;c!==null;){var p=c.next,v=S0(c,a);v===0?(c.next=null,o===null?tu=p:o.next=p,p===null&&(Ai=o)):(o=c,(n!==0||(v&3)!==0)&&(nu=!0)),c=p}Vt!==0&&Vt!==5||Xo(n),Ia!==0&&(Ia=0)}function S0(n,a){for(var o=n.suspendedLanes,c=n.pingedLanes,p=n.expirationTimes,v=n.pendingLanes&-62914561;0N)break;var ne=M.transferSize,ie=M.initiatorType;ne&&M0(ie)&&(M=M.responseEnd,w+=ne*(M"u"?null:document;function G0(n,a,o){var c=Ri;if(c&&typeof a=="string"&&a){var p=Kn(a);p='link[rel="'+n+'"][href="'+p+'"]',typeof o=="string"&&(p+='[crossorigin="'+o+'"]'),V0.has(p)||(V0.add(p),n={rel:n,crossOrigin:o,href:a},c.querySelector(p)===null&&(a=c.createElement("link"),rn(a,"link",n),Wt(a),c.head.appendChild(a)))}}function TR(n){aa.D(n),G0("dns-prefetch",n,null)}function NR(n,a){aa.C(n,a),G0("preconnect",n,a)}function AR(n,a,o){aa.L(n,a,o);var c=Ri;if(c&&n&&a){var p='link[rel="preload"][as="'+Kn(a)+'"]';a==="image"&&o&&o.imageSrcSet?(p+='[imagesrcset="'+Kn(o.imageSrcSet)+'"]',typeof o.imageSizes=="string"&&(p+='[imagesizes="'+Kn(o.imageSizes)+'"]')):p+='[href="'+Kn(n)+'"]';var v=p;switch(a){case"style":v=ji(n);break;case"script":v=ki(n)}tr.has(v)||(n=g({rel:"preload",href:a==="image"&&o&&o.imageSrcSet?void 0:n,as:a},o),tr.set(v,n),c.querySelector(p)!==null||a==="style"&&c.querySelector(el(v))||a==="script"&&c.querySelector(tl(v))||(a=c.createElement("link"),rn(a,"link",n),Wt(a),c.head.appendChild(a)))}}function RR(n,a){aa.m(n,a);var o=Ri;if(o&&n){var c=a&&typeof a.as=="string"?a.as:"script",p='link[rel="modulepreload"][as="'+Kn(c)+'"][href="'+Kn(n)+'"]',v=p;switch(c){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":v=ki(n)}if(!tr.has(v)&&(n=g({rel:"modulepreload",href:n},a),tr.set(v,n),o.querySelector(p)===null)){switch(c){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(o.querySelector(tl(v)))return}c=o.createElement("link"),rn(c,"link",n),Wt(c),o.head.appendChild(c)}}}function jR(n,a,o){aa.S(n,a,o);var c=Ri;if(c&&n){var p=Qs(c).hoistableStyles,v=ji(n);a=a||"default";var w=p.get(v);if(!w){var N={loading:0,preload:null};if(w=c.querySelector(el(v)))N.loading=5;else{n=g({rel:"stylesheet",href:n,"data-precedence":a},o),(o=tr.get(v))&&gm(n,o);var M=w=c.createElement("link");Wt(M),rn(M,"link",n),M._p=new Promise(function(V,ne){M.onload=V,M.onerror=ne}),M.addEventListener("load",function(){N.loading|=1}),M.addEventListener("error",function(){N.loading|=2}),N.loading|=4,ou(w,a,c)}w={type:"stylesheet",instance:w,count:1,state:N},p.set(v,w)}}}function kR(n,a){aa.X(n,a);var o=Ri;if(o&&n){var c=Qs(o).hoistableScripts,p=ki(n),v=c.get(p);v||(v=o.querySelector(tl(p)),v||(n=g({src:n,async:!0},a),(a=tr.get(p))&&vm(n,a),v=o.createElement("script"),Wt(v),rn(v,"link",n),o.head.appendChild(v)),v={type:"script",instance:v,count:1,state:null},c.set(p,v))}}function OR(n,a){aa.M(n,a);var o=Ri;if(o&&n){var c=Qs(o).hoistableScripts,p=ki(n),v=c.get(p);v||(v=o.querySelector(tl(p)),v||(n=g({src:n,async:!0,type:"module"},a),(a=tr.get(p))&&vm(n,a),v=o.createElement("script"),Wt(v),rn(v,"link",n),o.head.appendChild(v)),v={type:"script",instance:v,count:1,state:null},c.set(p,v))}}function F0(n,a,o,c){var p=(p=re.current)?iu(p):null;if(!p)throw Error(s(446));switch(n){case"meta":case"title":return null;case"style":return typeof o.precedence=="string"&&typeof o.href=="string"?(a=ji(o.href),o=Qs(p).hoistableStyles,c=o.get(a),c||(c={type:"style",instance:null,count:0,state:null},o.set(a,c)),c):{type:"void",instance:null,count:0,state:null};case"link":if(o.rel==="stylesheet"&&typeof o.href=="string"&&typeof o.precedence=="string"){n=ji(o.href);var v=Qs(p).hoistableStyles,w=v.get(n);if(w||(p=p.ownerDocument||p,w={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},v.set(n,w),(v=p.querySelector(el(n)))&&!v._p&&(w.instance=v,w.state.loading=5),tr.has(n)||(o={rel:"preload",as:"style",href:o.href,crossOrigin:o.crossOrigin,integrity:o.integrity,media:o.media,hrefLang:o.hrefLang,referrerPolicy:o.referrerPolicy},tr.set(n,o),v||MR(p,n,o,w.state))),a&&c===null)throw Error(s(528,""));return w}if(a&&c!==null)throw Error(s(529,""));return null;case"script":return a=o.async,o=o.src,typeof o=="string"&&a&&typeof a!="function"&&typeof a!="symbol"?(a=ki(o),o=Qs(p).hoistableScripts,c=o.get(a),c||(c={type:"script",instance:null,count:0,state:null},o.set(a,c)),c):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,n))}}function ji(n){return'href="'+Kn(n)+'"'}function el(n){return'link[rel="stylesheet"]['+n+"]"}function K0(n){return g({},n,{"data-precedence":n.precedence,precedence:null})}function MR(n,a,o,c){n.querySelector('link[rel="preload"][as="style"]['+a+"]")?c.loading=1:(a=n.createElement("link"),c.preload=a,a.addEventListener("load",function(){return c.loading|=1}),a.addEventListener("error",function(){return c.loading|=2}),rn(a,"link",o),Wt(a),n.head.appendChild(a))}function ki(n){return'[src="'+Kn(n)+'"]'}function tl(n){return"script[async]"+n}function Y0(n,a,o){if(a.count++,a.instance===null)switch(a.type){case"style":var c=n.querySelector('style[data-href~="'+Kn(o.href)+'"]');if(c)return a.instance=c,Wt(c),c;var p=g({},o,{"data-href":o.href,"data-precedence":o.precedence,href:null,precedence:null});return c=(n.ownerDocument||n).createElement("style"),Wt(c),rn(c,"style",p),ou(c,o.precedence,n),a.instance=c;case"stylesheet":p=ji(o.href);var v=n.querySelector(el(p));if(v)return a.state.loading|=4,a.instance=v,Wt(v),v;c=K0(o),(p=tr.get(p))&&gm(c,p),v=(n.ownerDocument||n).createElement("link"),Wt(v);var w=v;return w._p=new Promise(function(N,M){w.onload=N,w.onerror=M}),rn(v,"link",c),a.state.loading|=4,ou(v,o.precedence,n),a.instance=v;case"script":return v=ki(o.src),(p=n.querySelector(tl(v)))?(a.instance=p,Wt(p),p):(c=o,(p=tr.get(v))&&(c=g({},o),vm(c,p)),n=n.ownerDocument||n,p=n.createElement("script"),Wt(p),rn(p,"link",c),n.head.appendChild(p),a.instance=p);case"void":return null;default:throw Error(s(443,a.type))}else a.type==="stylesheet"&&(a.state.loading&4)===0&&(c=a.instance,a.state.loading|=4,ou(c,o.precedence,n));return a.instance}function ou(n,a,o){for(var c=o.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),p=c.length?c[c.length-1]:null,v=p,w=0;w title"):null)}function DR(n,a,o){if(o===1||a.itemProp!=null)return!1;switch(n){case"meta":case"title":return!0;case"style":if(typeof a.precedence!="string"||typeof a.href!="string"||a.href==="")break;return!0;case"link":if(typeof a.rel!="string"||typeof a.href!="string"||a.href===""||a.onLoad||a.onError)break;switch(a.rel){case"stylesheet":return n=a.disabled,typeof a.precedence=="string"&&n==null;default:return!0}case"script":if(a.async&&typeof a.async!="function"&&typeof a.async!="symbol"&&!a.onLoad&&!a.onError&&a.src&&typeof a.src=="string")return!0}return!1}function J0(n){return!(n.type==="stylesheet"&&(n.state.loading&3)===0)}function PR(n,a,o,c){if(o.type==="stylesheet"&&(typeof c.media!="string"||matchMedia(c.media).matches!==!1)&&(o.state.loading&4)===0){if(o.instance===null){var p=ji(c.href),v=a.querySelector(el(p));if(v){a=v._p,a!==null&&typeof a=="object"&&typeof a.then=="function"&&(n.count++,n=cu.bind(n),a.then(n,n)),o.state.loading|=4,o.instance=v,Wt(v);return}v=a.ownerDocument||a,c=K0(c),(p=tr.get(p))&&gm(c,p),v=v.createElement("link"),Wt(v);var w=v;w._p=new Promise(function(N,M){w.onload=N,w.onerror=M}),rn(v,"link",c),o.instance=v}n.stylesheets===null&&(n.stylesheets=new Map),n.stylesheets.set(o,a),(a=o.state.preload)&&(o.state.loading&3)===0&&(n.count++,o=cu.bind(n),a.addEventListener("load",o),a.addEventListener("error",o))}}var ym=0;function LR(n,a){return n.stylesheets&&n.count===0&&du(n,n.stylesheets),0ym?50:800)+a);return n.unsuspend=o,function(){n.unsuspend=null,clearTimeout(c),clearTimeout(p)}}:null}function cu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)du(this,this.stylesheets);else if(this.unsuspend){var n=this.unsuspend;this.unsuspend=null,n()}}}var uu=null;function du(n,a){n.stylesheets=null,n.unsuspend!==null&&(n.count++,uu=new Map,a.forEach(zR,n),uu=null,cu.call(n))}function zR(n,a){if(!(a.state.loading&4)){var o=uu.get(n);if(o)var c=o.get(null);else{o=new Map,uu.set(n,o);for(var p=n.querySelectorAll("link[data-precedence],style[data-precedence]"),v=0;v"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),Nm.exports=ZR(),Nm.exports}var tj=ej();/** + * react-router v7.15.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var wx="popstate";function _x(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function nj(e={}){function t(s,i){var h;let l=(h=i.state)==null?void 0:h.masked,{pathname:u,search:d,hash:f}=l||s.location;return fp("",{pathname:u,search:d,hash:f},i.state&&i.state.usr||null,i.state&&i.state.key||"default",l?{pathname:s.location.pathname,search:s.location.search,hash:s.location.hash}:void 0)}function r(s,i){return typeof i=="string"?i:Cl(i)}return aj(t,r,null,e)}function Tt(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function Lr(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function rj(){return Math.random().toString(36).substring(2,10)}function Sx(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function fp(e,t,r=null,s,i){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?lo(t):t,state:r,key:t&&t.key||s||rj(),mask:i}}function Cl({pathname:e="/",search:t="",hash:r=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),r&&r!=="#"&&(e+=r.charAt(0)==="#"?r:"#"+r),e}function lo(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substring(r),e=e.substring(0,r));let s=e.indexOf("?");s>=0&&(t.search=e.substring(s),e=e.substring(0,s)),e&&(t.pathname=e)}return t}function aj(e,t,r,s={}){let{window:i=document.defaultView,v5Compat:l=!1}=s,u=i.history,d="POP",f=null,h=b();h==null&&(h=0,u.replaceState({...u.state,idx:h},""));function b(){return(u.state||{idx:null}).idx}function g(){d="POP";let T=b(),C=T==null?null:T-h;h=T,f&&f({action:d,location:S.location,delta:C})}function x(T,C){d="PUSH";let R=_x(T)?T:fp(S.location,T,C);h=b()+1;let A=Sx(R,h),D=S.createHref(R.mask||R);try{u.pushState(A,"",D)}catch(B){if(B instanceof DOMException&&B.name==="DataCloneError")throw B;i.location.assign(D)}l&&f&&f({action:d,location:S.location,delta:1})}function _(T,C){d="REPLACE";let R=_x(T)?T:fp(S.location,T,C);h=b();let A=Sx(R,h),D=S.createHref(R.mask||R);u.replaceState(A,"",D),l&&f&&f({action:d,location:S.location,delta:0})}function E(T){return sj(T)}let S={get action(){return d},get location(){return e(i,u)},listen(T){if(f)throw new Error("A history only accepts one active listener");return i.addEventListener(wx,g),f=T,()=>{i.removeEventListener(wx,g),f=null}},createHref(T){return t(i,T)},createURL:E,encodeLocation(T){let C=E(T);return{pathname:C.pathname,search:C.search,hash:C.hash}},push:x,replace:_,go(T){return u.go(T)}};return S}function sj(e,t=!1){let r="http://localhost";typeof window<"u"&&(r=window.location.origin!=="null"?window.location.origin:window.location.href),Tt(r,"No window.location.(origin|href) available to create URL");let s=typeof e=="string"?e:Cl(e);return s=s.replace(/ $/,"%20"),!t&&s.startsWith("//")&&(s=r+s),new URL(s,r)}function x_(e,t,r="/"){return ij(e,t,r,!1)}function ij(e,t,r,s,i){let l=typeof t=="string"?lo(t):t,u=ua(l.pathname||"/",r);if(u==null)return null;let d=oj(e),f=null,h=bj(u);for(let b=0;f==null&&b{let b={relativePath:h===void 0?u.path||"":h,caseSensitive:u.caseSensitive===!0,childrenIndex:d,route:u};if(b.relativePath.startsWith("/")){if(!b.relativePath.startsWith(s)&&f)return;Tt(b.relativePath.startsWith(s),`Absolute route path "${b.relativePath}" nested under path "${s}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),b.relativePath=b.relativePath.slice(s.length)}let g=yr([s,b.relativePath]),x=r.concat(b);u.children&&u.children.length>0&&(Tt(u.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${g}".`),w_(u.children,t,x,g,f)),!(u.path==null&&!u.index)&&t.push({path:g,score:pj(g,u.index),routesMeta:x})};return e.forEach((u,d)=>{var f;if(u.path===""||!((f=u.path)!=null&&f.includes("?")))l(u,d);else for(let h of __(u.path))l(u,d,!0,h)}),t}function __(e){let t=e.split("/");if(t.length===0)return[];let[r,...s]=t,i=r.endsWith("?"),l=r.replace(/\?$/,"");if(s.length===0)return i?[l,""]:[l];let u=__(s.join("/")),d=[];return d.push(...u.map(f=>f===""?l:[l,f].join("/"))),i&&d.push(...u),d.map(f=>e.startsWith("/")&&f===""?"/":f)}function lj(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:gj(t.routesMeta.map(s=>s.childrenIndex),r.routesMeta.map(s=>s.childrenIndex)))}var cj=/^:[\w-]+$/,uj=3,dj=2,fj=1,hj=10,mj=-2,Ex=e=>e==="*";function pj(e,t){let r=e.split("/"),s=r.length;return r.some(Ex)&&(s+=mj),t&&(s+=dj),r.filter(i=>!Ex(i)).reduce((i,l)=>i+(cj.test(l)?uj:l===""?fj:hj),s)}function gj(e,t){return e.length===t.length&&e.slice(0,-1).every((s,i)=>s===t[i])?e[e.length-1]-t[t.length-1]:0}function vj(e,t,r=!1){let{routesMeta:s}=e,i={},l="/",u=[];for(let d=0;d{if(b==="*"){let E=d[x]||"";u=l.slice(0,l.length-E.length).replace(/(.)\/+$/,"$1")}const _=d[x];return g&&!_?h[b]=void 0:h[b]=(_||"").replace(/%2F/g,"/"),h},{}),pathname:l,pathnameBase:u,pattern:e}}function yj(e,t=!1,r=!0){Lr(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let s=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(u,d,f,h,b)=>{if(s.push({paramName:d,isOptional:f!=null}),f){let g=b.charAt(h+u.length);return g&&g!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(s.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),s]}function bj(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return Lr(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ua(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,s=e.charAt(r);return s&&s!=="/"?null:e.slice(r)||"/"}var xj=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function wj(e,t="/"){let{pathname:r,search:s="",hash:i=""}=typeof e=="string"?lo(e):e,l;return r?(r=E_(r),r.startsWith("/")?l=Cx(r.substring(1),"/"):l=Cx(r,t)):l=t,{pathname:l,search:Ej(s),hash:Cj(i)}}function Cx(e,t){let r=sd(t).split("/");return e.split("/").forEach(i=>{i===".."?r.length>1&&r.pop():i!=="."&&r.push(i)}),r.length>1?r.join("/"):"/"}function km(e,t,r,s){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(s)}]. Please separate it out to the \`to.${r}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function _j(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function S_(e){let t=_j(e);return t.map((r,s)=>s===t.length-1?r.pathname:r.pathnameBase)}function ag(e,t,r,s=!1){let i;typeof e=="string"?i=lo(e):(i={...e},Tt(!i.pathname||!i.pathname.includes("?"),km("?","pathname","search",i)),Tt(!i.pathname||!i.pathname.includes("#"),km("#","pathname","hash",i)),Tt(!i.search||!i.search.includes("#"),km("#","search","hash",i)));let l=e===""||i.pathname==="",u=l?"/":i.pathname,d;if(u==null)d=r;else{let g=t.length-1;if(!s&&u.startsWith("..")){let x=u.split("/");for(;x[0]==="..";)x.shift(),g-=1;i.pathname=x.join("/")}d=g>=0?t[g]:"/"}let f=wj(i,d),h=u&&u!=="/"&&u.endsWith("/"),b=(l||u===".")&&r.endsWith("/");return!f.pathname.endsWith("/")&&(h||b)&&(f.pathname+="/"),f}var E_=e=>e.replace(/\/\/+/g,"/"),yr=e=>E_(e.join("/")),sd=e=>e.replace(/\/+$/,""),Sj=e=>sd(e).replace(/^\/*/,"/"),Ej=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,Cj=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,Tj=class{constructor(e,t,r,s=!1){this.status=e,this.statusText=t||"",this.internal=s,r instanceof Error?(this.data=r.toString(),this.error=r):this.data=r}};function Nj(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function Aj(e){let t=e.map(r=>r.route.path).filter(Boolean);return yr(t)||"/"}var C_=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function T_(e,t){let r=e;if(typeof r!="string"||!xj.test(r))return{absoluteURL:void 0,isExternal:!1,to:r};let s=r,i=!1;if(C_)try{let l=new URL(window.location.href),u=r.startsWith("//")?new URL(l.protocol+r):new URL(r),d=ua(u.pathname,t);u.origin===l.origin&&d!=null?r=d+u.search+u.hash:i=!0}catch{Lr(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:s,isExternal:i,to:r}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var N_=["POST","PUT","PATCH","DELETE"];new Set(N_);var Rj=["GET",...N_];new Set(Rj);var co=y.createContext(null);co.displayName="DataRouter";var kd=y.createContext(null);kd.displayName="DataRouterState";var A_=y.createContext(!1);function jj(){return y.useContext(A_)}var R_=y.createContext({isTransitioning:!1});R_.displayName="ViewTransition";var kj=y.createContext(new Map);kj.displayName="Fetchers";var Oj=y.createContext(null);Oj.displayName="Await";var ir=y.createContext(null);ir.displayName="Navigation";var Bl=y.createContext(null);Bl.displayName="Location";var ga=y.createContext({outlet:null,matches:[],isDataRoute:!1});ga.displayName="Route";var sg=y.createContext(null);sg.displayName="RouteError";var j_="REACT_ROUTER_ERROR",Mj="REDIRECT",Dj="ROUTE_ERROR_RESPONSE";function Pj(e){if(e.startsWith(`${j_}:${Mj}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function Lj(e){if(e.startsWith(`${j_}:${Dj}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new Tj(t.status,t.statusText,t.data)}catch{}}function zj(e,{relative:t}={}){Tt(Hl(),"useHref() may be used only in the context of a component.");let{basename:r,navigator:s}=y.useContext(ir),{hash:i,pathname:l,search:u}=ql(e,{relative:t}),d=l;return r!=="/"&&(d=l==="/"?r:yr([r,l])),s.createHref({pathname:d,search:u,hash:i})}function Hl(){return y.useContext(Bl)!=null}function va(){return Tt(Hl(),"useLocation() may be used only in the context of a component."),y.useContext(Bl).location}var k_="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function O_(e){y.useContext(ir).static||y.useLayoutEffect(e)}function Ij(){let{isDataRoute:e}=y.useContext(ga);return e?Jj():Uj()}function Uj(){Tt(Hl(),"useNavigate() may be used only in the context of a component.");let e=y.useContext(co),{basename:t,navigator:r}=y.useContext(ir),{matches:s}=y.useContext(ga),{pathname:i}=va(),l=JSON.stringify(S_(s)),u=y.useRef(!1);return O_(()=>{u.current=!0}),y.useCallback((f,h={})=>{if(Lr(u.current,k_),!u.current)return;if(typeof f=="number"){r.go(f);return}let b=ag(f,JSON.parse(l),i,h.relative==="path");e==null&&t!=="/"&&(b.pathname=b.pathname==="/"?t:yr([t,b.pathname])),(h.replace?r.replace:r.push)(b,h.state,h)},[t,r,l,i,e])}y.createContext(null);function ql(e,{relative:t}={}){let{matches:r}=y.useContext(ga),{pathname:s}=va(),i=JSON.stringify(S_(r));return y.useMemo(()=>ag(e,JSON.parse(i),s,t==="path"),[e,i,s,t])}function $j(e,t){return M_(e,t)}function M_(e,t,r){var T;Tt(Hl(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=y.useContext(ir),{matches:i}=y.useContext(ga),l=i[i.length-1],u=l?l.params:{},d=l?l.pathname:"/",f=l?l.pathnameBase:"/",h=l&&l.route;{let C=h&&h.path||"";P_(d,!h||C.endsWith("*")||C.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let b=va(),g;if(t){let C=typeof t=="string"?lo(t):t;Tt(f==="/"||((T=C.pathname)==null?void 0:T.startsWith(f)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${f}" but pathname "${C.pathname}" was given in the \`location\` prop.`),g=C}else g=b;let x=g.pathname||"/",_=x;if(f!=="/"){let C=f.replace(/^\//,"").split("/");_="/"+x.replace(/^\//,"").split("/").slice(C.length).join("/")}let E=r&&r.state.matches.length?r.state.matches.map(C=>Object.assign(C,{route:r.manifest[C.route.id]||C.route})):x_(e,{pathname:_});Lr(h||E!=null,`No routes matched location "${g.pathname}${g.search}${g.hash}" `),Lr(E==null||E[E.length-1].route.element!==void 0||E[E.length-1].route.Component!==void 0||E[E.length-1].route.lazy!==void 0,`Matched leaf route at location "${g.pathname}${g.search}${g.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let S=Gj(E&&E.map(C=>Object.assign({},C,{params:Object.assign({},u,C.params),pathname:yr([f,s.encodeLocation?s.encodeLocation(C.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:C.pathname]),pathnameBase:C.pathnameBase==="/"?f:yr([f,s.encodeLocation?s.encodeLocation(C.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:C.pathnameBase])})),i,r);return t&&S?y.createElement(Bl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...g},navigationType:"POP"}},S):S}function Bj(){let e=Xj(),t=Nj(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,s="rgba(200,200,200, 0.5)",i={padding:"0.5rem",backgroundColor:s},l={padding:"2px 4px",backgroundColor:s},u=null;return console.error("Error handled by React Router default ErrorBoundary:",e),u=y.createElement(y.Fragment,null,y.createElement("p",null,"💿 Hey developer 👋"),y.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",y.createElement("code",{style:l},"ErrorBoundary")," or"," ",y.createElement("code",{style:l},"errorElement")," prop on your route.")),y.createElement(y.Fragment,null,y.createElement("h2",null,"Unexpected Application Error!"),y.createElement("h3",{style:{fontStyle:"italic"}},t),r?y.createElement("pre",{style:i},r):null,u)}var Hj=y.createElement(Bj,null),D_=class extends y.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const r=Lj(e.digest);r&&(e=r)}let t=e!==void 0?y.createElement(ga.Provider,{value:this.props.routeContext},y.createElement(sg.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?y.createElement(qj,{error:e},t):t}};D_.contextType=A_;var Om=new WeakMap;function qj({children:e,error:t}){let{basename:r}=y.useContext(ir);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let s=Pj(t.digest);if(s){let i=Om.get(t);if(i)throw i;let l=T_(s.location,r);if(C_&&!Om.get(t))if(l.isExternal||s.reloadDocument)window.location.href=l.absoluteURL||l.to;else{const u=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(l.to,{replace:s.replace}));throw Om.set(t,u),u}return y.createElement("meta",{httpEquiv:"refresh",content:`0;url=${l.absoluteURL||l.to}`})}}return e}function Vj({routeContext:e,match:t,children:r}){let s=y.useContext(co);return s&&s.static&&s.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(s.staticContext._deepestRenderedBoundaryId=t.route.id),y.createElement(ga.Provider,{value:e},r)}function Gj(e,t=[],r){let s=r==null?void 0:r.state;if(e==null){if(!s)return null;if(s.errors)e=s.matches;else if(t.length===0&&!s.initialized&&s.matches.length>0)e=s.matches;else return null}let i=e,l=s==null?void 0:s.errors;if(l!=null){let b=i.findIndex(g=>g.route.id&&(l==null?void 0:l[g.route.id])!==void 0);Tt(b>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(l).join(",")}`),i=i.slice(0,Math.min(i.length,b+1))}let u=!1,d=-1;if(r&&s){u=s.renderFallback;for(let b=0;b=0?i=i.slice(0,d+1):i=[i[0]];break}}}}let f=r==null?void 0:r.onError,h=s&&f?(b,g)=>{var x,_;f(b,{location:s.location,params:((_=(x=s.matches)==null?void 0:x[0])==null?void 0:_.params)??{},pattern:Aj(s.matches),errorInfo:g})}:void 0;return i.reduceRight((b,g,x)=>{let _,E=!1,S=null,T=null;s&&(_=l&&g.route.id?l[g.route.id]:void 0,S=g.route.errorElement||Hj,u&&(d<0&&x===0?(P_("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),E=!0,T=null):d===x&&(E=!0,T=g.route.hydrateFallbackElement||null)));let C=t.concat(i.slice(0,x+1)),R=()=>{let A;return _?A=S:E?A=T:g.route.Component?A=y.createElement(g.route.Component,null):g.route.element?A=g.route.element:A=b,y.createElement(Vj,{match:g,routeContext:{outlet:b,matches:C,isDataRoute:s!=null},children:A})};return s&&(g.route.ErrorBoundary||g.route.errorElement||x===0)?y.createElement(D_,{location:s.location,revalidation:s.revalidation,component:S,error:_,children:R(),routeContext:{outlet:null,matches:C,isDataRoute:!0},onError:h}):R()},null)}function ig(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Fj(e){let t=y.useContext(co);return Tt(t,ig(e)),t}function Kj(e){let t=y.useContext(kd);return Tt(t,ig(e)),t}function Yj(e){let t=y.useContext(ga);return Tt(t,ig(e)),t}function og(e){let t=Yj(e),r=t.matches[t.matches.length-1];return Tt(r.route.id,`${e} can only be used on routes that contain a unique "id"`),r.route.id}function Wj(){return og("useRouteId")}function Xj(){var s;let e=y.useContext(sg),t=Kj("useRouteError"),r=og("useRouteError");return e!==void 0?e:(s=t.errors)==null?void 0:s[r]}function Jj(){let{router:e}=Fj("useNavigate"),t=og("useNavigate"),r=y.useRef(!1);return O_(()=>{r.current=!0}),y.useCallback(async(i,l={})=>{Lr(r.current,k_),r.current&&(typeof i=="number"?await e.navigate(i):await e.navigate(i,{fromRouteId:t,...l}))},[e,t])}var Tx={};function P_(e,t,r){!t&&!Tx[e]&&(Tx[e]=!0,Lr(!1,r))}y.memo(Qj);function Qj({routes:e,manifest:t,future:r,state:s,isStatic:i,onError:l}){return M_(e,void 0,{manifest:t,state:s,isStatic:i,onError:l})}function L_(e){Tt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Zj({basename:e="/",children:t=null,location:r,navigationType:s="POP",navigator:i,static:l=!1,useTransitions:u}){Tt(!Hl(),"You cannot render a inside another . You should never have more than one in your app.");let d=e.replace(/^\/*/,"/"),f=y.useMemo(()=>({basename:d,navigator:i,static:l,useTransitions:u,future:{}}),[d,i,l,u]);typeof r=="string"&&(r=lo(r));let{pathname:h="/",search:b="",hash:g="",state:x=null,key:_="default",mask:E}=r,S=y.useMemo(()=>{let T=ua(h,d);return T==null?null:{location:{pathname:T,search:b,hash:g,state:x,key:_,mask:E},navigationType:s}},[d,h,b,g,x,_,s,E]);return Lr(S!=null,` is not able to match the URL "${h}${b}${g}" because it does not start with the basename, so the won't render anything.`),S==null?null:y.createElement(ir.Provider,{value:f},y.createElement(Bl.Provider,{children:t,value:S}))}function ek({children:e,location:t}){return $j(hp(e),t)}function hp(e,t=[]){let r=[];return y.Children.forEach(e,(s,i)=>{if(!y.isValidElement(s))return;let l=[...t,i];if(s.type===y.Fragment){r.push.apply(r,hp(s.props.children,l));return}Tt(s.type===L_,`[${typeof s.type=="string"?s.type:s.type.name}] is not a component. All component children of must be a or `),Tt(!s.props.index||!s.props.children,"An index route cannot have child routes.");let u={id:s.props.id||l.join("-"),caseSensitive:s.props.caseSensitive,element:s.props.element,Component:s.props.Component,index:s.props.index,path:s.props.path,middleware:s.props.middleware,loader:s.props.loader,action:s.props.action,hydrateFallbackElement:s.props.hydrateFallbackElement,HydrateFallback:s.props.HydrateFallback,errorElement:s.props.errorElement,ErrorBoundary:s.props.ErrorBoundary,hasErrorBoundary:s.props.hasErrorBoundary===!0||s.props.ErrorBoundary!=null||s.props.errorElement!=null,shouldRevalidate:s.props.shouldRevalidate,handle:s.props.handle,lazy:s.props.lazy};s.props.children&&(u.children=hp(s.props.children,l)),r.push(u)}),r}var Ku="get",Yu="application/x-www-form-urlencoded";function Od(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function tk(e){return Od(e)&&e.tagName.toLowerCase()==="button"}function nk(e){return Od(e)&&e.tagName.toLowerCase()==="form"}function rk(e){return Od(e)&&e.tagName.toLowerCase()==="input"}function ak(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function sk(e,t){return e.button===0&&(!t||t==="_self")&&!ak(e)}var bu=null;function ik(){if(bu===null)try{new FormData(document.createElement("form"),0),bu=!1}catch{bu=!0}return bu}var ok=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Mm(e){return e!=null&&!ok.has(e)?(Lr(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Yu}"`),null):e}function lk(e,t){let r,s,i,l,u;if(nk(e)){let d=e.getAttribute("action");s=d?ua(d,t):null,r=e.getAttribute("method")||Ku,i=Mm(e.getAttribute("enctype"))||Yu,l=new FormData(e)}else if(tk(e)||rk(e)&&(e.type==="submit"||e.type==="image")){let d=e.form;if(d==null)throw new Error('Cannot submit a + + + + ); + } + return this.props.children; + } +} + +// ─── Auth Gate ─────────────────────────────────────────────────────────────── + +function AuthGate() { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ +
+

Cargando tablero…

+
+
+ ); + } + + if (!user) { + return ; + } + + return ( + + } /> + + ); +} + +// ─── App ───────────────────────────────────────────────────────────────────── + +export function App() { + return ( + + + + + + + + + ); +} diff --git a/src/components/GLMLogo.tsx b/src/components/GLMLogo.tsx new file mode 100644 index 0000000..c0e73a1 --- /dev/null +++ b/src/components/GLMLogo.tsx @@ -0,0 +1,24 @@ +import { cn } from "@/lib/utils"; + +type GLMLogoProps = { + size?: "sm" | "lg"; + showWordmark?: boolean; + className?: string; +}; + +export function GLMLogo({ size = "sm", className }: GLMLogoProps) { + const imageSize = size === "lg" ? "h-auto w-[168px]" : "h-auto w-[104px]"; + const logoNudge = size === "lg" ? "-translate-x-1" : ""; + + return ( +
+ GLM +
+ ); +} diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx new file mode 100644 index 0000000..e731610 --- /dev/null +++ b/src/components/LoginScreen.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/context/AuthContext"; +import { GLMLogo } from "@/components/GLMLogo"; + +export function LoginScreen() { + const { loginWithGoogle, error, loading } = useAuth(); + + return ( +
+
+ {/* Logo / marca */} +
+ +

Tablero CDC

+
+ + {/* Card */} +
+ + + {error && ( +
+

{error}

+
+ )} +
+ +

+ Acceso restringido a usuarios autorizados +

+
+
+ ); +} diff --git a/src/components/board/ColorPicker.tsx b/src/components/board/ColorPicker.tsx new file mode 100644 index 0000000..ea9732f --- /dev/null +++ b/src/components/board/ColorPicker.tsx @@ -0,0 +1,31 @@ +import { COLORS, type ColorId } from "@/data/lists"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export function ColorPicker({ + value, + onChange, +}: { + value: ColorId; + onChange: (c: ColorId) => void; +}) { + return ( +
+ {COLORS.map((c) => ( + + ))} +
+ ); +} diff --git a/src/components/board/LinkList.tsx b/src/components/board/LinkList.tsx new file mode 100644 index 0000000..20af392 --- /dev/null +++ b/src/components/board/LinkList.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { Link as LinkIcon, ExternalLink, Plus, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +interface Props { + label: string; + prefix?: string; // ej "AF" + links: string[]; + onChange: (next: string[]) => void; + readOnly?: boolean; + emptyText?: string; +} + +export function LinkList({ label, prefix, links, onChange, readOnly, emptyText }: Props) { + const [draft, setDraft] = useState(""); + + const add = () => { + const v = draft.trim(); + if (!v) return; + onChange([...links, v]); + setDraft(""); + }; + const update = (i: number, v: string) => onChange(links.map((x, idx) => (idx === i ? v : x))); + const remove = (i: number) => onChange(links.filter((_, idx) => idx !== i)); + + return ( +
+
+ + {label}{" "} + {prefix && ({prefix})} + + {links.length} +
+ + {links.length === 0 && ( +

{emptyText ?? "Sin links aún"}

+ )} + +
+ {links.map((l, i) => ( +
+
+ {prefix ?? "·"} + {i + 1} +
+ {readOnly ? ( + + + {l} + + + ) : ( + <> + update(i, e.target.value)} + placeholder="https://…" + className="h-9 text-sm" + /> + + + )} +
+ ))} +
+ + {!readOnly && ( +
+ setDraft(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), add())} + placeholder="Pegar link y Enter" + className="h-9 text-sm" + /> + +
+ )} +
+ ); +} diff --git a/src/components/board/ProjectCard.tsx b/src/components/board/ProjectCard.tsx new file mode 100644 index 0000000..1edea5f --- /dev/null +++ b/src/components/board/ProjectCard.tsx @@ -0,0 +1,116 @@ +import { MapPin, User, Tag, Link2, FileCheck2, CheckCircle2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { colorHex } from "@/lib/colors"; +import type { Project } from "@/lib/store"; +import { useAuth } from "@/context/AuthContext"; + +interface ProjectCardProps { + project: Project; + onClick: () => void; +} + +export function ProjectCard({ project, onClick }: ProjectCardProps) { + const closed = !!project.status; + const { isGerardo } = useAuth(); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onClick(); + } + }; + + return ( +
{ + if (!closed) e.currentTarget.style.boxShadow = "var(--shadow-hover)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = closed + ? "0 1px 2px oklch(0.5 0.02 280 / 0.04)" + : "var(--shadow-soft)"; + }} + > + {/* Pestaña de color con título */} +
+

+ {project.nombre || "Sin título"} +

+
+ + {/* Cuerpo blanco */} +
+ } label="Cliente" value={project.cliente} /> + } label="Marca" value={project.marca} /> + } label="País" value={project.bu} /> + } label="Solicita" value={project.solicitante} /> + +
+
+ {project.propuestaLinks.length > 0 && ( + + {project.propuestaLinks.length} + + )} + {project.afLinks.length > 0 && ( + + AF·{project.afLinks.length} + + )} +
+ +
+ {isGerardo && project.monto != null && ( + + ${project.monto.toLocaleString()} + + )} + {project.status && ( + + {project.status === "Aprobado" && } + {project.status} + + )} +
+
+
+
+ ); +} + +function Row({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) { + return ( +
+ {icon} + {label} + {value || "—"} +
+ ); +} diff --git a/src/components/board/ProjectDialog.tsx b/src/components/board/ProjectDialog.tsx new file mode 100644 index 0000000..8ac933e --- /dev/null +++ b/src/components/board/ProjectDialog.tsx @@ -0,0 +1,1251 @@ +import { useCallback, useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { + AlertCircle, + Loader2, + Lock, + DollarSign, + Calendar, + Trash2, + Search, + History, + MessageSquarePlus, + Clock3, +} from "lucide-react"; +import { ColorPicker } from "./ColorPicker"; +import { LinkList } from "./LinkList"; +import { SearchableSelect } from "./SearchableSelect"; +import { SearchableMultiSelect } from "./SearchableMultiSelect"; +import { ProjectPricingPanel } from "./ProjectPricingPanel"; +import { MESES, type Status } from "@/data/lists"; +import { useAppLists } from "@/lib/appLists"; +import { + addProjectActivityNote, + loadProjectActivities, + loadProjectPricingItems, + useProjects, + type Project, + type ProjectActivity, + type ProjectPricingItemInput, +} from "@/lib/store"; +import { useAuth } from "@/context/AuthContext"; +import { colorHex } from "@/lib/colors"; +import { cn } from "@/lib/utils"; +import { + canonicalOptionLabel, + dedupeOptions, + ensureOption, + normalizeOptionKey, +} from "@/lib/optionUtils"; +import { toast } from "sonner"; + +interface Props { + open: boolean; + onOpenChange: (v: boolean) => void; + project?: Project | null; // editar; vacío = crear + onDelete?: (project: Project) => void; + deleting?: boolean; +} + +const WALMART_CONNECT_CLIENT = "Walmart Connect WMC"; +const ACTIVITY_PAGE_SIZE = 15; + +function formatActivityDate(timestamp: number) { + return new Intl.DateTimeFormat("es-DO", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(new Date(timestamp)); +} + +function formatActivityDescription(description: string) { + return description + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); +} + +function getPricingTotal(items: ProjectPricingItemInput[]) { + return items.reduce((sum, item) => sum + Number(item.amount || 0), 0); +} + +function toPricingInputItems( + items: Awaited>, +): ProjectPricingItemInput[] { + return items.map((item) => ({ + id: item.id, + source: item.source, + category: item.category, + serviceName: item.serviceName, + workType: item.workType, + complexityLevel: item.complexityLevel, + referenceLabel: item.referenceLabel, + referenceMin: item.referenceMin, + referenceMax: item.referenceMax, + amount: item.amount, + description: item.description, + })); +} + +function hasManualInternalAmountOverride( + savedAmount: number | null | undefined, + items: ProjectPricingItemInput[], +) { + if (!items.length || savedAmount == null) return false; + + const cleanSavedAmount = Number(savedAmount); + + if (!Number.isFinite(cleanSavedAmount) || cleanSavedAmount <= 0) return false; + + const calculatedTotal = getPricingTotal(items); + + return Math.abs(cleanSavedAmount - calculatedTotal) > 0.01; +} + +function splitBrandValues(value: string | null | undefined, knownBrands: string[] = []) { + const cleanValue = canonicalOptionLabel(value); + + if (!cleanValue) return []; + + const knownByKey = new Map( + dedupeOptions(knownBrands).map((brand) => [normalizeOptionKey(brand), brand] as const), + ); + const exactKnownBrand = knownByKey.get(normalizeOptionKey(cleanValue)); + + if (exactKnownBrand) { + return [exactKnownBrand]; + } + + const parts = cleanValue + .split(/[,;]/) + .map((brand) => canonicalOptionLabel(brand)) + .filter(Boolean); + + if (parts.length <= 1) { + return dedupeOptions([exactKnownBrand || cleanValue]); + } + + const parsedBrands: string[] = []; + let index = 0; + + while (index < parts.length) { + let matchedBrand = ""; + let nextIndex = index + 1; + + for (let end = parts.length; end > index; end -= 1) { + const candidate = parts.slice(index, end).join(", "); + const knownBrand = knownByKey.get(normalizeOptionKey(candidate)); + + if (knownBrand) { + matchedBrand = knownBrand; + nextIndex = end; + break; + } + } + + parsedBrands.push(matchedBrand || parts[index]); + index = nextIndex; + } + + return dedupeOptions(parsedBrands); +} + +function joinBrandValues(values: string[]) { + return dedupeOptions(values.map((brand) => canonicalOptionLabel(brand)).filter(Boolean)).join( + ", ", + ); +} + +function splitCountryValues(value: string | null | undefined, knownCountries: string[] = []) { + const cleanValue = canonicalOptionLabel(value); + + if (!cleanValue) return []; + + const knownByKey = new Map( + dedupeOptions(knownCountries).map((country) => [normalizeOptionKey(country), country] as const), + ); + const exactKnownCountry = knownByKey.get(normalizeOptionKey(cleanValue)); + + if (exactKnownCountry) { + return [exactKnownCountry]; + } + + const parts = cleanValue + .split(/[,;]/) + .map((country) => canonicalOptionLabel(country)) + .filter(Boolean); + + if (parts.length <= 1) { + return dedupeOptions([exactKnownCountry || cleanValue]); + } + + return dedupeOptions( + parts.map((country) => knownByKey.get(normalizeOptionKey(country)) || country), + ); +} + +function joinCountryValues(values: string[]) { + return dedupeOptions(values.map((country) => canonicalOptionLabel(country)).filter(Boolean)).join( + ", ", + ); +} + +function isWalmartConnectClient(client: string | null | undefined) { + return normalizeOptionKey(client) === normalizeOptionKey(WALMART_CONNECT_CLIENT); +} + +const empty = (): Omit => { + const now = new Date(); + return { + nombre: "", + color: "blue", + cliente: "", + bu: "", + cm: "", + marca: "", + solicitante: "", + comentarios: "", + briefLink: "", + propuestaLinks: [], + afLinks: [], + status: "", + monto: null, + mes: MESES[now.getMonth()], + anio: now.getFullYear(), + }; +}; + +export function ProjectDialog({ open, onOpenChange, project, onDelete, deleting = false }: Props) { + const { add, update } = useProjects(); + const { lists, loading: listsLoading, error: listsError, refresh: refreshLists } = useAppLists(); + const { isGerardo, canDeleteProjects, canManageInternalPricing } = useAuth(); + const isEdit = !!project; + + const [form, setForm] = useState>(empty()); + const [selectedWmcCountries, setSelectedWmcCountries] = useState([]); + const [saving, setSaving] = useState(false); + const [wmcCountrySearch, setWmcCountrySearch] = useState(""); + const [savingStep, setSavingStep] = useState(null); + const [formError, setFormError] = useState(null); + const [activityItems, setActivityItems] = useState([]); + const [activityLoading, setActivityLoading] = useState(false); + const [activityError, setActivityError] = useState(null); + const [activityNote, setActivityNote] = useState(""); + const [activityHasMore, setActivityHasMore] = useState(false); + const [activityLoadingMore, setActivityLoadingMore] = useState(false); + const [addingActivityNote, setAddingActivityNote] = useState(false); + const [pricingItems, setPricingItems] = useState([]); + const [pricingLoading, setPricingLoading] = useState(false); + const [pricingError, setPricingError] = useState(null); + const [internalAmountManuallyEdited, setInternalAmountManuallyEdited] = useState(false); + + const selectedBrands = splitBrandValues(form.marca, lists.marcas); + const selectedCountries = splitCountryValues(form.bu, lists.bus); + const clienteOptions = dedupeOptions(ensureOption(lists.clientes, form.cliente)); + const marcaOptions = dedupeOptions([...lists.marcas, ...selectedBrands]); + const buOptions = dedupeOptions([...lists.bus, ...selectedCountries]); + const statusOptions = dedupeOptions(ensureOption(lists.status, form.status)) as Status[]; + const filteredWmcBuOptions = buOptions.filter((b) => + normalizeOptionKey(b).includes(normalizeOptionKey(wmcCountrySearch)), + ); + const pricingTotal = getPricingTotal(pricingItems); + + useEffect(() => { + setFormError(null); + setSavingStep(null); + + if (project) { + const { id: _i, createdAt: _c, ...rest } = project; + const cleanBu = canonicalOptionLabel(rest.bu); + setForm({ + ...rest, + bu: cleanBu, + }); + setSelectedWmcCountries(cleanBu ? [cleanBu] : []); + setWmcCountrySearch(""); + setActivityItems([]); + setActivityHasMore(false); + setActivityError(null); + setActivityNote(""); + setPricingItems([]); + setPricingError(null); + setInternalAmountManuallyEdited(false); + } else if (open) { + setForm(empty()); + setSelectedWmcCountries([]); + setWmcCountrySearch(""); + setActivityItems([]); + setActivityHasMore(false); + setActivityError(null); + setActivityNote(""); + setPricingItems([]); + setPricingError(null); + setInternalAmountManuallyEdited(false); + } + }, [project, open]); + + const refreshActivity = useCallback(async () => { + if (!project?.id) return; + + try { + setActivityLoading(true); + setActivityError(null); + const items = await loadProjectActivities(project.id, ACTIVITY_PAGE_SIZE + 1); + setActivityItems(items.slice(0, ACTIVITY_PAGE_SIZE)); + setActivityHasMore(items.length > ACTIVITY_PAGE_SIZE); + } catch (error) { + console.warn("No se pudo cargar el historial del proyecto:", error); + setActivityError( + "No se pudo cargar el historial. Revisa que el SQL de actividad esté ejecutado en Supabase.", + ); + setActivityItems([]); + setActivityHasMore(false); + } finally { + setActivityLoading(false); + } + }, [project?.id]); + + const loadMoreActivity = useCallback(async () => { + if (!project?.id || activityLoadingMore || !activityHasMore) return; + + try { + setActivityLoadingMore(true); + setActivityError(null); + const items = await loadProjectActivities( + project.id, + ACTIVITY_PAGE_SIZE + 1, + activityItems.length, + ); + setActivityItems((current) => [...current, ...items.slice(0, ACTIVITY_PAGE_SIZE)]); + setActivityHasMore(items.length > ACTIVITY_PAGE_SIZE); + } catch (error) { + console.warn("No se pudo cargar más historial del proyecto:", error); + setActivityError("No se pudo cargar más actividad. Inténtalo nuevamente."); + } finally { + setActivityLoadingMore(false); + } + }, [activityHasMore, activityItems.length, activityLoadingMore, project?.id]); + + useEffect(() => { + if (!open || !project?.id) return; + + void refreshActivity(); + }, [open, project?.id, refreshActivity]); + + useEffect(() => { + if (!open || !project?.id || !canManageInternalPricing) return; + + let cancelled = false; + + const loadPricing = async () => { + try { + setPricingLoading(true); + setPricingError(null); + const items = await loadProjectPricingItems(project.id); + const inputItems = toPricingInputItems(items); + + if (!cancelled) { + setInternalAmountManuallyEdited( + hasManualInternalAmountOverride(project.monto, inputItems), + ); + setPricingItems(inputItems); + } + } catch (error) { + console.warn("No se pudo cargar el tarifario del proyecto:", error); + + if (!cancelled) { + setPricingItems([]); + setPricingError( + "No se pudieron cargar los costos guardados. Revisa que el SQL del tarifario esté ejecutado en Supabase.", + ); + } + } finally { + if (!cancelled) { + setPricingLoading(false); + } + } + }; + + void loadPricing(); + + return () => { + cancelled = true; + }; + }, [canManageInternalPricing, open, project?.id, project?.monto]); + + const set = (k: K, v: (typeof form)[K]) => + setForm((f) => ({ ...f, [k]: v })); + + useEffect(() => { + if (!canManageInternalPricing || !pricingItems.length || internalAmountManuallyEdited) return; + + const nextAmount = Number(pricingTotal.toFixed(2)); + setForm((current) => + current.monto === nextAmount ? current : { ...current, monto: nextAmount }, + ); + }, [canManageInternalPricing, internalAmountManuallyEdited, pricingItems.length, pricingTotal]); + + const getCountryManager = (bu: string) => { + const direct = lists.buCm[bu]; + if (direct) return direct; + + const buKey = normalizeOptionKey(bu); + const match = Object.entries(lists.buCm).find(([key]) => normalizeOptionKey(key) === buKey); + + return match?.[1] ?? ""; + }; + + // BU → Country Manager automático desde Supabase tablero_cdc_app_lists, con fallback local. + const onCountries = (countries: string[]) => { + const cleanCountries = dedupeOptions( + countries.map((country) => canonicalOptionLabel(country)).filter(Boolean), + ); + const managers = dedupeOptions(cleanCountries.map(getCountryManager).filter(Boolean)); + + setForm((f) => ({ + ...f, + bu: joinCountryValues(cleanCountries), + cm: managers.join(", "), + })); + }; + + const onCliente = (cliente: string) => { + const cleanCliente = canonicalOptionLabel(cliente); + + setForm((f) => ({ + ...f, + cliente: cleanCliente, + ...(isWalmartConnectClient(cleanCliente) && !isEdit + ? { bu: "", cm: "" } + : f.cliente && isWalmartConnectClient(f.cliente) + ? { bu: "", cm: "" } + : {}), + })); + + if (!isWalmartConnectClient(cleanCliente)) { + setSelectedWmcCountries([]); + setWmcCountrySearch(""); + } + }; + + const isWalmartConnectCreate = !isEdit && isWalmartConnectClient(form.cliente); + const selectedCreateCountries = isWalmartConnectCreate ? selectedWmcCountries : selectedCountries; + const isMultiCountryCreate = !isEdit && selectedCreateCountries.length > 1; + + const toggleWmcCountry = (country: string, checked: boolean) => { + const cleanCountry = canonicalOptionLabel(country); + + setSelectedWmcCountries((current) => { + if (checked) { + return dedupeOptions([...current, cleanCountry]); + } + + return current.filter( + (item) => normalizeOptionKey(item) !== normalizeOptionKey(cleanCountry), + ); + }); + }; + + const selectedWmcCountryManagers = selectedWmcCountries.map((country) => ({ + country, + manager: getCountryManager(country), + })); + const selectedCountryManagers = selectedCountries.map((country) => ({ + country, + manager: getCountryManager(country), + })); + + const requiredOk = + form.nombre.trim() && + form.cliente && + form.marca && + form.solicitante.trim() && + (isWalmartConnectCreate ? selectedWmcCountries.length > 0 : selectedCountries.length > 0); + + const showSaveToast = (result: Awaited>) => { + if (result.sheetSyncStatus === "synced") { + toast.success(isEdit ? "Proyecto actualizado" : "Proyecto creado", { + description: "Se guardó y se sincronizó con el Google Sheet.", + }); + return; + } + + if (result.sheetSyncStatus === "skipped") { + toast.success(isEdit ? "Proyecto actualizado" : "Proyecto creado", { + description: "Se guardó. La sincronización con Google Sheet no está configurada.", + }); + return; + } + + toast.warning("Proyecto guardado con aviso", { + description: result.sheetSyncMessage, + }); + }; + + const addActivityNote = async () => { + if (!project?.id || !activityNote.trim() || addingActivityNote) return; + + try { + setAddingActivityNote(true); + const result = await addProjectActivityNote(project.id, activityNote); + set("comentarios", result.comments); + setActivityNote(""); + await refreshActivity(); + + if (result.sheetSyncStatus === "synced") { + toast.success("Comentario agregado al historial", { + description: "También se actualizó la columna Comentarios del Google Sheet.", + }); + } else if (result.sheetSyncStatus === "skipped") { + toast.success("Comentario agregado al historial", { + description: + "Se guardó como comentario interno. La sincronización con Sheet no está configurada.", + }); + } else { + toast.warning("Comentario agregado con aviso", { + description: result.sheetSyncMessage, + }); + } + } catch (error) { + console.error("Error agregando comentario al historial:", error); + const message = + error instanceof Error ? error.message : "No se pudo agregar el comentario al historial."; + toast.error("No se pudo agregar el comentario", { description: message }); + } finally { + setAddingActivityNote(false); + } + }; + + const submit = async () => { + if (!requiredOk || saving) return; + + const safeForm = isGerardo + ? form + : canManageInternalPricing + ? { + ...form, + status: project?.status ?? "", + } + : { + ...form, + status: project?.status ?? "", + monto: project?.monto ?? null, + }; + + try { + setSaving(true); + setFormError(null); + setSavingStep("Guardando y sincronizando con Google Sheet…"); + + if (isMultiCountryCreate) { + const countries = selectedCreateCountries; + const results = []; + + for (const [index, country] of countries.entries()) { + setSavingStep(`Guardando país ${index + 1} de ${countries.length}: ${country}…`); + + const countryProject = { + ...safeForm, + bu: country, + cm: getCountryManager(country), + id: crypto.randomUUID(), + createdAt: Date.now() + index, + }; + + results.push( + await add({ + ...countryProject, + pricingItems: canManageInternalPricing ? pricingItems : undefined, + }), + ); + } + + const failedSyncs = results.filter((result) => result.sheetSyncStatus !== "synced"); + + if (failedSyncs.length === 0) { + toast.success("Proyectos multipaís creados", { + description: `Se crearon ${countries.length} proyectos y ${countries.length} filas en Google Sheet. El Interno Cargado se aplicó por país.`, + }); + } else { + toast.warning("Proyectos multipaís creados con aviso", { + description: `${failedSyncs.length} de ${countries.length} sincronizaciones con Google Sheet requieren revisión.`, + }); + } + } else { + const result = + isEdit && project + ? await update(project.id, { + ...safeForm, + pricingItems: canManageInternalPricing ? pricingItems : undefined, + }) + : await add({ + ...safeForm, + id: crypto.randomUUID(), + createdAt: Date.now(), + pricingItems: canManageInternalPricing ? pricingItems : undefined, + }); + + showSaveToast(result); + } + + onOpenChange(false); + } catch (error) { + console.error("Error guardando proyecto:", error); + const message = + error instanceof Error + ? error.message + : "No se pudo guardar el proyecto. Revisa los permisos o intenta de nuevo."; + + setFormError(message); + toast.error("No se pudo guardar el proyecto", { + description: message, + }); + } finally { + setSaving(false); + setSavingStep(null); + } + }; + + const closed = !!form.status; + + return ( + + + {/* Pestaña de color superior */} +
+ +
+ +
+
+ + {isEdit ? form.nombre || "Sin título" : "Nuevo proyecto"} + + + + {form.mes} · {form.anio} + {closed && ( + + Cerrado + + )} + +
+
+
+ +
+ {/* Nombre */} +
+ + set("nombre", e.target.value)} + placeholder="Ej: Promoción Día de las Madres" + className="h-10" + /> +
+ + {/* Color */} +
+ + set("color", c)} /> +
+ + + + {(listsLoading || listsError) && ( +
+ + {listsLoading ? ( + <> + + Cargando listas… + + ) : ( + <> + + No se pudieron cargar las listas. Se usaron opciones locales. + + )} + + + {listsError && ( + + )} +
+ )} + + {/* Cliente */} +
+ + +
+ + {/* Marca */} +
+ + set("marca", joinBrandValues(values))} + placeholder="Seleccionar una o varias marcas…" + searchPlaceholder="Buscar marca…" + emptyText="No se encontró esa marca." + summaryLabel="marcas seleccionadas" + /> +
+ + {/* BU */} + {isWalmartConnectCreate ? ( +
+
+ + + {selectedWmcCountries.length} seleccionado(s) + +
+

+ Para Walmart Connect WMC puedes seleccionar varios países. La app creará un + proyecto y una fila del Sheet por cada país, como hasta ahora. +

+
+ + setWmcCountrySearch(e.target.value)} + placeholder="Buscar país…" + className="h-9 pl-9 bg-card" + /> +
+
+ {filteredWmcBuOptions.map((b) => { + const checked = selectedWmcCountries.some( + (country) => normalizeOptionKey(country) === normalizeOptionKey(b), + ); + const manager = getCountryManager(b); + + return ( + + ); + })} +
+
+ ) : ( +
+ + +

+ Si seleccionas varios países al crear, la app creará un proyecto y una fila del + Sheet por cada país. +

+
+ )} + + {/* CM autocompletado */} +
+ + {isWalmartConnectCreate ? ( +
+ {selectedWmcCountryManagers.length === 0 ? ( + Selecciona uno o más países. + ) : ( +
+ {selectedWmcCountryManagers.map(({ country, manager }) => ( + + {country}: {manager || "—"} + + ))} +
+ )} +
+ ) : ( +
+ {selectedCountryManagers.length === 0 ? ( + Selecciona uno o más países. + ) : ( +
+ {selectedCountryManagers.map(({ country, manager }) => ( + + {country}: {manager || "—"} + + ))} +
+ )} +
+ )} +
+ + {isMultiCountryCreate && canManageInternalPricing && ( +
+ Si seleccionas varios países, el Interno Cargado se aplicará por cada país. La app + no divide el monto automáticamente. +
+ )} + + {/* Solicitante */} +
+ + set("solicitante", e.target.value)} + placeholder="Nombre de quien solicita" + className="h-10" + /> +
+ + + + + + {/* Brief link único */} +
+ + set("briefLink", e.target.value)} + placeholder="https://drive.google.com/…" + className="h-10" + /> +
+ + {/* Propuestas */} +
+ set("propuestaLinks", v)} + emptyText="Aún no hay propuestas. Pega tantos links como necesites." + /> +
+ + {/* Artes Finales */} +
+ set("afLinks", v)} + emptyText="Aún no hay artes finales." + /> +
+ + + + {canManageInternalPricing && ( + + )} + + + + {/* Status: Director Creativo edita; el equipo solo visualiza */} +
+ + + {isGerardo ? ( + + ) : ( +
+ {form.status || "Sin estatus"} +
+ )} + + {!isGerardo && ( +

+ Solo el Director Creativo puede cambiar el estatus. +

+ )} +
+ + {/* Monto: SOLO visible para usuarios internos autorizados */} + {canManageInternalPricing && ( +
+ + { + setInternalAmountManuallyEdited(true); + set("monto", e.target.value === "" ? null : Number(e.target.value)); + }} + placeholder="0.00" + className="h-10" + /> +

+ Privado · solo usuarios internos autorizados. + {internalAmountManuallyEdited && pricingItems.length > 0 + ? " Monto ajustado manualmente; se respetará al guardar." + : pricingItems.length > 0 + ? " El total del tarifario se refleja aquí y puedes ajustarlo." + : ""} +

+
+ )} +
+ + {(formError || savingStep) && ( +
+ {formError ? ( + + ) : ( + + )} + {formError || savingStep} +
+ )} +
+ + +
+
+ {isEdit && project && canDeleteProjects && onDelete && ( + + )} +
+ +
+ + +
+
+
+ +
+ ); +} + +function ProjectActivityPanel({ + isEdit, + items, + loading, + error, + note, + hasMore, + loadingMore, + addingNote, + onNoteChange, + onAddNote, + onRefresh, + onLoadMore, +}: { + isEdit: boolean; + items: ProjectActivity[]; + loading: boolean; + error: string | null; + note: string; + hasMore: boolean; + loadingMore: boolean; + addingNote: boolean; + onNoteChange: (value: string) => void; + onAddNote: () => Promise; + onRefresh: () => Promise; + onLoadMore: () => Promise; +}) { + return ( +
+
+
+ + + +
+

Actividad del proyecto

+

+ Comentarios y movimientos recientes con fecha. +

+
+
+ + {isEdit && ( + + )} +
+ + {!isEdit ? ( +
+ El historial se activará cuando el proyecto sea creado. +
+ ) : ( +
+
+ +
+