From 7e5c955044dcfaa886a541551b132727fac0c105 Mon Sep 17 00:00:00 2001 From: Isaac Aracena Date: Sun, 16 Aug 2026 19:08:48 -0400 Subject: [PATCH] feat: actualizar carpeta dist probada en XAMPP y modulos de edicion de tiempo y persistencia UI --- .env.example | 1 + ...ION_TIEMPO_Y_PERSISTENCIA_UI_2026-08-16.md | 25 + METRICAS_Y_MULTIPAIS_2026-08-16.md | 22 + REGISTRO_HORAS_AUTOMATICO.md | 34 ++ TIEMPO_PROYECTO_IMPLEMENTACION.md | 11 + dist/assets/index-1r0QKSgX.js | 486 ++++++++++++++++ dist/assets/index-C83ItQ_0.css | 1 + dist/assets/index-Deer85MW.js | 486 ---------------- dist/assets/index-Npcv_R3U.css | 1 - dist/index.html | 4 +- src/components/board/ProjectCard.tsx | 2 +- src/components/board/ProjectDialog.tsx | 84 +-- src/components/board/ProjectTimeDialog.tsx | 205 +++++-- src/components/lists/ListManagerDialog.tsx | 117 +++- src/context/AuthContext.tsx | 4 + src/lib/appMetrics.ts | 47 ++ src/lib/store.ts | 41 ++ src/lib/timeSheetSync.ts | 90 +++ src/pages/BoardPage.tsx | 141 ++++- supabase_tablero_cdc_dashboard_metrics.sql | 519 ++++++++++++++++++ 20 files changed, 1714 insertions(+), 607 deletions(-) create mode 100644 EDICION_TIEMPO_Y_PERSISTENCIA_UI_2026-08-16.md create mode 100644 METRICAS_Y_MULTIPAIS_2026-08-16.md create mode 100644 REGISTRO_HORAS_AUTOMATICO.md create mode 100644 dist/assets/index-1r0QKSgX.js create mode 100644 dist/assets/index-C83ItQ_0.css delete mode 100644 dist/assets/index-Deer85MW.js delete mode 100644 dist/assets/index-Npcv_R3U.css create mode 100644 src/lib/appMetrics.ts create mode 100644 src/lib/timeSheetSync.ts create mode 100644 supabase_tablero_cdc_dashboard_metrics.sql diff --git a/.env.example b/.env.example index 5ec386d..6f06a3b 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,4 @@ 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" VITE_LISTS_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-listas-admin" +VITE_TIME_SHEET_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-registro-horas" diff --git a/EDICION_TIEMPO_Y_PERSISTENCIA_UI_2026-08-16.md b/EDICION_TIEMPO_Y_PERSISTENCIA_UI_2026-08-16.md new file mode 100644 index 0000000..8f8c72e --- /dev/null +++ b/EDICION_TIEMPO_Y_PERSISTENCIA_UI_2026-08-16.md @@ -0,0 +1,25 @@ +# Edición de Tiempo + Persistencia de Vista — 2026-08-16 + +## Edición de registros de tiempo + +- Cada registro de **Tiempo** ahora tiene acción **Editar** además de **Eliminar**. +- Editar carga en el formulario: país, tarea, horas/minutos, fecha y nota. +- **Guardar cambios** actualiza el mismo registro en `tablero_cdc_project_time_entries`; no crea un registro nuevo. +- Después de editar se vuelve a ejecutar la sincronización de Registro horas, por lo que el Excel se reconcilia usando el mismo `Tablero Time ID`. +- Se puede cancelar la edición sin modificar el registro. +- La política RLS existente ya permite `UPDATE`, por lo que no requiere SQL adicional. + +## Persistencia de la vista + +La app guarda el estado de navegación para evitar volver a la vista general si el navegador recarga o descarta una pestaña inactiva. + +Se preservan: + +- apartado: Todos / Activos / Cerrados / Nuevos / Rechazados; +- búsqueda; +- filtros avanzados; +- página actual por apartado; +- posición vertical de la página; +- apertura de Administración de Listas o Tarifario. + +El source utiliza `localStorage` y el `dist` incluye un módulo de compatibilidad para conservar la misma experiencia en la versión compilada/probada. diff --git a/METRICAS_Y_MULTIPAIS_2026-08-16.md b/METRICAS_Y_MULTIPAIS_2026-08-16.md new file mode 100644 index 0000000..4f5cbc1 --- /dev/null +++ b/METRICAS_Y_MULTIPAIS_2026-08-16.md @@ -0,0 +1,22 @@ +# Tablero CDC — Métricas + proyecto multipaís único + +## Cambios + +1. El monto interno visible en cada tarjeta se aumentó ligeramente para mejorar legibilidad. +2. Los proyectos creados con varios países ahora se guardan como **un solo proyecto**: + - `country`: países separados por coma. + - `country_manager`: CM correspondientes separados por coma. + - El workflow de Sync Proyecto existente hace un único upsert por `Project ID`, por lo que queda una sola fila en `PROYECTOS 2026`. + - Los filtros existentes continúan encontrando el proyecto por cualquiera de sus países porque soportan valores múltiples/`ilike`. +3. Se agregó `supabase_tablero_cdc_dashboard_metrics.sql` para: + - registrar sesiones/usuarios que abren la app; + - registrar creación y cambios de estado de proyectos; + - registrar recepción/aprobación/rechazo de briefs; + - hacer backfill de proyectos/briefs existentes; + - exponer la vista `tablero_cdc_dashboard_metrics` con métricas listas para un dashboard futuro. +4. El tracking de apertura es no bloqueante: si el SQL aún no se ha ejecutado, la app sigue funcionando normalmente. + +## Importante + +- No se fusionan automáticamente proyectos multipaís históricos que ya existen como varias filas/proyectos, porque cada uno tiene su propio `Project ID` y podría tener tiempo, links o tarifario asociado. +- El cambio aplica a nuevas creaciones multipaís. diff --git a/REGISTRO_HORAS_AUTOMATICO.md b/REGISTRO_HORAS_AUTOMATICO.md new file mode 100644 index 0000000..f8f2bdc --- /dev/null +++ b/REGISTRO_HORAS_AUTOMATICO.md @@ -0,0 +1,34 @@ +# Registro de horas automático — Tablero CDC + +## Objetivo +Evitar que el equipo registre el tiempo dos veces. El usuario registra el tiempo una sola vez desde **Tiempo** en Tablero CDC y la app solicita a n8n que reconcilie ese proyecto con la hoja **Registro horas** del archivo **CD_Latam_Registro Horas_2024**. + +## Mapeo + +| Registro horas | Origen | +|---|---| +| Fecha | Fecha del registro de Tiempo | +| Mes | Derivado de Fecha | +| Día | Derivado de Fecha | +| Proyecto | Proyecto de Tablero CDC | +| Cliente | Cliente del proyecto | +| Pais / BU | País seleccionado en Tiempo | +| CM | Lista País/BU → Country Manager | +| Trabajado | Usuario que registró el tiempo | +| Tipo de trabajo | Tarea registrada | +| Horas | Minutos / 60 | +| Comentarios | Nota del registro | +| # MES | Derivado de Fecha | +| AÑO | Derivado de Fecha | + +Las columnas **Fecha requerimiento, Fecha solicitada, Fecha entregada, Ejecutado / Ganado y Time (L:P)** no se inventan: quedan disponibles para el uso actual del equipo y, si alguien las completa manualmente en una fila sincronizada, el reconciliador las preserva. + +## Idempotencia y eliminación +El workflow utiliza dos columnas técnicas de la hoja: +- **T: Tablero Time ID** +- **U: Tablero Project ID** + +Así puede volver a ejecutar una sincronización sin duplicar filas y puede eliminar del Excel una fila cuyo registro de Tiempo ya fue eliminado de Supabase. + +## Recuperación automática +Al crear o eliminar un registro, la app solicita la sincronización inmediatamente. Además, cada vez que se abre el módulo **Tiempo** de un proyecto, se solicita una reconciliación silenciosa en segundo plano. Esto permite reparar un fallo temporal de Google Sheets sin volver a registrar las horas. diff --git a/TIEMPO_PROYECTO_IMPLEMENTACION.md b/TIEMPO_PROYECTO_IMPLEMENTACION.md index 03ed61e..9bd1c4d 100644 --- a/TIEMPO_PROYECTO_IMPLEMENTACION.md +++ b/TIEMPO_PROYECTO_IMPLEMENTACION.md @@ -36,3 +36,14 @@ public.tablero_cdc_project_time_entries ``` La duración se guarda en minutos enteros para evitar redondeos. + +## Sincronización automática con Registro horas (2026-08-15) + +Cada alta o eliminación en **Tiempo dedicado** solicita una reconciliación del proyecto con el workflow n8n `Tablero CDC - Sync Registro Horas`. + +- Fuente del tiempo: `tablero_cdc_project_time_entries` en Supabase. +- Destino: `CD_Latam_Registro Horas_2024` → hoja `Registro horas`. +- El workflow usa `Tablero Time ID` y `Tablero Project ID` en columnas T/U para evitar duplicados y detectar eliminaciones. +- Mapeo automático: Fecha, Mes, Día, Proyecto, Cliente, País/BU, CM, Trabajado, Tipo de trabajo, Horas, Comentarios, # MES y AÑO. +- Columnas L:P se dejan disponibles y se preservan si alguien las completa manualmente. +- Al volver a abrir el modal Tiempo, la app vuelve a solicitar la reconciliación en segundo plano, de modo que un fallo temporal de Google Sheets pueda repararse sin registrar las horas dos veces. diff --git a/dist/assets/index-1r0QKSgX.js b/dist/assets/index-1r0QKSgX.js new file mode 100644 index 0000000..3e6f194 --- /dev/null +++ b/dist/assets/index-1r0QKSgX.js @@ -0,0 +1,486 @@ +function CR(e,t){for(var n=0;ns[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 c of l.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&s(c)}).observe(document,{childList:!0,subtree:!0});function n(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=n(i);fetch(i.href,l)}})();function J_(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Hm={exports:{}},pl={};/** + * @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 Px;function NR(){if(Px)return pl;Px=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function n(s,i,l){var c=null;if(l!==void 0&&(c=""+l),i.key!==void 0&&(c=""+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:c,ref:i!==void 0?i:null,props:l}}return pl.Fragment=t,pl.jsx=n,pl.jsxs=n,pl}var zx;function jR(){return zx||(zx=1,Hm.exports=NR()),Hm.exports}var f=jR(),qm={exports:{}},Ge={};/** + * @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 Ix;function TR(){if(Ix)return Ge;Ix=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),b=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),x=Symbol.iterator;function w(R){return R===null||typeof R!="object"?null:(R=x&&R[x]||R["@@iterator"],typeof R=="function"?R:null)}var E={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,N={};function C(R,k,I){this.props=R,this.context=k,this.refs=N,this.updater=I||E}C.prototype.isReactComponent={},C.prototype.setState=function(R,k){if(typeof R!="object"&&typeof R!="function"&&R!=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,R,k,"setState")},C.prototype.forceUpdate=function(R){this.updater.enqueueForceUpdate(this,R,"forceUpdate")};function A(){}A.prototype=C.prototype;function T(R,k,I){this.props=R,this.context=k,this.refs=N,this.updater=I||E}var M=T.prototype=new A;M.constructor=T,S(M,C.prototype),M.isPureReactComponent=!0;var U=Array.isArray;function z(){}var L={H:null,A:null,T:null,S:null},B=Object.prototype.hasOwnProperty;function q(R,k,I){var X=I.ref;return{$$typeof:e,type:R,key:k,ref:X!==void 0?X:null,props:I}}function ue(R,k){return q(R.type,k,R.props)}function le(R){return typeof R=="object"&&R!==null&&R.$$typeof===e}function he(R){var k={"=":"=0",":":"=2"};return"$"+R.replace(/[=:]/g,function(I){return k[I]})}var de=/\/+/g;function ve(R,k){return typeof R=="object"&&R!==null&&R.key!=null?he(""+R.key):k.toString(36)}function pe(R){switch(R.status){case"fulfilled":return R.value;case"rejected":throw R.reason;default:switch(typeof R.status=="string"?R.then(z,z):(R.status="pending",R.then(function(k){R.status==="pending"&&(R.status="fulfilled",R.value=k)},function(k){R.status==="pending"&&(R.status="rejected",R.reason=k)})),R.status){case"fulfilled":return R.value;case"rejected":throw R.reason}}throw R}function O(R,k,I,X,H){var te=typeof R;(te==="undefined"||te==="boolean")&&(R=null);var ee=!1;if(R===null)ee=!0;else switch(te){case"bigint":case"string":case"number":ee=!0;break;case"object":switch(R.$$typeof){case e:case t:ee=!0;break;case b:return ee=R._init,O(ee(R._payload),k,I,X,H)}}if(ee)return H=H(R),ee=X===""?"."+ve(R,0):X,U(H)?(I="",ee!=null&&(I=ee.replace(de,"$&/")+"/"),O(H,k,I,"",function(Ne){return Ne})):H!=null&&(le(H)&&(H=ue(H,I+(H.key==null||R&&R.key===H.key?"":(""+H.key).replace(de,"$&/")+"/")+ee)),k.push(H)),1;ee=0;var Q=X===""?".":X+":";if(U(R))for(var re=0;re>>1,V=O[J];if(0>>1;Ji(I,W))Xi(H,I)?(O[J]=H,O[X]=W,J=X):(O[J]=I,O[k]=W,J=k);else if(Xi(H,W))O[J]=H,O[X]=W,J=X;else break e}}return K}function i(O,K){var W=O.sortIndex-K.sortIndex;return W!==0?W:O.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 c=Date,d=c.now();e.unstable_now=function(){return c.now()-d}}var h=[],m=[],b=1,g=null,x=3,w=!1,E=!1,S=!1,N=!1,C=typeof setTimeout=="function"?setTimeout:null,A=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function M(O){for(var K=n(m);K!==null;){if(K.callback===null)s(m);else if(K.startTime<=O)s(m),K.sortIndex=K.expirationTime,t(h,K);else break;K=n(m)}}function U(O){if(S=!1,M(O),!E)if(n(h)!==null)E=!0,z||(z=!0,he());else{var K=n(m);K!==null&&pe(U,K.startTime-O)}}var z=!1,L=-1,B=5,q=-1;function ue(){return N?!0:!(e.unstable_now()-qO&&ue());){var J=g.callback;if(typeof J=="function"){g.callback=null,x=g.priorityLevel;var V=J(g.expirationTime<=O);if(O=e.unstable_now(),typeof V=="function"){g.callback=V,M(O),K=!0;break t}g===n(h)&&s(h),M(O)}else s(h);g=n(h)}if(g!==null)K=!0;else{var R=n(m);R!==null&&pe(U,R.startTime-O),K=!1}}break e}finally{g=null,x=W,w=!1}K=void 0}}finally{K?he():z=!1}}}var he;if(typeof T=="function")he=function(){T(le)};else if(typeof MessageChannel<"u"){var de=new MessageChannel,ve=de.port2;de.port1.onmessage=le,he=function(){ve.postMessage(null)}}else he=function(){C(le,0)};function pe(O,K){L=C(function(){O(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(O){O.callback=null},e.unstable_forceFrameRate=function(O){0>O||125J?(O.sortIndex=W,t(m,O),n(h)===null&&O===n(m)&&(S?(A(L),L=-1):S=!0,pe(U,W-J))):(O.sortIndex=V,t(h,O),E||w||(E=!0,z||(z=!0,he()))),O},e.unstable_shouldYield=ue,e.unstable_wrapCallback=function(O){var K=x;return function(){var W=x;x=K;try{return O.apply(this,arguments)}finally{x=W}}}})(Gm)),Gm}var Bx;function RR(){return Bx||(Bx=1,Fm.exports=AR()),Fm.exports}var Km={exports:{}},mn={};/** + * @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 Hx;function kR(){if(Hx)return mn;Hx=1;var e=Vd();function t(h){var m="https://react.dev/errors/"+h;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),Km.exports=kR(),Km.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 Vx;function OR(){if(Vx)return gl;Vx=1;var e=RR(),t=Vd(),n=Q_();function s(r){var a="https://react.dev/errors/"+r;if(1V||(r.current=J[V],J[V]=null,V--)}function I(r,a){V++,J[V]=r.current,r.current=a}var X=R(null),H=R(null),te=R(null),ee=R(null);function Q(r,a){switch(I(te,a),I(H,r),I(X,null),a.nodeType){case 9:case 11:r=(r=a.documentElement)&&(r=r.namespaceURI)?sx(r):0;break;default:if(r=a.tagName,a=a.namespaceURI)a=sx(a),r=ix(a,r);else switch(r){case"svg":r=1;break;case"math":r=2;break;default:r=0}}k(X),I(X,r)}function re(){k(X),k(H),k(te)}function Ne(r){r.memoizedState!==null&&I(ee,r);var a=X.current,o=ix(a,r.type);a!==o&&(I(H,r),I(X,o))}function ze(r){H.current===r&&(k(X),k(H)),ee.current===r&&(k(ee),dl._currentValue=W)}var Oe,Qe;function Ze(r){if(Oe===void 0)try{throw Error()}catch(o){var a=o.stack.trim().match(/\n( *(at )?)/);Oe=a&&a[1]||"",Qe=-1)":-1p||D[u]!==G[p]){var ne=` +`+D[u].replace(" at new "," at ");return r.displayName&&ne.includes("")&&(ne=ne.replace("",r.displayName)),ne}while(1<=u&&0<=p);break}}}finally{vt=!1,Error.prepareStackTrace=o}return(o=r?r.displayName||r.name:"")?Ze(o):""}function se(r,a){switch(r.tag){case 26:case 27:case 5:return Ze(r.type);case 16:return Ze("Lazy");case 13:return r.child!==a&&a!==null?Ze("Suspense Fallback"):Ze("Suspense");case 19:return Ze("SuspenseList");case 0:case 15:return oe(r.type,!1);case 11:return oe(r.type.render,!1);case 1:return oe(r.type,!0);case 31:return Ze("Activity");default:return""}}function Re(r){try{var a="",o=null;do a+=se(r,o),o=r,r=r.return;while(r);return a}catch(u){return` +Error generating stack: `+u.message+` +`+u.stack}}var De=Object.prototype.hasOwnProperty,ht=e.unstable_scheduleCallback,Ye=e.unstable_cancelCallback,mt=e.unstable_shouldYield,zt=e.unstable_requestPaint,qe=e.unstable_now,ce=e.unstable_getCurrentPriorityLevel,Pe=e.unstable_ImmediatePriority,ct=e.unstable_UserBlockingPriority,tn=e.unstable_NormalPriority,Dt=e.unstable_LowPriority,Gt=e.unstable_IdlePriority,dn=e.log,gn=e.unstable_setDisableYieldValue,fn=null,Nt=null;function Kt(r){if(typeof dn=="function"&&gn(r),Nt&&typeof Nt.setStrictMode=="function")try{Nt.setStrictMode(fn,r)}catch{}}var Tt=Math.clz32?Math.clz32:It,cr=Math.log,ur=Math.LN2;function It(r){return r>>>=0,r===0?32:31-(cr(r)/ur|0)|0}var fe=256,Ue=262144,me=4194304;function $e(r){var a=r&42;if(a!==0)return a;switch(r&-r){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 r&261888;case 262144:case 524288:case 1048576:case 2097152:return r&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return r&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return r}}function Se(r,a,o){var u=r.pendingLanes;if(u===0)return 0;var p=0,y=r.suspendedLanes,_=r.pingedLanes;r=r.warmLanes;var j=u&134217727;return j!==0?(u=j&~y,u!==0?p=$e(u):(_&=j,_!==0?p=$e(_):o||(o=j&~r,o!==0&&(p=$e(o))))):(j=u&~y,j!==0?p=$e(j):_!==0?p=$e(_):o||(o=u&~r,o!==0&&(p=$e(o)))),p===0?0:a!==0&&a!==p&&(a&y)===0&&(y=p&-p,o=a&-a,y>=o||y===32&&(o&4194048)!==0)?a:p}function st(r,a){return(r.pendingLanes&~(r.suspendedLanes&~r.pingedLanes)&a)===0}function St(r,a){switch(r){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 hn(){var r=me;return me<<=1,(me&62914560)===0&&(me=4194304),r}function _a(r){for(var a=[],o=0;31>o;o++)a.push(r);return a}function At(r,a){r.pendingLanes|=a,a!==268435456&&(r.suspendedLanes=0,r.pingedLanes=0,r.warmLanes=0)}function wn(r,a,o,u,p,y){var _=r.pendingLanes;r.pendingLanes=o,r.suspendedLanes=0,r.pingedLanes=0,r.warmLanes=0,r.expiredLanes&=o,r.entangledLanes&=o,r.errorRecoveryDisabledLanes&=o,r.shellSuspendCounter=0;var j=r.entanglements,D=r.expirationTimes,G=r.hiddenUpdates;for(o=_&~o;0"u")return null;try{return r.activeElement||r.body}catch{return r.body}}var bT=/[\n"\\]/g;function Yn(r){return r.replace(bT,function(a){return"\\"+a.charCodeAt(0).toString(16)+" "})}function Pf(r,a,o,u,p,y,_,j){r.name="",_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"?r.type=_:r.removeAttribute("type"),a!=null?_==="number"?(a===0&&r.value===""||r.value!=a)&&(r.value=""+Kn(a)):r.value!==""+Kn(a)&&(r.value=""+Kn(a)):_!=="submit"&&_!=="reset"||r.removeAttribute("value"),a!=null?zf(r,_,Kn(a)):o!=null?zf(r,_,Kn(o)):u!=null&&r.removeAttribute("value"),p==null&&y!=null&&(r.defaultChecked=!!y),p!=null&&(r.checked=p&&typeof p!="function"&&typeof p!="symbol"),j!=null&&typeof j!="function"&&typeof j!="symbol"&&typeof j!="boolean"?r.name=""+Kn(j):r.removeAttribute("name")}function Zv(r,a,o,u,p,y,_,j){if(y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"&&(r.type=y),a!=null||o!=null){if(!(y!=="submit"&&y!=="reset"||a!=null)){Lf(r);return}o=o!=null?""+Kn(o):"",a=a!=null?""+Kn(a):o,j||a===r.value||(r.value=a),r.defaultValue=a}u=u??p,u=typeof u!="function"&&typeof u!="symbol"&&!!u,r.checked=j?r.checked:!!u,r.defaultChecked=!!u,_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"&&(r.name=_),Lf(r)}function zf(r,a,o){a==="number"&&gc(r.ownerDocument)===r||r.defaultValue===""+o||(r.defaultValue=""+o)}function si(r,a,o,u){if(r=r.options,a){a={};for(var p=0;p"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Hf=!1;if(Fr)try{var Ao={};Object.defineProperty(Ao,"passive",{get:function(){Hf=!0}}),window.addEventListener("test",Ao,Ao),window.removeEventListener("test",Ao,Ao)}catch{Hf=!1}var Ea=null,qf=null,yc=null;function iy(){if(yc)return yc;var r,a=qf,o=a.length,u,p="value"in Ea?Ea.value:Ea.textContent,y=p.length;for(r=0;r=Oo),fy=" ",hy=!1;function my(r,a){switch(r){case"keyup":return KT.indexOf(a.keyCode)!==-1;case"keydown":return a.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function py(r){return r=r.detail,typeof r=="object"&&"data"in r?r.data:null}var ci=!1;function WT(r,a){switch(r){case"compositionend":return py(a);case"keypress":return a.which!==32?null:(hy=!0,fy);case"textInput":return r=a.data,r===fy&&hy?null:r;default:return null}}function XT(r,a){if(ci)return r==="compositionend"||!Yf&&my(r,a)?(r=iy(),yc=qf=Ea=null,ci=!1,r):null;switch(r){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-r};r=u}e:{for(;o;){if(o.nextSibling){o=o.nextSibling;break e}o=o.parentNode}o=void 0}o=Sy(o)}}function Cy(r,a){return r&&a?r===a?!0:r&&r.nodeType===3?!1:a&&a.nodeType===3?Cy(r,a.parentNode):"contains"in r?r.contains(a):r.compareDocumentPosition?!!(r.compareDocumentPosition(a)&16):!1:!1}function Ny(r){r=r!=null&&r.ownerDocument!=null&&r.ownerDocument.defaultView!=null?r.ownerDocument.defaultView:window;for(var a=gc(r.document);a instanceof r.HTMLIFrameElement;){try{var o=typeof a.contentWindow.location.href=="string"}catch{o=!1}if(o)r=a.contentWindow;else break;a=gc(r.document)}return a}function Jf(r){var a=r&&r.nodeName&&r.nodeName.toLowerCase();return a&&(a==="input"&&(r.type==="text"||r.type==="search"||r.type==="tel"||r.type==="url"||r.type==="password")||a==="textarea"||r.contentEditable==="true")}var aA=Fr&&"documentMode"in document&&11>=document.documentMode,ui=null,Qf=null,Po=null,Zf=!1;function jy(r,a,o){var u=o.window===o?o.document:o.nodeType===9?o:o.ownerDocument;Zf||ui==null||ui!==gc(u)||(u=ui,"selectionStart"in u&&Jf(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),Po&&Lo(Po,u)||(Po=u,u=du(Qf,"onSelect"),0>=_,p-=_,Cr=1<<32-Tt(a)+p|o<We?(at=Me,Me=null):at=Me.sibling;var dt=Y($,Me,F[We],ae);if(dt===null){Me===null&&(Me=at);break}r&&Me&&dt.alternate===null&&a($,Me),P=y(dt,P,We),ut===null?Ie=dt:ut.sibling=dt,ut=dt,Me=at}if(We===F.length)return o($,Me),it&&Kr($,We),Ie;if(Me===null){for(;WeWe?(at=Me,Me=null):at=Me.sibling;var Ga=Y($,Me,dt.value,ae);if(Ga===null){Me===null&&(Me=at);break}r&&Me&&Ga.alternate===null&&a($,Me),P=y(Ga,P,We),ut===null?Ie=Ga:ut.sibling=Ga,ut=Ga,Me=at}if(dt.done)return o($,Me),it&&Kr($,We),Ie;if(Me===null){for(;!dt.done;We++,dt=F.next())dt=ie($,dt.value,ae),dt!==null&&(P=y(dt,P,We),ut===null?Ie=dt:ut.sibling=dt,ut=dt);return it&&Kr($,We),Ie}for(Me=u(Me);!dt.done;We++,dt=F.next())dt=Z(Me,$,We,dt.value,ae),dt!==null&&(r&&dt.alternate!==null&&Me.delete(dt.key===null?We:dt.key),P=y(dt,P,We),ut===null?Ie=dt:ut.sibling=dt,ut=dt);return r&&Me.forEach(function(ER){return a($,ER)}),it&&Kr($,We),Ie}function xt($,P,F,ae){if(typeof F=="object"&&F!==null&&F.type===S&&F.key===null&&(F=F.props.children),typeof F=="object"&&F!==null){switch(F.$$typeof){case w:e:{for(var Ie=F.key;P!==null;){if(P.key===Ie){if(Ie=F.type,Ie===S){if(P.tag===7){o($,P.sibling),ae=p(P,F.props.children),ae.return=$,$=ae;break e}}else if(P.elementType===Ie||typeof Ie=="object"&&Ie!==null&&Ie.$$typeof===B&&ws(Ie)===P.type){o($,P.sibling),ae=p(P,F.props),Ho(ae,F),ae.return=$,$=ae;break e}o($,P);break}else a($,P);P=P.sibling}F.type===S?(ae=gs(F.props.children,$.mode,ae,F.key),ae.return=$,$=ae):(ae=Tc(F.type,F.key,F.props,null,$.mode,ae),Ho(ae,F),ae.return=$,$=ae)}return _($);case E:e:{for(Ie=F.key;P!==null;){if(P.key===Ie)if(P.tag===4&&P.stateNode.containerInfo===F.containerInfo&&P.stateNode.implementation===F.implementation){o($,P.sibling),ae=p(P,F.children||[]),ae.return=$,$=ae;break e}else{o($,P);break}else a($,P);P=P.sibling}ae=ih(F,$.mode,ae),ae.return=$,$=ae}return _($);case B:return F=ws(F),xt($,P,F,ae)}if(pe(F))return je($,P,F,ae);if(he(F)){if(Ie=he(F),typeof Ie!="function")throw Error(s(150));return F=Ie.call(F),Be($,P,F,ae)}if(typeof F.then=="function")return xt($,P,Lc(F),ae);if(F.$$typeof===T)return xt($,P,kc($,F),ae);Pc($,F)}return typeof F=="string"&&F!==""||typeof F=="number"||typeof F=="bigint"?(F=""+F,P!==null&&P.tag===6?(o($,P.sibling),ae=p(P,F),ae.return=$,$=ae):(o($,P),ae=sh(F,$.mode,ae),ae.return=$,$=ae),_($)):o($,P)}return function($,P,F,ae){try{Bo=0;var Ie=xt($,P,F,ae);return wi=null,Ie}catch(Me){if(Me===xi||Me===Mc)throw Me;var ut=In(29,Me,null,$.mode);return ut.lanes=ae,ut.return=$,ut}finally{}}}var Ss=Xy(!0),Jy=Xy(!1),Aa=!1;function yh(r){r.updateQueue={baseState:r.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function bh(r,a){r=r.updateQueue,a.updateQueue===r&&(a.updateQueue={baseState:r.baseState,firstBaseUpdate:r.firstBaseUpdate,lastBaseUpdate:r.lastBaseUpdate,shared:r.shared,callbacks:null})}function Ra(r){return{lane:r,tag:0,payload:null,callback:null,next:null}}function ka(r,a,o){var u=r.updateQueue;if(u===null)return null;if(u=u.shared,(ft&2)!==0){var p=u.pending;return p===null?a.next=a:(a.next=p.next,p.next=a),u.pending=a,a=jc(r),Dy(r,null,o),a}return Nc(r,u,a,o),jc(r)}function qo(r,a,o){if(a=a.updateQueue,a!==null&&(a=a.shared,(o&4194048)!==0)){var u=a.lanes;u&=r.pendingLanes,o|=u,a.lanes=o,_n(r,o)}}function xh(r,a){var o=r.updateQueue,u=r.alternate;if(u!==null&&(u=u.updateQueue,o===u)){var p=null,y=null;if(o=o.firstBaseUpdate,o!==null){do{var _={lane:o.lane,tag:o.tag,payload:o.payload,callback:null,next:null};y===null?p=y=_:y=y.next=_,o=o.next}while(o!==null);y===null?p=y=a:y=y.next=a}else p=y=a;o={baseState:u.baseState,firstBaseUpdate:p,lastBaseUpdate:y,shared:u.shared,callbacks:u.callbacks},r.updateQueue=o;return}r=o.lastBaseUpdate,r===null?o.firstBaseUpdate=a:r.next=a,o.lastBaseUpdate=a}var wh=!1;function Vo(){if(wh){var r=bi;if(r!==null)throw r}}function Fo(r,a,o,u){wh=!1;var p=r.updateQueue;Aa=!1;var y=p.firstBaseUpdate,_=p.lastBaseUpdate,j=p.shared.pending;if(j!==null){p.shared.pending=null;var D=j,G=D.next;D.next=null,_===null?y=G:_.next=G,_=D;var ne=r.alternate;ne!==null&&(ne=ne.updateQueue,j=ne.lastBaseUpdate,j!==_&&(j===null?ne.firstBaseUpdate=G:j.next=G,ne.lastBaseUpdate=D))}if(y!==null){var ie=p.baseState;_=0,ne=G=D=null,j=y;do{var Y=j.lane&-536870913,Z=Y!==j.lane;if(Z?(rt&Y)===Y:(u&Y)===Y){Y!==0&&Y===yi&&(wh=!0),ne!==null&&(ne=ne.next={lane:0,tag:j.tag,payload:j.payload,callback:null,next:null});e:{var je=r,Be=j;Y=a;var xt=o;switch(Be.tag){case 1:if(je=Be.payload,typeof je=="function"){ie=je.call(xt,ie,Y);break e}ie=je;break e;case 3:je.flags=je.flags&-65537|128;case 0:if(je=Be.payload,Y=typeof je=="function"?je.call(xt,ie,Y):je,Y==null)break e;ie=g({},ie,Y);break e;case 2:Aa=!0}}Y=j.callback,Y!==null&&(r.flags|=64,Z&&(r.flags|=8192),Z=p.callbacks,Z===null?p.callbacks=[Y]:Z.push(Y))}else Z={lane:Y,tag:j.tag,payload:j.payload,callback:j.callback,next:null},ne===null?(G=ne=Z,D=ie):ne=ne.next=Z,_|=Y;if(j=j.next,j===null){if(j=p.shared.pending,j===null)break;Z=j,j=Z.next,Z.next=null,p.lastBaseUpdate=Z,p.shared.pending=null}}while(!0);ne===null&&(D=ie),p.baseState=D,p.firstBaseUpdate=G,p.lastBaseUpdate=ne,y===null&&(p.shared.lanes=0),Pa|=_,r.lanes=_,r.memoizedState=ie}}function Qy(r,a){if(typeof r!="function")throw Error(s(191,r));r.call(a)}function Zy(r,a){var o=r.callbacks;if(o!==null)for(r.callbacks=null,r=0;ry?y:8;var _=O.T,j={};O.T=j,$h(r,!1,a,o);try{var D=p(),G=O.S;if(G!==null&&G(j,D),D!==null&&typeof D=="object"&&typeof D.then=="function"){var ne=hA(D,u);Yo(r,a,ne,qn(r))}else Yo(r,a,u,qn(r))}catch(ie){Yo(r,a,{then:function(){},status:"rejected",reason:ie},qn())}finally{K.p=y,_!==null&&j.types!==null&&(_.types=j.types),O.T=_}}function bA(){}function Ih(r,a,o,u){if(r.tag!==5)throw Error(s(476));var p=kb(r).queue;Rb(r,p,a,W,o===null?bA:function(){return Ob(r),o(u)})}function kb(r){var a=r.memoizedState;if(a!==null)return a;a={memoizedState:W,baseState:W,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jr,lastRenderedState:W},next:null};var o={};return a.next={memoizedState:o,baseState:o,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jr,lastRenderedState:o},next:null},r.memoizedState=a,r=r.alternate,r!==null&&(r.memoizedState=a),a}function Ob(r){var a=kb(r);a.next===null&&(a=r.alternate.memoizedState),Yo(r,a.next.queue,{},qn())}function Uh(){return an(dl)}function Mb(){return Pt().memoizedState}function Db(){return Pt().memoizedState}function xA(r){for(var a=r.return;a!==null;){switch(a.tag){case 24:case 3:var o=qn();r=Ra(o);var u=ka(a,r,o);u!==null&&(Rn(u,a,o),qo(u,a,o)),a={cache:mh()},r.payload=a;return}a=a.return}}function wA(r,a,o){var u=qn();o={lane:u,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null},Gc(r)?Pb(a,o):(o=rh(r,a,o,u),o!==null&&(Rn(o,r,u),zb(o,a,u)))}function Lb(r,a,o){var u=qn();Yo(r,a,o,u)}function Yo(r,a,o,u){var p={lane:u,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null};if(Gc(r))Pb(a,p);else{var y=r.alternate;if(r.lanes===0&&(y===null||y.lanes===0)&&(y=a.lastRenderedReducer,y!==null))try{var _=a.lastRenderedState,j=y(_,o);if(p.hasEagerState=!0,p.eagerState=j,zn(j,_))return Nc(r,a,p,0),wt===null&&Cc(),!1}catch{}finally{}if(o=rh(r,a,p,u),o!==null)return Rn(o,r,u),zb(o,a,u),!0}return!1}function $h(r,a,o,u){if(u={lane:2,revertLane:ym(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},Gc(r)){if(a)throw Error(s(479))}else a=rh(r,o,u,2),a!==null&&Rn(a,r,2)}function Gc(r){var a=r.alternate;return r===Ke||a!==null&&a===Ke}function Pb(r,a){Si=Uc=!0;var o=r.pending;o===null?a.next=a:(a.next=o.next,o.next=a),r.pending=a}function zb(r,a,o){if((o&4194048)!==0){var u=a.lanes;u&=r.pendingLanes,o|=u,a.lanes=o,_n(r,o)}}var Wo={readContext:an,use:Hc,useCallback:kt,useContext:kt,useEffect:kt,useImperativeHandle:kt,useLayoutEffect:kt,useInsertionEffect:kt,useMemo:kt,useReducer:kt,useRef:kt,useState:kt,useDebugValue:kt,useDeferredValue:kt,useTransition:kt,useSyncExternalStore:kt,useId:kt,useHostTransitionStatus:kt,useFormState:kt,useActionState:kt,useOptimistic:kt,useMemoCache:kt,useCacheRefresh:kt};Wo.useEffectEvent=kt;var Ib={readContext:an,use:Hc,useCallback:function(r,a){return vn().memoizedState=[r,a===void 0?null:a],r},useContext:an,useEffect:wb,useImperativeHandle:function(r,a,o){o=o!=null?o.concat([r]):null,Vc(4194308,4,Cb.bind(null,a,r),o)},useLayoutEffect:function(r,a){return Vc(4194308,4,r,a)},useInsertionEffect:function(r,a){Vc(4,2,r,a)},useMemo:function(r,a){var o=vn();a=a===void 0?null:a;var u=r();if(Es){Kt(!0);try{r()}finally{Kt(!1)}}return o.memoizedState=[u,a],u},useReducer:function(r,a,o){var u=vn();if(o!==void 0){var p=o(a);if(Es){Kt(!0);try{o(a)}finally{Kt(!1)}}}else p=a;return u.memoizedState=u.baseState=p,r={pending:null,lanes:0,dispatch:null,lastRenderedReducer:r,lastRenderedState:p},u.queue=r,r=r.dispatch=wA.bind(null,Ke,r),[u.memoizedState,r]},useRef:function(r){var a=vn();return r={current:r},a.memoizedState=r},useState:function(r){r=Mh(r);var a=r.queue,o=Lb.bind(null,Ke,a);return a.dispatch=o,[r.memoizedState,o]},useDebugValue:Ph,useDeferredValue:function(r,a){var o=vn();return zh(o,r,a)},useTransition:function(){var r=Mh(!1);return r=Rb.bind(null,Ke,r.queue,!0,!1),vn().memoizedState=r,[!1,r]},useSyncExternalStore:function(r,a,o){var u=Ke,p=vn();if(it){if(o===void 0)throw Error(s(407));o=o()}else{if(o=a(),wt===null)throw Error(s(349));(rt&127)!==0||sb(u,a,o)}p.memoizedState=o;var y={value:o,getSnapshot:a};return p.queue=y,wb(ob.bind(null,u,y,r),[r]),u.flags|=2048,Ci(9,{destroy:void 0},ib.bind(null,u,y,o,a),null),o},useId:function(){var r=vn(),a=wt.identifierPrefix;if(it){var o=Nr,u=Cr;o=(u&~(1<<32-Tt(u)-1)).toString(32)+o,a="_"+a+"R_"+o,o=$c++,0<\/script>",y=y.removeChild(y.firstChild);break;case"select":y=typeof u.is=="string"?_.createElement("select",{is:u.is}):_.createElement("select"),u.multiple?y.multiple=!0:u.size&&(y.size=u.size);break;default:y=typeof u.is=="string"?_.createElement(p,{is:u.is}):_.createElement(p)}}y[nn]=a,y[En]=u;e:for(_=a.child;_!==null;){if(_.tag===5||_.tag===6)y.appendChild(_.stateNode);else if(_.tag!==4&&_.tag!==27&&_.child!==null){_.child.return=_,_=_.child;continue}if(_===a)break e;for(;_.sibling===null;){if(_.return===null||_.return===a)break e;_=_.return}_.sibling.return=_.return,_=_.sibling}a.stateNode=y;e:switch(on(y,p,u),p){case"button":case"input":case"select":case"textarea":u=!!u.autoFocus;break e;case"img":u=!0;break e;default:u=!1}u&&Zr(a)}}return Ct(a),em(a,a.type,r===null?null:r.memoizedProps,a.pendingProps,o),null;case 6:if(r&&a.stateNode!=null)r.memoizedProps!==u&&Zr(a);else{if(typeof u!="string"&&a.stateNode===null)throw Error(s(166));if(r=te.current,gi(a)){if(r=a.stateNode,o=a.memoizedProps,u=null,p=rn,p!==null)switch(p.tag){case 27:case 5:u=p.memoizedProps}r[nn]=a,r=!!(r.nodeValue===o||u!==null&&u.suppressHydrationWarning===!0||rx(r.nodeValue,o)),r||ja(a,!0)}else r=fu(r).createTextNode(u),r[nn]=a,a.stateNode=r}return Ct(a),null;case 31:if(o=a.memoizedState,r===null||r.memoizedState!==null){if(u=gi(a),o!==null){if(r===null){if(!u)throw Error(s(318));if(r=a.memoizedState,r=r!==null?r.dehydrated:null,!r)throw Error(s(557));r[nn]=a}else vs(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;Ct(a),r=!1}else o=uh(),r!==null&&r.memoizedState!==null&&(r.memoizedState.hydrationErrors=o),r=!0;if(!r)return a.flags&256?($n(a),a):($n(a),null);if((a.flags&128)!==0)throw Error(s(558))}return Ct(a),null;case 13:if(u=a.memoizedState,r===null||r.memoizedState!==null&&r.memoizedState.dehydrated!==null){if(p=gi(a),u!==null&&u.dehydrated!==null){if(r===null){if(!p)throw Error(s(318));if(p=a.memoizedState,p=p!==null?p.dehydrated:null,!p)throw Error(s(317));p[nn]=a}else vs(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;Ct(a),p=!1}else p=uh(),r!==null&&r.memoizedState!==null&&(r.memoizedState.hydrationErrors=p),p=!0;if(!p)return a.flags&256?($n(a),a):($n(a),null)}return $n(a),(a.flags&128)!==0?(a.lanes=o,a):(o=u!==null,r=r!==null&&r.memoizedState!==null,o&&(u=a.child,p=null,u.alternate!==null&&u.alternate.memoizedState!==null&&u.alternate.memoizedState.cachePool!==null&&(p=u.alternate.memoizedState.cachePool.pool),y=null,u.memoizedState!==null&&u.memoizedState.cachePool!==null&&(y=u.memoizedState.cachePool.pool),y!==p&&(u.flags|=2048)),o!==r&&o&&(a.child.flags|=8192),Jc(a,a.updateQueue),Ct(a),null);case 4:return re(),r===null&&_m(a.stateNode.containerInfo),Ct(a),null;case 10:return Wr(a.type),Ct(a),null;case 19:if(k(Lt),u=a.memoizedState,u===null)return Ct(a),null;if(p=(a.flags&128)!==0,y=u.rendering,y===null)if(p)Jo(u,!1);else{if(Ot!==0||r!==null&&(r.flags&128)!==0)for(r=a.child;r!==null;){if(y=Ic(r),y!==null){for(a.flags|=128,Jo(u,!1),r=y.updateQueue,a.updateQueue=r,Jc(a,r),a.subtreeFlags=0,r=o,o=a.child;o!==null;)Ly(o,r),o=o.sibling;return I(Lt,Lt.current&1|2),it&&Kr(a,u.treeForkCount),a.child}r=r.sibling}u.tail!==null&&qe()>nu&&(a.flags|=128,p=!0,Jo(u,!1),a.lanes=4194304)}else{if(!p)if(r=Ic(y),r!==null){if(a.flags|=128,p=!0,r=r.updateQueue,a.updateQueue=r,Jc(a,r),Jo(u,!0),u.tail===null&&u.tailMode==="hidden"&&!y.alternate&&!it)return Ct(a),null}else 2*qe()-u.renderingStartTime>nu&&o!==536870912&&(a.flags|=128,p=!0,Jo(u,!1),a.lanes=4194304);u.isBackwards?(y.sibling=a.child,a.child=y):(r=u.last,r!==null?r.sibling=y:a.child=y,u.last=y)}return u.tail!==null?(r=u.tail,u.rendering=r,u.tail=r.sibling,u.renderingStartTime=qe(),r.sibling=null,o=Lt.current,I(Lt,p?o&1|2:o&1),it&&Kr(a,u.treeForkCount),r):(Ct(a),null);case 22:case 23:return $n(a),Sh(),u=a.memoizedState!==null,r!==null?r.memoizedState!==null!==u&&(a.flags|=8192):u&&(a.flags|=8192),u?(o&536870912)!==0&&(a.flags&128)===0&&(Ct(a),a.subtreeFlags&6&&(a.flags|=8192)):Ct(a),o=a.updateQueue,o!==null&&Jc(a,o.retryQueue),o=null,r!==null&&r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==o&&(a.flags|=2048),r!==null&&k(xs),null;case 24:return o=null,r!==null&&(o=r.memoizedState.cache),a.memoizedState.cache!==o&&(a.flags|=2048),Wr(Ut),Ct(a),null;case 25:return null;case 30:return null}throw Error(s(156,a.tag))}function NA(r,a){switch(lh(a),a.tag){case 1:return r=a.flags,r&65536?(a.flags=r&-65537|128,a):null;case 3:return Wr(Ut),re(),r=a.flags,(r&65536)!==0&&(r&128)===0?(a.flags=r&-65537|128,a):null;case 26:case 27:case 5:return ze(a),null;case 31:if(a.memoizedState!==null){if($n(a),a.alternate===null)throw Error(s(340));vs()}return r=a.flags,r&65536?(a.flags=r&-65537|128,a):null;case 13:if($n(a),r=a.memoizedState,r!==null&&r.dehydrated!==null){if(a.alternate===null)throw Error(s(340));vs()}return r=a.flags,r&65536?(a.flags=r&-65537|128,a):null;case 19:return k(Lt),null;case 4:return re(),null;case 10:return Wr(a.type),null;case 22:case 23:return $n(a),Sh(),r!==null&&k(xs),r=a.flags,r&65536?(a.flags=r&-65537|128,a):null;case 24:return Wr(Ut),null;case 25:return null;default:return null}}function l0(r,a){switch(lh(a),a.tag){case 3:Wr(Ut),re();break;case 26:case 27:case 5:ze(a);break;case 4:re();break;case 31:a.memoizedState!==null&&$n(a);break;case 13:$n(a);break;case 19:k(Lt);break;case 10:Wr(a.type);break;case 22:case 23:$n(a),Sh(),r!==null&&k(xs);break;case 24:Wr(Ut)}}function Qo(r,a){try{var o=a.updateQueue,u=o!==null?o.lastEffect:null;if(u!==null){var p=u.next;o=p;do{if((o.tag&r)===r){u=void 0;var y=o.create,_=o.inst;u=y(),_.destroy=u}o=o.next}while(o!==p)}}catch(j){gt(a,a.return,j)}}function Da(r,a,o){try{var u=a.updateQueue,p=u!==null?u.lastEffect:null;if(p!==null){var y=p.next;u=y;do{if((u.tag&r)===r){var _=u.inst,j=_.destroy;if(j!==void 0){_.destroy=void 0,p=a;var D=o,G=j;try{G()}catch(ne){gt(p,D,ne)}}}u=u.next}while(u!==y)}}catch(ne){gt(a,a.return,ne)}}function c0(r){var a=r.updateQueue;if(a!==null){var o=r.stateNode;try{Zy(a,o)}catch(u){gt(r,r.return,u)}}}function u0(r,a,o){o.props=Cs(r.type,r.memoizedProps),o.state=r.memoizedState;try{o.componentWillUnmount()}catch(u){gt(r,a,u)}}function Zo(r,a){try{var o=r.ref;if(o!==null){switch(r.tag){case 26:case 27:case 5:var u=r.stateNode;break;case 30:u=r.stateNode;break;default:u=r.stateNode}typeof o=="function"?r.refCleanup=o(u):o.current=u}}catch(p){gt(r,a,p)}}function jr(r,a){var o=r.ref,u=r.refCleanup;if(o!==null)if(typeof u=="function")try{u()}catch(p){gt(r,a,p)}finally{r.refCleanup=null,r=r.alternate,r!=null&&(r.refCleanup=null)}else if(typeof o=="function")try{o(null)}catch(p){gt(r,a,p)}else o.current=null}function d0(r){var a=r.type,o=r.memoizedProps,u=r.stateNode;try{e:switch(a){case"button":case"input":case"select":case"textarea":o.autoFocus&&u.focus();break e;case"img":o.src?u.src=o.src:o.srcSet&&(u.srcset=o.srcSet)}}catch(p){gt(r,r.return,p)}}function tm(r,a,o){try{var u=r.stateNode;YA(u,r.type,o,a),u[En]=a}catch(p){gt(r,r.return,p)}}function f0(r){return r.tag===5||r.tag===3||r.tag===26||r.tag===27&&Ba(r.type)||r.tag===4}function nm(r){e:for(;;){for(;r.sibling===null;){if(r.return===null||f0(r.return))return null;r=r.return}for(r.sibling.return=r.return,r=r.sibling;r.tag!==5&&r.tag!==6&&r.tag!==18;){if(r.tag===27&&Ba(r.type)||r.flags&2||r.child===null||r.tag===4)continue e;r.child.return=r,r=r.child}if(!(r.flags&2))return r.stateNode}}function rm(r,a,o){var u=r.tag;if(u===5||u===6)r=r.stateNode,a?(o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o).insertBefore(r,a):(a=o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o,a.appendChild(r),o=o._reactRootContainer,o!=null||a.onclick!==null||(a.onclick=Vr));else if(u!==4&&(u===27&&Ba(r.type)&&(o=r.stateNode,a=null),r=r.child,r!==null))for(rm(r,a,o),r=r.sibling;r!==null;)rm(r,a,o),r=r.sibling}function Qc(r,a,o){var u=r.tag;if(u===5||u===6)r=r.stateNode,a?o.insertBefore(r,a):o.appendChild(r);else if(u!==4&&(u===27&&Ba(r.type)&&(o=r.stateNode),r=r.child,r!==null))for(Qc(r,a,o),r=r.sibling;r!==null;)Qc(r,a,o),r=r.sibling}function h0(r){var a=r.stateNode,o=r.memoizedProps;try{for(var u=r.type,p=a.attributes;p.length;)a.removeAttributeNode(p[0]);on(a,u,o),a[nn]=r,a[En]=o}catch(y){gt(r,r.return,y)}}var ea=!1,Ht=!1,am=!1,m0=typeof WeakSet=="function"?WeakSet:Set,Qt=null;function jA(r,a){if(r=r.containerInfo,Cm=bu,r=Ny(r),Jf(r)){if("selectionStart"in r)var o={start:r.selectionStart,end:r.selectionEnd};else e:{o=(o=r.ownerDocument)&&o.defaultView||window;var u=o.getSelection&&o.getSelection();if(u&&u.rangeCount!==0){o=u.anchorNode;var p=u.anchorOffset,y=u.focusNode;u=u.focusOffset;try{o.nodeType,y.nodeType}catch{o=null;break e}var _=0,j=-1,D=-1,G=0,ne=0,ie=r,Y=null;t:for(;;){for(var Z;ie!==o||p!==0&&ie.nodeType!==3||(j=_+p),ie!==y||u!==0&&ie.nodeType!==3||(D=_+u),ie.nodeType===3&&(_+=ie.nodeValue.length),(Z=ie.firstChild)!==null;)Y=ie,ie=Z;for(;;){if(ie===r)break t;if(Y===o&&++G===p&&(j=_),Y===y&&++ne===u&&(D=_),(Z=ie.nextSibling)!==null)break;ie=Y,Y=ie.parentNode}ie=Z}o=j===-1||D===-1?null:{start:j,end:D}}else o=null}o=o||{start:0,end:0}}else o=null;for(Nm={focusedElem:r,selectionRange:o},bu=!1,Qt=a;Qt!==null;)if(a=Qt,r=a.child,(a.subtreeFlags&1028)!==0&&r!==null)r.return=a,Qt=r;else for(;Qt!==null;){switch(a=Qt,y=a.alternate,r=a.flags,a.tag){case 0:if((r&4)!==0&&(r=a.updateQueue,r=r!==null?r.events:null,r!==null))for(o=0;o title"))),on(y,u,o),y[nn]=r,Jt(y),u=y;break e;case"link":var _=xx("link","href",p).get(u+(o.href||""));if(_){for(var j=0;j<_.length;j++)if(y=_[j],y.getAttribute("href")===(o.href==null||o.href===""?null:o.href)&&y.getAttribute("rel")===(o.rel==null?null:o.rel)&&y.getAttribute("title")===(o.title==null?null:o.title)&&y.getAttribute("crossorigin")===(o.crossOrigin==null?null:o.crossOrigin)){_.splice(j,1);break t}}y=p.createElement(u),on(y,u,o),p.head.appendChild(y);break;case"meta":if(_=xx("meta","content",p).get(u+(o.content||""))){for(j=0;j<_.length;j++)if(y=_[j],y.getAttribute("content")===(o.content==null?null:""+o.content)&&y.getAttribute("name")===(o.name==null?null:o.name)&&y.getAttribute("property")===(o.property==null?null:o.property)&&y.getAttribute("http-equiv")===(o.httpEquiv==null?null:o.httpEquiv)&&y.getAttribute("charset")===(o.charSet==null?null:o.charSet)){_.splice(j,1);break t}}y=p.createElement(u),on(y,u,o),p.head.appendChild(y);break;default:throw Error(s(468,u))}y[nn]=r,Jt(y),u=y}r.stateNode=u}else wx(p,r.type,r.stateNode);else r.stateNode=bx(p,u,r.memoizedProps);else y!==u?(y===null?o.stateNode!==null&&(o=o.stateNode,o.parentNode.removeChild(o)):y.count--,u===null?wx(p,r.type,r.stateNode):bx(p,u,r.memoizedProps)):u===null&&r.stateNode!==null&&tm(r,r.memoizedProps,o.memoizedProps)}break;case 27:jn(a,r),Tn(r),u&512&&(Ht||o===null||jr(o,o.return)),o!==null&&u&4&&tm(r,r.memoizedProps,o.memoizedProps);break;case 5:if(jn(a,r),Tn(r),u&512&&(Ht||o===null||jr(o,o.return)),r.flags&32){p=r.stateNode;try{ii(p,"")}catch(je){gt(r,r.return,je)}}u&4&&r.stateNode!=null&&(p=r.memoizedProps,tm(r,p,o!==null?o.memoizedProps:p)),u&1024&&(am=!0);break;case 6:if(jn(a,r),Tn(r),u&4){if(r.stateNode===null)throw Error(s(162));u=r.memoizedProps,o=r.stateNode;try{o.nodeValue=u}catch(je){gt(r,r.return,je)}}break;case 3:if(pu=null,p=hr,hr=hu(a.containerInfo),jn(a,r),hr=p,Tn(r),u&4&&o!==null&&o.memoizedState.isDehydrated)try{zi(a.containerInfo)}catch(je){gt(r,r.return,je)}am&&(am=!1,w0(r));break;case 4:u=hr,hr=hu(r.stateNode.containerInfo),jn(a,r),Tn(r),hr=u;break;case 12:jn(a,r),Tn(r);break;case 31:jn(a,r),Tn(r),u&4&&(u=r.updateQueue,u!==null&&(r.updateQueue=null,Zc(r,u)));break;case 13:jn(a,r),Tn(r),r.child.flags&8192&&r.memoizedState!==null!=(o!==null&&o.memoizedState!==null)&&(tu=qe()),u&4&&(u=r.updateQueue,u!==null&&(r.updateQueue=null,Zc(r,u)));break;case 22:p=r.memoizedState!==null;var D=o!==null&&o.memoizedState!==null,G=ea,ne=Ht;if(ea=G||p,Ht=ne||D,jn(a,r),Ht=ne,ea=G,Tn(r),u&8192)e:for(a=r.stateNode,a._visibility=p?a._visibility&-2:a._visibility|1,p&&(o===null||D||ea||Ht||Ns(r)),o=null,a=r;;){if(a.tag===5||a.tag===26){if(o===null){D=o=a;try{if(y=D.stateNode,p)_=y.style,typeof _.setProperty=="function"?_.setProperty("display","none","important"):_.display="none";else{j=D.stateNode;var ie=D.memoizedProps.style,Y=ie!=null&&ie.hasOwnProperty("display")?ie.display:null;j.style.display=Y==null||typeof Y=="boolean"?"":(""+Y).trim()}}catch(je){gt(D,D.return,je)}}}else if(a.tag===6){if(o===null){D=a;try{D.stateNode.nodeValue=p?"":D.memoizedProps}catch(je){gt(D,D.return,je)}}}else if(a.tag===18){if(o===null){D=a;try{var Z=D.stateNode;p?ux(Z,!0):ux(D.stateNode,!1)}catch(je){gt(D,D.return,je)}}}else if((a.tag!==22&&a.tag!==23||a.memoizedState===null||a===r)&&a.child!==null){a.child.return=a,a=a.child;continue}if(a===r)break e;for(;a.sibling===null;){if(a.return===null||a.return===r)break e;o===a&&(o=null),a=a.return}o===a&&(o=null),a.sibling.return=a.return,a=a.sibling}u&4&&(u=r.updateQueue,u!==null&&(o=u.retryQueue,o!==null&&(u.retryQueue=null,Zc(r,o))));break;case 19:jn(a,r),Tn(r),u&4&&(u=r.updateQueue,u!==null&&(r.updateQueue=null,Zc(r,u)));break;case 30:break;case 21:break;default:jn(a,r),Tn(r)}}function Tn(r){var a=r.flags;if(a&2){try{for(var o,u=r.return;u!==null;){if(f0(u)){o=u;break}u=u.return}if(o==null)throw Error(s(160));switch(o.tag){case 27:var p=o.stateNode,y=nm(r);Qc(r,y,p);break;case 5:var _=o.stateNode;o.flags&32&&(ii(_,""),o.flags&=-33);var j=nm(r);Qc(r,j,_);break;case 3:case 4:var D=o.stateNode.containerInfo,G=nm(r);rm(r,G,D);break;default:throw Error(s(161))}}catch(ne){gt(r,r.return,ne)}r.flags&=-3}a&4096&&(r.flags&=-4097)}function w0(r){if(r.subtreeFlags&1024)for(r=r.child;r!==null;){var a=r;w0(a),a.tag===5&&a.flags&1024&&a.stateNode.reset(),r=r.sibling}}function na(r,a){if(a.subtreeFlags&8772)for(a=a.child;a!==null;)p0(r,a.alternate,a),a=a.sibling}function Ns(r){for(r=r.child;r!==null;){var a=r;switch(a.tag){case 0:case 11:case 14:case 15:Da(4,a,a.return),Ns(a);break;case 1:jr(a,a.return);var o=a.stateNode;typeof o.componentWillUnmount=="function"&&u0(a,a.return,o),Ns(a);break;case 27:ll(a.stateNode);case 26:case 5:jr(a,a.return),Ns(a);break;case 22:a.memoizedState===null&&Ns(a);break;case 30:Ns(a);break;default:Ns(a)}r=r.sibling}}function ra(r,a,o){for(o=o&&(a.subtreeFlags&8772)!==0,a=a.child;a!==null;){var u=a.alternate,p=r,y=a,_=y.flags;switch(y.tag){case 0:case 11:case 15:ra(p,y,o),Qo(4,y);break;case 1:if(ra(p,y,o),u=y,p=u.stateNode,typeof p.componentDidMount=="function")try{p.componentDidMount()}catch(G){gt(u,u.return,G)}if(u=y,p=u.updateQueue,p!==null){var j=u.stateNode;try{var D=p.shared.hiddenCallbacks;if(D!==null)for(p.shared.hiddenCallbacks=null,p=0;pxt&&(_=xt,xt=Be,Be=_);var $=Ey(j,Be),P=Ey(j,xt);if($&&P&&(Z.rangeCount!==1||Z.anchorNode!==$.node||Z.anchorOffset!==$.offset||Z.focusNode!==P.node||Z.focusOffset!==P.offset)){var F=ie.createRange();F.setStart($.node,$.offset),Z.removeAllRanges(),Be>xt?(Z.addRange(F),Z.extend(P.node,P.offset)):(F.setEnd(P.node,P.offset),Z.addRange(F))}}}}for(ie=[],Z=j;Z=Z.parentNode;)Z.nodeType===1&&ie.push({element:Z,left:Z.scrollLeft,top:Z.scrollTop});for(typeof j.focus=="function"&&j.focus(),j=0;jo?32:o,O.T=null,o=dm,dm=null;var y=Ia,_=sa;if(Yt=0,Ri=Ia=null,sa=0,(ft&6)!==0)throw Error(s(331));var j=ft;if(ft|=4,C0(y.current),_0(y,y.current,_,o),ft=j,sl(0,!1),Nt&&typeof Nt.onPostCommitFiberRoot=="function")try{Nt.onPostCommitFiberRoot(fn,y)}catch{}return!0}finally{K.p=p,O.T=u,q0(r,a)}}function F0(r,a,o){a=Xn(o,a),a=Vh(r.stateNode,a,2),r=ka(r,a,2),r!==null&&(At(r,2),Tr(r))}function gt(r,a,o){if(r.tag===3)F0(r,r,o);else for(;a!==null;){if(a.tag===3){F0(a,r,o);break}else if(a.tag===1){var u=a.stateNode;if(typeof a.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(za===null||!za.has(u))){r=Xn(o,r),o=Gb(2),u=ka(a,o,2),u!==null&&(Kb(o,u,a,r),At(u,2),Tr(u));break}}a=a.return}}function pm(r,a,o){var u=r.pingCache;if(u===null){u=r.pingCache=new RA;var p=new Set;u.set(a,p)}else p=u.get(a),p===void 0&&(p=new Set,u.set(a,p));p.has(o)||(om=!0,p.add(o),r=LA.bind(null,r,a,o),a.then(r,r))}function LA(r,a,o){var u=r.pingCache;u!==null&&u.delete(a),r.pingedLanes|=r.suspendedLanes&o,r.warmLanes&=~o,wt===r&&(rt&o)===o&&(Ot===4||Ot===3&&(rt&62914560)===rt&&300>qe()-tu?(ft&2)===0&&ki(r,0):lm|=o,Ai===rt&&(Ai=0)),Tr(r)}function G0(r,a){a===0&&(a=hn()),r=ps(r,a),r!==null&&(At(r,a),Tr(r))}function PA(r){var a=r.memoizedState,o=0;a!==null&&(o=a.retryLane),G0(r,o)}function zA(r,a){var o=0;switch(r.tag){case 31:case 13:var u=r.stateNode,p=r.memoizedState;p!==null&&(o=p.retryLane);break;case 19:u=r.stateNode;break;case 22:u=r.stateNode._retryCache;break;default:throw Error(s(314))}u!==null&&u.delete(a),G0(r,o)}function IA(r,a){return ht(r,a)}var lu=null,Mi=null,gm=!1,cu=!1,vm=!1,$a=0;function Tr(r){r!==Mi&&r.next===null&&(Mi===null?lu=Mi=r:Mi=Mi.next=r),cu=!0,gm||(gm=!0,$A())}function sl(r,a){if(!vm&&cu){vm=!0;do for(var o=!1,u=lu;u!==null;){if(r!==0){var p=u.pendingLanes;if(p===0)var y=0;else{var _=u.suspendedLanes,j=u.pingedLanes;y=(1<<31-Tt(42|r)+1)-1,y&=p&~(_&~j),y=y&201326741?y&201326741|1:y?y|2:0}y!==0&&(o=!0,X0(u,y))}else y=rt,y=Se(u,u===wt?y:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(y&3)===0||st(u,y)||(o=!0,X0(u,y));u=u.next}while(o);vm=!1}}function UA(){K0()}function K0(){cu=gm=!1;var r=0;$a!==0&&XA()&&(r=$a);for(var a=qe(),o=null,u=lu;u!==null;){var p=u.next,y=Y0(u,a);y===0?(u.next=null,o===null?lu=p:o.next=p,p===null&&(Mi=o)):(o=u,(r!==0||(y&3)!==0)&&(cu=!0)),u=p}Yt!==0&&Yt!==5||sl(r),$a!==0&&($a=0)}function Y0(r,a){for(var o=r.suspendedLanes,u=r.pingedLanes,p=r.expirationTimes,y=r.pendingLanes&-62914561;0j)break;var ne=D.transferSize,ie=D.initiatorType;ne&&ax(ie)&&(D=D.responseEnd,_+=ne*(D"u"?null:document;function gx(r,a,o){var u=Di;if(u&&typeof a=="string"&&a){var p=Yn(a);p='link[rel="'+r+'"][href="'+p+'"]',typeof o=="string"&&(p+='[crossorigin="'+o+'"]'),px.has(p)||(px.add(p),r={rel:r,crossOrigin:o,href:a},u.querySelector(p)===null&&(a=u.createElement("link"),on(a,"link",r),Jt(a),u.head.appendChild(a)))}}function sR(r){ia.D(r),gx("dns-prefetch",r,null)}function iR(r,a){ia.C(r,a),gx("preconnect",r,a)}function oR(r,a,o){ia.L(r,a,o);var u=Di;if(u&&r&&a){var p='link[rel="preload"][as="'+Yn(a)+'"]';a==="image"&&o&&o.imageSrcSet?(p+='[imagesrcset="'+Yn(o.imageSrcSet)+'"]',typeof o.imageSizes=="string"&&(p+='[imagesizes="'+Yn(o.imageSizes)+'"]')):p+='[href="'+Yn(r)+'"]';var y=p;switch(a){case"style":y=Li(r);break;case"script":y=Pi(r)}nr.has(y)||(r=g({rel:"preload",href:a==="image"&&o&&o.imageSrcSet?void 0:r,as:a},o),nr.set(y,r),u.querySelector(p)!==null||a==="style"&&u.querySelector(cl(y))||a==="script"&&u.querySelector(ul(y))||(a=u.createElement("link"),on(a,"link",r),Jt(a),u.head.appendChild(a)))}}function lR(r,a){ia.m(r,a);var o=Di;if(o&&r){var u=a&&typeof a.as=="string"?a.as:"script",p='link[rel="modulepreload"][as="'+Yn(u)+'"][href="'+Yn(r)+'"]',y=p;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":y=Pi(r)}if(!nr.has(y)&&(r=g({rel:"modulepreload",href:r},a),nr.set(y,r),o.querySelector(p)===null)){switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(o.querySelector(ul(y)))return}u=o.createElement("link"),on(u,"link",r),Jt(u),o.head.appendChild(u)}}}function cR(r,a,o){ia.S(r,a,o);var u=Di;if(u&&r){var p=ri(u).hoistableStyles,y=Li(r);a=a||"default";var _=p.get(y);if(!_){var j={loading:0,preload:null};if(_=u.querySelector(cl(y)))j.loading=5;else{r=g({rel:"stylesheet",href:r,"data-precedence":a},o),(o=nr.get(y))&&Mm(r,o);var D=_=u.createElement("link");Jt(D),on(D,"link",r),D._p=new Promise(function(G,ne){D.onload=G,D.onerror=ne}),D.addEventListener("load",function(){j.loading|=1}),D.addEventListener("error",function(){j.loading|=2}),j.loading|=4,mu(_,a,u)}_={type:"stylesheet",instance:_,count:1,state:j},p.set(y,_)}}}function uR(r,a){ia.X(r,a);var o=Di;if(o&&r){var u=ri(o).hoistableScripts,p=Pi(r),y=u.get(p);y||(y=o.querySelector(ul(p)),y||(r=g({src:r,async:!0},a),(a=nr.get(p))&&Dm(r,a),y=o.createElement("script"),Jt(y),on(y,"link",r),o.head.appendChild(y)),y={type:"script",instance:y,count:1,state:null},u.set(p,y))}}function dR(r,a){ia.M(r,a);var o=Di;if(o&&r){var u=ri(o).hoistableScripts,p=Pi(r),y=u.get(p);y||(y=o.querySelector(ul(p)),y||(r=g({src:r,async:!0,type:"module"},a),(a=nr.get(p))&&Dm(r,a),y=o.createElement("script"),Jt(y),on(y,"link",r),o.head.appendChild(y)),y={type:"script",instance:y,count:1,state:null},u.set(p,y))}}function vx(r,a,o,u){var p=(p=te.current)?hu(p):null;if(!p)throw Error(s(446));switch(r){case"meta":case"title":return null;case"style":return typeof o.precedence=="string"&&typeof o.href=="string"?(a=Li(o.href),o=ri(p).hoistableStyles,u=o.get(a),u||(u={type:"style",instance:null,count:0,state:null},o.set(a,u)),u):{type:"void",instance:null,count:0,state:null};case"link":if(o.rel==="stylesheet"&&typeof o.href=="string"&&typeof o.precedence=="string"){r=Li(o.href);var y=ri(p).hoistableStyles,_=y.get(r);if(_||(p=p.ownerDocument||p,_={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},y.set(r,_),(y=p.querySelector(cl(r)))&&!y._p&&(_.instance=y,_.state.loading=5),nr.has(r)||(o={rel:"preload",as:"style",href:o.href,crossOrigin:o.crossOrigin,integrity:o.integrity,media:o.media,hrefLang:o.hrefLang,referrerPolicy:o.referrerPolicy},nr.set(r,o),y||fR(p,r,o,_.state))),a&&u===null)throw Error(s(528,""));return _}if(a&&u!==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=Pi(o),o=ri(p).hoistableScripts,u=o.get(a),u||(u={type:"script",instance:null,count:0,state:null},o.set(a,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,r))}}function Li(r){return'href="'+Yn(r)+'"'}function cl(r){return'link[rel="stylesheet"]['+r+"]"}function yx(r){return g({},r,{"data-precedence":r.precedence,precedence:null})}function fR(r,a,o,u){r.querySelector('link[rel="preload"][as="style"]['+a+"]")?u.loading=1:(a=r.createElement("link"),u.preload=a,a.addEventListener("load",function(){return u.loading|=1}),a.addEventListener("error",function(){return u.loading|=2}),on(a,"link",o),Jt(a),r.head.appendChild(a))}function Pi(r){return'[src="'+Yn(r)+'"]'}function ul(r){return"script[async]"+r}function bx(r,a,o){if(a.count++,a.instance===null)switch(a.type){case"style":var u=r.querySelector('style[data-href~="'+Yn(o.href)+'"]');if(u)return a.instance=u,Jt(u),u;var p=g({},o,{"data-href":o.href,"data-precedence":o.precedence,href:null,precedence:null});return u=(r.ownerDocument||r).createElement("style"),Jt(u),on(u,"style",p),mu(u,o.precedence,r),a.instance=u;case"stylesheet":p=Li(o.href);var y=r.querySelector(cl(p));if(y)return a.state.loading|=4,a.instance=y,Jt(y),y;u=yx(o),(p=nr.get(p))&&Mm(u,p),y=(r.ownerDocument||r).createElement("link"),Jt(y);var _=y;return _._p=new Promise(function(j,D){_.onload=j,_.onerror=D}),on(y,"link",u),a.state.loading|=4,mu(y,o.precedence,r),a.instance=y;case"script":return y=Pi(o.src),(p=r.querySelector(ul(y)))?(a.instance=p,Jt(p),p):(u=o,(p=nr.get(y))&&(u=g({},o),Dm(u,p)),r=r.ownerDocument||r,p=r.createElement("script"),Jt(p),on(p,"link",u),r.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&&(u=a.instance,a.state.loading|=4,mu(u,o.precedence,r));return a.instance}function mu(r,a,o){for(var u=o.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),p=u.length?u[u.length-1]:null,y=p,_=0;_ title"):null)}function hR(r,a,o){if(o===1||a.itemProp!=null)return!1;switch(r){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 r=a.disabled,typeof a.precedence=="string"&&r==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 _x(r){return!(r.type==="stylesheet"&&(r.state.loading&3)===0)}function mR(r,a,o,u){if(o.type==="stylesheet"&&(typeof u.media!="string"||matchMedia(u.media).matches!==!1)&&(o.state.loading&4)===0){if(o.instance===null){var p=Li(u.href),y=a.querySelector(cl(p));if(y){a=y._p,a!==null&&typeof a=="object"&&typeof a.then=="function"&&(r.count++,r=gu.bind(r),a.then(r,r)),o.state.loading|=4,o.instance=y,Jt(y);return}y=a.ownerDocument||a,u=yx(u),(p=nr.get(p))&&Mm(u,p),y=y.createElement("link"),Jt(y);var _=y;_._p=new Promise(function(j,D){_.onload=j,_.onerror=D}),on(y,"link",u),o.instance=y}r.stylesheets===null&&(r.stylesheets=new Map),r.stylesheets.set(o,a),(a=o.state.preload)&&(o.state.loading&3)===0&&(r.count++,o=gu.bind(r),a.addEventListener("load",o),a.addEventListener("error",o))}}var Lm=0;function pR(r,a){return r.stylesheets&&r.count===0&&yu(r,r.stylesheets),0Lm?50:800)+a);return r.unsuspend=o,function(){r.unsuspend=null,clearTimeout(u),clearTimeout(p)}}:null}function gu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)yu(this,this.stylesheets);else if(this.unsuspend){var r=this.unsuspend;this.unsuspend=null,r()}}}var vu=null;function yu(r,a){r.stylesheets=null,r.unsuspend!==null&&(r.count++,vu=new Map,a.forEach(gR,r),vu=null,gu.call(r))}function gR(r,a){if(!(a.state.loading&4)){var o=vu.get(r);if(o)var u=o.get(null);else{o=new Map,vu.set(r,o);for(var p=r.querySelectorAll("link[data-precedence],style[data-precedence]"),y=0;y"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),Vm.exports=OR(),Vm.exports}var DR=MR();/** + * 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 Gx="popstate";function Kx(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function LR(e={}){function t(s,i){var m;let l=(m=i.state)==null?void 0:m.masked,{pathname:c,search:d,hash:h}=l||s.location;return Lp("",{pathname:c,search:d,hash:h},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 n(s,i){return typeof i=="string"?i:zl(i)}return zR(t,n,null,e)}function Rt(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function Ur(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function PR(){return Math.random().toString(36).substring(2,10)}function Yx(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 Lp(e,t,n=null,s,i){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?vo(t):t,state:n,key:t&&t.key||s||PR(),mask:i}}function zl({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function vo(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let s=e.indexOf("?");s>=0&&(t.search=e.substring(s),e=e.substring(0,s)),e&&(t.pathname=e)}return t}function zR(e,t,n,s={}){let{window:i=document.defaultView,v5Compat:l=!1}=s,c=i.history,d="POP",h=null,m=b();m==null&&(m=0,c.replaceState({...c.state,idx:m},""));function b(){return(c.state||{idx:null}).idx}function g(){d="POP";let N=b(),C=N==null?null:N-m;m=N,h&&h({action:d,location:S.location,delta:C})}function x(N,C){d="PUSH";let A=Kx(N)?N:Lp(S.location,N,C);m=b()+1;let T=Yx(A,m),M=S.createHref(A.mask||A);try{c.pushState(T,"",M)}catch(U){if(U instanceof DOMException&&U.name==="DataCloneError")throw U;i.location.assign(M)}l&&h&&h({action:d,location:S.location,delta:1})}function w(N,C){d="REPLACE";let A=Kx(N)?N:Lp(S.location,N,C);m=b();let T=Yx(A,m),M=S.createHref(A.mask||A);c.replaceState(T,"",M),l&&h&&h({action:d,location:S.location,delta:0})}function E(N){return IR(N)}let S={get action(){return d},get location(){return e(i,c)},listen(N){if(h)throw new Error("A history only accepts one active listener");return i.addEventListener(Gx,g),h=N,()=>{i.removeEventListener(Gx,g),h=null}},createHref(N){return t(i,N)},createURL:E,encodeLocation(N){let C=E(N);return{pathname:C.pathname,search:C.search,hash:C.hash}},push:x,replace:w,go(N){return c.go(N)}};return S}function IR(e,t=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),Rt(n,"No window.location.(origin|href) available to create URL");let s=typeof e=="string"?e:zl(e);return s=s.replace(/ $/,"%20"),!t&&s.startsWith("//")&&(s=n+s),new URL(s,n)}function Z_(e,t,n="/"){return UR(e,t,n,!1)}function UR(e,t,n,s,i){let l=typeof t=="string"?vo(t):t,c=ha(l.pathname||"/",n);if(c==null)return null;let d=$R(e),h=null,m=QR(c);for(let b=0;h==null&&b{let b={relativePath:m===void 0?c.path||"":m,caseSensitive:c.caseSensitive===!0,childrenIndex:d,route:c};if(b.relativePath.startsWith("/")){if(!b.relativePath.startsWith(s)&&h)return;Rt(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=wr([s,b.relativePath]),x=n.concat(b);c.children&&c.children.length>0&&(Rt(c.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${g}".`),eS(c.children,t,x,g,h)),!(c.path==null&&!c.index)&&t.push({path:g,score:YR(g,c.index),routesMeta:x})};return e.forEach((c,d)=>{var h;if(c.path===""||!((h=c.path)!=null&&h.includes("?")))l(c,d);else for(let m of tS(c.path))l(c,d,!0,m)}),t}function tS(e){let t=e.split("/");if(t.length===0)return[];let[n,...s]=t,i=n.endsWith("?"),l=n.replace(/\?$/,"");if(s.length===0)return i?[l,""]:[l];let c=tS(s.join("/")),d=[];return d.push(...c.map(h=>h===""?l:[l,h].join("/"))),i&&d.push(...c),d.map(h=>e.startsWith("/")&&h===""?"/":h)}function BR(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:WR(t.routesMeta.map(s=>s.childrenIndex),n.routesMeta.map(s=>s.childrenIndex)))}var HR=/^:[\w-]+$/,qR=3,VR=2,FR=1,GR=10,KR=-2,Wx=e=>e==="*";function YR(e,t){let n=e.split("/"),s=n.length;return n.some(Wx)&&(s+=KR),t&&(s+=VR),n.filter(i=>!Wx(i)).reduce((i,l)=>i+(HR.test(l)?qR:l===""?FR:GR),s)}function WR(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 XR(e,t,n=!1){let{routesMeta:s}=e,i={},l="/",c=[];for(let d=0;d{if(b==="*"){let E=d[x]||"";c=l.slice(0,l.length-E.length).replace(/(.)\/+$/,"$1")}const w=d[x];return g&&!w?m[b]=void 0:m[b]=(w||"").replace(/%2F/g,"/"),m},{}),pathname:l,pathnameBase:c,pattern:e}}function JR(e,t=!1,n=!0){Ur(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,(c,d,h,m,b)=>{if(s.push({paramName:d,isOptional:h!=null}),h){let g=b.charAt(m+c.length);return g&&g!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(s.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),s]}function QR(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return Ur(!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 ha(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,s=e.charAt(n);return s&&s!=="/"?null:e.slice(n)||"/"}var ZR=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ek(e,t="/"){let{pathname:n,search:s="",hash:i=""}=typeof e=="string"?vo(e):e,l;return n?(n=rS(n),n.startsWith("/")?l=Xx(n.substring(1),"/"):l=Xx(n,t)):l=t,{pathname:l,search:rk(s),hash:ak(i)}}function Xx(e,t){let n=pd(t).split("/");return e.split("/").forEach(i=>{i===".."?n.length>1&&n.pop():i!=="."&&n.push(i)}),n.length>1?n.join("/"):"/"}function Ym(e,t,n,s){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(s)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function tk(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function nS(e){let t=tk(e);return t.map((n,s)=>s===t.length-1?n.pathname:n.pathnameBase)}function Tg(e,t,n,s=!1){let i;typeof e=="string"?i=vo(e):(i={...e},Rt(!i.pathname||!i.pathname.includes("?"),Ym("?","pathname","search",i)),Rt(!i.pathname||!i.pathname.includes("#"),Ym("#","pathname","hash",i)),Rt(!i.search||!i.search.includes("#"),Ym("#","search","hash",i)));let l=e===""||i.pathname==="",c=l?"/":i.pathname,d;if(c==null)d=n;else{let g=t.length-1;if(!s&&c.startsWith("..")){let x=c.split("/");for(;x[0]==="..";)x.shift(),g-=1;i.pathname=x.join("/")}d=g>=0?t[g]:"/"}let h=ek(i,d),m=c&&c!=="/"&&c.endsWith("/"),b=(l||c===".")&&n.endsWith("/");return!h.pathname.endsWith("/")&&(m||b)&&(h.pathname+="/"),h}var rS=e=>e.replace(/\/\/+/g,"/"),wr=e=>rS(e.join("/")),pd=e=>e.replace(/\/+$/,""),nk=e=>pd(e).replace(/^\/*/,"/"),rk=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,ak=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,sk=class{constructor(e,t,n,s=!1){this.status=e,this.statusText=t||"",this.internal=s,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function ik(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function ok(e){let t=e.map(n=>n.route.path).filter(Boolean);return wr(t)||"/"}var aS=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function sS(e,t){let n=e;if(typeof n!="string"||!ZR.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let s=n,i=!1;if(aS)try{let l=new URL(window.location.href),c=n.startsWith("//")?new URL(l.protocol+n):new URL(n),d=ha(c.pathname,t);c.origin===l.origin&&d!=null?n=d+c.search+c.hash:i=!0}catch{Ur(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:s,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var iS=["POST","PUT","PATCH","DELETE"];new Set(iS);var lk=["GET",...iS];new Set(lk);var yo=v.createContext(null);yo.displayName="DataRouter";var Fd=v.createContext(null);Fd.displayName="DataRouterState";var oS=v.createContext(!1);function ck(){return v.useContext(oS)}var lS=v.createContext({isTransitioning:!1});lS.displayName="ViewTransition";var uk=v.createContext(new Map);uk.displayName="Fetchers";var dk=v.createContext(null);dk.displayName="Await";var ir=v.createContext(null);ir.displayName="Navigation";var Jl=v.createContext(null);Jl.displayName="Location";var ya=v.createContext({outlet:null,matches:[],isDataRoute:!1});ya.displayName="Route";var Ag=v.createContext(null);Ag.displayName="RouteError";var cS="REACT_ROUTER_ERROR",fk="REDIRECT",hk="ROUTE_ERROR_RESPONSE";function mk(e){if(e.startsWith(`${cS}:${fk}:{`))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 pk(e){if(e.startsWith(`${cS}:${hk}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new sk(t.status,t.statusText,t.data)}catch{}}function gk(e,{relative:t}={}){Rt(Ql(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:s}=v.useContext(ir),{hash:i,pathname:l,search:c}=Zl(e,{relative:t}),d=l;return n!=="/"&&(d=l==="/"?n:wr([n,l])),s.createHref({pathname:d,search:c,hash:i})}function Ql(){return v.useContext(Jl)!=null}function ba(){return Rt(Ql(),"useLocation() may be used only in the context of a component."),v.useContext(Jl).location}var uS="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function dS(e){v.useContext(ir).static||v.useLayoutEffect(e)}function vk(){let{isDataRoute:e}=v.useContext(ya);return e?Rk():yk()}function yk(){Rt(Ql(),"useNavigate() may be used only in the context of a component.");let e=v.useContext(yo),{basename:t,navigator:n}=v.useContext(ir),{matches:s}=v.useContext(ya),{pathname:i}=ba(),l=JSON.stringify(nS(s)),c=v.useRef(!1);return dS(()=>{c.current=!0}),v.useCallback((h,m={})=>{if(Ur(c.current,uS),!c.current)return;if(typeof h=="number"){n.go(h);return}let b=Tg(h,JSON.parse(l),i,m.relative==="path");e==null&&t!=="/"&&(b.pathname=b.pathname==="/"?t:wr([t,b.pathname])),(m.replace?n.replace:n.push)(b,m.state,m)},[t,n,l,i,e])}v.createContext(null);function Zl(e,{relative:t}={}){let{matches:n}=v.useContext(ya),{pathname:s}=ba(),i=JSON.stringify(nS(n));return v.useMemo(()=>Tg(e,JSON.parse(i),s,t==="path"),[e,i,s,t])}function bk(e,t){return fS(e,t)}function fS(e,t,n){var N;Rt(Ql(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=v.useContext(ir),{matches:i}=v.useContext(ya),l=i[i.length-1],c=l?l.params:{},d=l?l.pathname:"/",h=l?l.pathnameBase:"/",m=l&&l.route;{let C=m&&m.path||"";mS(d,!m||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=ba(),g;if(t){let C=typeof t=="string"?vo(t):t;Rt(h==="/"||((N=C.pathname)==null?void 0:N.startsWith(h)),`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 "${h}" but pathname "${C.pathname}" was given in the \`location\` prop.`),g=C}else g=b;let x=g.pathname||"/",w=x;if(h!=="/"){let C=h.replace(/^\//,"").split("/");w="/"+x.replace(/^\//,"").split("/").slice(C.length).join("/")}let E=n&&n.state.matches.length?n.state.matches.map(C=>Object.assign(C,{route:n.manifest[C.route.id]||C.route})):Z_(e,{pathname:w});Ur(m||E!=null,`No routes matched location "${g.pathname}${g.search}${g.hash}" `),Ur(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=Ek(E&&E.map(C=>Object.assign({},C,{params:Object.assign({},c,C.params),pathname:wr([h,s.encodeLocation?s.encodeLocation(C.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:C.pathname]),pathnameBase:C.pathnameBase==="/"?h:wr([h,s.encodeLocation?s.encodeLocation(C.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:C.pathnameBase])})),i,n);return t&&S?v.createElement(Jl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...g},navigationType:"POP"}},S):S}function xk(){let e=Ak(),t=ik(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=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},c=null;return console.error("Error handled by React Router default ErrorBoundary:",e),c=v.createElement(v.Fragment,null,v.createElement("p",null,"💿 Hey developer 👋"),v.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",v.createElement("code",{style:l},"ErrorBoundary")," or"," ",v.createElement("code",{style:l},"errorElement")," prop on your route.")),v.createElement(v.Fragment,null,v.createElement("h2",null,"Unexpected Application Error!"),v.createElement("h3",{style:{fontStyle:"italic"}},t),n?v.createElement("pre",{style:i},n):null,c)}var wk=v.createElement(xk,null),hS=class extends v.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 n=pk(e.digest);n&&(e=n)}let t=e!==void 0?v.createElement(ya.Provider,{value:this.props.routeContext},v.createElement(Ag.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?v.createElement(_k,{error:e},t):t}};hS.contextType=oS;var Wm=new WeakMap;function _k({children:e,error:t}){let{basename:n}=v.useContext(ir);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let s=mk(t.digest);if(s){let i=Wm.get(t);if(i)throw i;let l=sS(s.location,n);if(aS&&!Wm.get(t))if(l.isExternal||s.reloadDocument)window.location.href=l.absoluteURL||l.to;else{const c=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(l.to,{replace:s.replace}));throw Wm.set(t,c),c}return v.createElement("meta",{httpEquiv:"refresh",content:`0;url=${l.absoluteURL||l.to}`})}}return e}function Sk({routeContext:e,match:t,children:n}){let s=v.useContext(yo);return s&&s.static&&s.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(s.staticContext._deepestRenderedBoundaryId=t.route.id),v.createElement(ya.Provider,{value:e},n)}function Ek(e,t=[],n){let s=n==null?void 0:n.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);Rt(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 c=!1,d=-1;if(n&&s){c=s.renderFallback;for(let b=0;b=0?i=i.slice(0,d+1):i=[i[0]];break}}}}let h=n==null?void 0:n.onError,m=s&&h?(b,g)=>{var x,w;h(b,{location:s.location,params:((w=(x=s.matches)==null?void 0:x[0])==null?void 0:w.params)??{},pattern:ok(s.matches),errorInfo:g})}:void 0;return i.reduceRight((b,g,x)=>{let w,E=!1,S=null,N=null;s&&(w=l&&g.route.id?l[g.route.id]:void 0,S=g.route.errorElement||wk,c&&(d<0&&x===0?(mS("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),E=!0,N=null):d===x&&(E=!0,N=g.route.hydrateFallbackElement||null)));let C=t.concat(i.slice(0,x+1)),A=()=>{let T;return w?T=S:E?T=N:g.route.Component?T=v.createElement(g.route.Component,null):g.route.element?T=g.route.element:T=b,v.createElement(Sk,{match:g,routeContext:{outlet:b,matches:C,isDataRoute:s!=null},children:T})};return s&&(g.route.ErrorBoundary||g.route.errorElement||x===0)?v.createElement(hS,{location:s.location,revalidation:s.revalidation,component:S,error:w,children:A(),routeContext:{outlet:null,matches:C,isDataRoute:!0},onError:m}):A()},null)}function Rg(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ck(e){let t=v.useContext(yo);return Rt(t,Rg(e)),t}function Nk(e){let t=v.useContext(Fd);return Rt(t,Rg(e)),t}function jk(e){let t=v.useContext(ya);return Rt(t,Rg(e)),t}function kg(e){let t=jk(e),n=t.matches[t.matches.length-1];return Rt(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Tk(){return kg("useRouteId")}function Ak(){var s;let e=v.useContext(Ag),t=Nk("useRouteError"),n=kg("useRouteError");return e!==void 0?e:(s=t.errors)==null?void 0:s[n]}function Rk(){let{router:e}=Ck("useNavigate"),t=kg("useNavigate"),n=v.useRef(!1);return dS(()=>{n.current=!0}),v.useCallback(async(i,l={})=>{Ur(n.current,uS),n.current&&(typeof i=="number"?await e.navigate(i):await e.navigate(i,{fromRouteId:t,...l}))},[e,t])}var Jx={};function mS(e,t,n){!t&&!Jx[e]&&(Jx[e]=!0,Ur(!1,n))}v.memo(kk);function kk({routes:e,manifest:t,future:n,state:s,isStatic:i,onError:l}){return fS(e,void 0,{manifest:t,state:s,isStatic:i,onError:l})}function pS(e){Rt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Ok({basename:e="/",children:t=null,location:n,navigationType:s="POP",navigator:i,static:l=!1,useTransitions:c}){Rt(!Ql(),"You cannot render a inside another . You should never have more than one in your app.");let d=e.replace(/^\/*/,"/"),h=v.useMemo(()=>({basename:d,navigator:i,static:l,useTransitions:c,future:{}}),[d,i,l,c]);typeof n=="string"&&(n=vo(n));let{pathname:m="/",search:b="",hash:g="",state:x=null,key:w="default",mask:E}=n,S=v.useMemo(()=>{let N=ha(m,d);return N==null?null:{location:{pathname:N,search:b,hash:g,state:x,key:w,mask:E},navigationType:s}},[d,m,b,g,x,w,s,E]);return Ur(S!=null,` is not able to match the URL "${m}${b}${g}" because it does not start with the basename, so the won't render anything.`),S==null?null:v.createElement(ir.Provider,{value:h},v.createElement(Jl.Provider,{children:t,value:S}))}function Mk({children:e,location:t}){return bk(Pp(e),t)}function Pp(e,t=[]){let n=[];return v.Children.forEach(e,(s,i)=>{if(!v.isValidElement(s))return;let l=[...t,i];if(s.type===v.Fragment){n.push.apply(n,Pp(s.props.children,l));return}Rt(s.type===pS,`[${typeof s.type=="string"?s.type:s.type.name}] is not a component. All component children of must be a or `),Rt(!s.props.index||!s.props.children,"An index route cannot have child routes.");let c={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&&(c.children=Pp(s.props.children,l)),n.push(c)}),n}var nd="get",rd="application/x-www-form-urlencoded";function Gd(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function Dk(e){return Gd(e)&&e.tagName.toLowerCase()==="button"}function Lk(e){return Gd(e)&&e.tagName.toLowerCase()==="form"}function Pk(e){return Gd(e)&&e.tagName.toLowerCase()==="input"}function zk(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ik(e,t){return e.button===0&&(!t||t==="_self")&&!zk(e)}var Nu=null;function Uk(){if(Nu===null)try{new FormData(document.createElement("form"),0),Nu=!1}catch{Nu=!0}return Nu}var $k=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Xm(e){return e!=null&&!$k.has(e)?(Ur(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${rd}"`),null):e}function Bk(e,t){let n,s,i,l,c;if(Lk(e)){let d=e.getAttribute("action");s=d?ha(d,t):null,n=e.getAttribute("method")||nd,i=Xm(e.getAttribute("enctype"))||rd,l=new FormData(e)}else if(Dk(e)||Pk(e)&&(e.type==="submit"||e.type==="image")){let d=e.form;if(d==null)throw new Error('Cannot submit a diff --git a/src/components/board/ProjectTimeDialog.tsx b/src/components/board/ProjectTimeDialog.tsx index 7a116b1..d3eb0d1 100644 --- a/src/components/board/ProjectTimeDialog.tsx +++ b/src/components/board/ProjectTimeDialog.tsx @@ -7,6 +7,7 @@ import { ListChecks, Loader2, MapPin, + Pencil, Plus, RefreshCw, Trash2, @@ -31,10 +32,12 @@ import { addProjectTimeEntry, deleteProjectTimeEntry, loadProjectTimeEntries, + updateProjectTimeEntry, type Project, type ProjectTimeEntry, } from "@/lib/store"; import { cn } from "@/lib/utils"; +import { syncProjectTimeToSheet } from "@/lib/timeSheetSync"; import { toast } from "sonner"; interface ProjectTimeDialogProps { @@ -143,7 +146,9 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [deletingId, setDeletingId] = useState(null); + const [editingId, setEditingId] = useState(null); const [error, setError] = useState(null); + const [sheetSyncWarning, setSheetSyncWarning] = useState(null); const [country, setCountry] = useState(""); const [taskName, setTaskName] = useState(""); const [hours, setHours] = useState(""); @@ -193,6 +198,13 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi setError(null); const rows = await loadProjectTimeEntries(project.id); setEntries(rows); + + // Reconciliación silenciosa: al abrir Tiempo se vuelve a comprobar Excel. + void syncProjectTimeToSheet(project.id) + .then(() => setSheetSyncWarning(null)) + .catch((syncError) => { + console.warn("No se pudo reconciliar el registro de horas con Excel:", syncError); + }); } catch (loadError) { console.error("No se pudo cargar el tiempo del proyecto:", loadError); setEntries([]); @@ -217,6 +229,8 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi setMinutes(""); setWorkDate(todayLocal()); setNotes(""); + setSheetSyncWarning(null); + setEditingId(null); setPage(1); void loadEntries(); }, [loadEntries, open, project?.id, projectCountries]); @@ -230,6 +244,35 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi Math.max(0, Math.floor(Number(minutes || 0))); const validDuration = Number.isFinite(durationMinutes) && durationMinutes > 0; const canSubmit = Boolean(country.trim() && taskName.trim() && workDate && validDuration); + const isEditing = Boolean(editingId); + + const resetForm = () => { + setCountry(projectCountries[0] || ""); + setTaskName(""); + setHours(""); + setMinutes(""); + setWorkDate(todayLocal()); + setNotes(""); + setEditingId(null); + }; + + const handleEdit = (entry: ProjectTimeEntry) => { + const safeMinutes = Math.max(0, Math.round(entry.durationMinutes)); + setEditingId(entry.id); + setCountry(entry.country); + setTaskName(entry.taskName); + setHours(String(Math.floor(safeMinutes / 60))); + setMinutes(String(safeMinutes % 60)); + setWorkDate(entry.workDate); + setNotes(entry.notes || ""); + setError(null); + requestAnimationFrame(() => { + document.querySelector('[data-project-time-form="true"]')?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }); + }; const handleAdd = async () => { if (!project?.id || !canSubmit || saving) return; @@ -237,23 +280,38 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi try { setSaving(true); setError(null); - const saved = await addProjectTimeEntry(project.id, { - country, - taskName, - durationMinutes, - workDate, - notes, - }); + const payload = { country, taskName, durationMinutes, workDate, notes }; + const saved = editingId + ? await updateProjectTimeEntry(editingId, payload) + : await addProjectTimeEntry(project.id, payload); - setEntries((current) => [saved, ...current]); - setTaskName(""); - setHours(""); - setMinutes(""); - setNotes(""); - setPage(1); - toast.success("Tiempo registrado", { + if (editingId) { + setEntries((current) => + current + .map((item) => (item.id === saved.id ? saved : item)) + .sort((a, b) => b.workDate.localeCompare(a.workDate) || b.createdAt - a.createdAt), + ); + } else { + setEntries((current) => [saved, ...current]); + setPage(1); + } + resetForm(); + toast.success(editingId ? "Registro de tiempo actualizado" : "Tiempo registrado", { description: `${formatDuration(saved.durationMinutes, true)} · ${saved.country} · ${saved.taskName}`, }); + + try { + await syncProjectTimeToSheet(project.id); + setSheetSyncWarning(null); + } catch (syncError) { + console.warn("El tiempo se guardó, pero Excel no pudo sincronizarse todavía:", syncError); + setSheetSyncWarning( + "El tiempo quedó guardado en el Tablero, pero el Excel no pudo actualizarse ahora. Se reintentará automáticamente al volver a abrir Tiempo.", + ); + toast.warning("Tiempo guardado; Excel pendiente", { + description: "La app volverá a intentar la sincronización automáticamente.", + }); + } } catch (saveError) { console.error("No se pudo registrar el tiempo:", saveError); const message = isMissingTimeTableError(saveError) @@ -279,7 +337,23 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi setDeletingId(entry.id); await deleteProjectTimeEntry(entry.id); setEntries((current) => current.filter((item) => item.id !== entry.id)); + if (editingId === entry.id) resetForm(); toast.success("Registro de tiempo eliminado"); + + if (project?.id) { + try { + await syncProjectTimeToSheet(project.id); + setSheetSyncWarning(null); + } catch (syncError) { + console.warn("El registro se eliminó, pero Excel no pudo sincronizarse todavía:", syncError); + setSheetSyncWarning( + "El registro se eliminó del Tablero, pero el Excel no pudo actualizarse ahora. Se reintentará automáticamente al volver a abrir Tiempo.", + ); + toast.warning("Registro eliminado; Excel pendiente", { + description: "La app volverá a intentar la sincronización automáticamente.", + }); + } + } } catch (deleteError) { console.error("No se pudo eliminar el registro de tiempo:", deleteError); toast.error("No se pudo eliminar", { @@ -329,12 +403,14 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi /> -
+
-

Agregar tiempo

+

{isEditing ? "Editar tiempo" : "Agregar tiempo"}

- Cada registro queda asociado a este proyecto y al usuario que lo agrega. + {isEditing + ? "Modifica los datos del registro seleccionado y guarda los cambios." + : "Cada registro queda asociado a este proyecto y al usuario que lo agrega."}

)} - {saving ? "Guardando…" : "Agregar registro"} - + +
+ {sheetSyncWarning && ( +
+ {sheetSyncWarning} +
+ )} + {error && (
{error} @@ -533,6 +630,8 @@ export function ProjectTimeDialog({ open, onOpenChange, project }: ProjectTimeDi key={entry.id} entry={entry} deleting={deletingId === entry.id} + editing={editingId === entry.id} + onEdit={() => handleEdit(entry)} onDelete={() => void handleDelete(entry)} /> ))} @@ -653,10 +752,14 @@ function BreakdownCard({ function TimeEntryRow({ entry, deleting, + editing, + onEdit, onDelete, }: { entry: ProjectTimeEntry; deleting: boolean; + editing: boolean; + onEdit: () => void; onDelete: () => void; }) { return ( @@ -691,20 +794,36 @@ function TimeEntryRow({
- +
+ + +
); } diff --git a/src/components/lists/ListManagerDialog.tsx b/src/components/lists/ListManagerDialog.tsx index 136b619..3021fc9 100644 --- a/src/components/lists/ListManagerDialog.tsx +++ b/src/components/lists/ListManagerDialog.tsx @@ -79,9 +79,46 @@ function sortRows(rows: EditableRow[]) { return [...rows].sort((a, b) => a.value.localeCompare(b.value, "es", { sensitivity: "base" })); } +const PAGE_SIZE = 25; + +type PaginationItem = number | "ellipsis-start" | "ellipsis-end"; + +function buildPaginationItems(currentPage: number, totalPages: number): PaginationItem[] { + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, index) => index + 1); + } + + if (currentPage <= 4) { + return [1, 2, 3, 4, 5, "ellipsis-end", totalPages]; + } + + if (currentPage >= totalPages - 3) { + return [ + 1, + "ellipsis-start", + totalPages - 4, + totalPages - 3, + totalPages - 2, + totalPages - 1, + totalPages, + ]; + } + + return [ + 1, + "ellipsis-start", + currentPage - 1, + currentPage, + currentPage + 1, + "ellipsis-end", + totalPages, + ]; +} + export function ListManagerDialog({ open, onOpenChange }: Props) { const [category, setCategory] = useState("client"); const [query, setQuery] = useState(""); + const [page, setPage] = useState(1); const [value, setValue] = useState(""); const [manager, setManager] = useState(""); const [editingOriginal, setEditingOriginal] = useState(null); @@ -117,6 +154,12 @@ export function ListManagerDialog({ open, onOpenChange }: Props) { ); }, [query, rows]); + const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE)); + const currentPage = Math.min(page, totalPages); + const pageStart = (currentPage - 1) * PAGE_SIZE; + const pageRows = filteredRows.slice(pageStart, pageStart + PAGE_SIZE); + const paginationItems = buildPaginationItems(currentPage, totalPages); + const resetDraft = () => { setValue(""); setManager(""); @@ -131,8 +174,17 @@ export function ListManagerDialog({ open, onOpenChange }: Props) { useEffect(() => { resetDraft(); setQuery(""); + setPage(1); }, [category]); + useEffect(() => { + setPage(1); + }, [query]); + + useEffect(() => { + if (page > totalPages) setPage(totalPages); + }, [page, totalPages]); + const beginEdit = (row: EditableRow) => { setEditingOriginal(row.value); setValue(row.value); @@ -240,8 +292,9 @@ export function ListManagerDialog({ open, onOpenChange }: Props) { Administración de listas - Los cambios se escriben primero en la hoja Listas y se - sincronizan inmediatamente con Supabase para que aparezcan en toda la app. + Los cambios que realices aquí se guardarán automáticamente en la hoja{` `} + Listas y se reflejarán de inmediato en las opciones disponibles + de la aplicación.
@@ -348,7 +401,7 @@ export function ListManagerDialog({ open, onOpenChange }: Props) { setQuery(event.target.value)} - placeholder={`Buscar en ${meta.label.toLowerCase()}…`} + placeholder={`Buscar en ${meta.label}…`} className="pl-9" />
@@ -375,7 +428,7 @@ export function ListManagerDialog({ open, onOpenChange }: Props) { ) : (
- {filteredRows.map((row) => ( + {pageRows.map((row) => (
)}
+ + {filteredRows.length > 0 && ( +
+ + Mostrando {pageStart + 1}–{Math.min(pageStart + PAGE_SIZE, filteredRows.length)} de{` `} + {filteredRows.length} + + {totalPages > 1 && ( +
+ + {paginationItems.map((item) => + typeof item === "number" ? ( + + ) : ( + + ), + )} + +
+ )} +
+ )}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index de04718..395ceda 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -10,6 +10,7 @@ import React, { import type { User as SupabaseUser } from "@supabase/supabase-js"; import { supabase } from "@/lib/supabase"; import { getActiveTableroCdcAccess, type TableroCdcUserAccess } from "@/lib/accessControl"; +import { recordTableroCdcOpen } from "@/lib/appMetrics"; function getRedirectTo(): string { if (typeof window === "undefined") return "/tablero-cdc/"; @@ -103,6 +104,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { setUser(normalizeSupabaseUser(supabaseUser)); setAccess(nextAccess); setError(null); + + // Métrica no bloqueante: una apertura por sesión autenticada/autorizada. + void recordTableroCdcOpen(); }, []); useEffect(() => { diff --git a/src/lib/appMetrics.ts b/src/lib/appMetrics.ts new file mode 100644 index 0000000..dfe450b --- /dev/null +++ b/src/lib/appMetrics.ts @@ -0,0 +1,47 @@ +import { supabase } from "@/lib/supabase"; + +const SESSION_ID_KEY = "tablero_cdc_metrics_session_id"; +const RECORDED_KEY = "tablero_cdc_metrics_open_recorded"; + +function getOrCreateSessionId() { + if (typeof window === "undefined") return crypto.randomUUID(); + + const current = window.sessionStorage.getItem(SESSION_ID_KEY); + if (current) return current; + + const next = crypto.randomUUID(); + window.sessionStorage.setItem(SESSION_ID_KEY, next); + return next; +} + +/** + * Registra una apertura válida del Tablero CDC una vez por sesión del navegador. + * Nunca bloquea el acceso a la app si el módulo de métricas todavía no está instalado. + */ +export async function recordTableroCdcOpen() { + if (typeof window !== "undefined" && window.sessionStorage.getItem(RECORDED_KEY) === "1") { + return; + } + + const sessionId = getOrCreateSessionId(); + const { error } = await supabase.rpc("tablero_cdc_track_app_open", { + p_session_id: sessionId, + }); + + if (error) { + const message = String(error.message || "").toLowerCase(); + const missingFunction = + message.includes("tablero_cdc_track_app_open") || + message.includes("could not find the function") || + message.includes("schema cache"); + + if (!missingFunction) { + console.warn("No se pudo registrar la métrica de apertura del Tablero CDC:", error); + } + return; + } + + if (typeof window !== "undefined") { + window.sessionStorage.setItem(RECORDED_KEY, "1"); + } +} diff --git a/src/lib/store.ts b/src/lib/store.ts index 223173c..d92daf0 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -1007,6 +1007,47 @@ export async function addProjectTimeEntry( return mapDbProjectTimeEntry(data as DbProjectTimeEntry); } +export async function updateProjectTimeEntry( + id: string, + input: ProjectTimeEntryInput, +): Promise { + const cleanId = String(id || "").trim(); + const country = String(input.country || "").trim(); + const taskName = String(input.taskName || "").trim(); + const durationMinutes = Math.round(Number(input.durationMinutes || 0)); + const workDate = String(input.workDate || "").trim(); + const notes = String(input.notes || "").trim(); + + if (!cleanId) throw new Error("No se encontró el registro de tiempo."); + if (!country) throw new Error("Selecciona el país."); + if (!taskName) throw new Error("Escribe la tarea realizada."); + if (!Number.isFinite(durationMinutes) || durationMinutes <= 0) { + throw new Error("La duración debe ser mayor que 0 minutos."); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(workDate)) { + throw new Error("Selecciona una fecha válida."); + } + + const { data, error } = await supabase + .from("tablero_cdc_project_time_entries") + .update({ + country, + task_name: taskName, + duration_minutes: durationMinutes, + work_date: workDate, + notes, + }) + .eq("id", cleanId) + .select( + "id, project_id, country, task_name, duration_minutes, work_date, notes, created_by_name, created_by_email, created_at, updated_at", + ) + .single(); + + if (error) throw error; + + return mapDbProjectTimeEntry(data as DbProjectTimeEntry); +} + export async function deleteProjectTimeEntry(id: string): Promise { const cleanId = String(id || "").trim(); diff --git a/src/lib/timeSheetSync.ts b/src/lib/timeSheetSync.ts new file mode 100644 index 0000000..2591df3 --- /dev/null +++ b/src/lib/timeSheetSync.ts @@ -0,0 +1,90 @@ +import { supabase } from "@/lib/supabase"; + +const DEFAULT_TIME_SHEET_WEBHOOK_URL = + "https://agenteit.digitalcompass.agency/webhook/tablero-cdc-sync-registro-horas"; + +export type TimeSheetSyncResult = { + ok: boolean; + message?: string; + projectId?: string; + syncedEntries?: number; + clearedEntries?: number; +}; + +function getWebhookUrl() { + return ( + String(import.meta.env.VITE_TIME_SHEET_WEBHOOK_URL || "").trim() || + DEFAULT_TIME_SHEET_WEBHOOK_URL + ); +} + +async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs = 20_000) { + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), timeoutMs); + + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + window.clearTimeout(timeout); + } +} + +function wait(ms: number) { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +export async function syncProjectTimeToSheet( + projectId: string, +): Promise { + const cleanProjectId = String(projectId || "").trim(); + if (!cleanProjectId) throw new Error("No se encontró el proyecto para sincronizar las horas."); + + const { + data: { session }, + error: sessionError, + } = await supabase.auth.getSession(); + + if (sessionError) throw sessionError; + if (!session?.access_token) { + throw new Error("No se encontró una sesión activa para sincronizar el registro de horas."); + } + + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const response = await fetchWithTimeout(getWebhookUrl(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "sync_project", + project_id: cleanProjectId, + access_token: session.access_token, + source: "tablero-cdc-time", + }), + }); + + const text = await response.text(); + let payload: TimeSheetSyncResult | null = null; + + if (text) { + try { + payload = JSON.parse(text) as TimeSheetSyncResult; + } catch { + payload = null; + } + } + + if (!response.ok || payload?.ok === false) { + throw new Error(payload?.message || text || `Error ${response.status} al sincronizar Excel.`); + } + + return payload || { ok: true, projectId: cleanProjectId }; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error || "Error de sincronización")); + if (attempt < 3) await wait(attempt * 1_200); + } + } + + throw lastError || new Error("No se pudo sincronizar el registro de horas con Excel."); +} diff --git a/src/pages/BoardPage.tsx b/src/pages/BoardPage.tsx index f575466..38243da 100644 --- a/src/pages/BoardPage.tsx +++ b/src/pages/BoardPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { ChevronDown, ChevronLeft, @@ -69,6 +69,73 @@ const EMPTY_ADVANCED_FILTERS: AdvancedFilters = { cm: "", }; +const DEFAULT_PAGE_BY_FILTER: Record = { + nuevos: 1, + todos: 1, + abiertos: 1, + cerrados: 1, + no_aprobados: 1, +}; + +const BOARD_UI_STATE_KEY = "tablero-cdc:ui-state:v1"; + +type PersistedBoardUiState = { + query: string; + filter: BoardView; + advancedFilters: AdvancedFilters; + pageByFilter: Record; + tariffManagerOpen: boolean; + listManagerOpen: boolean; + scrollY: number; +}; + +function loadBoardUiState(): PersistedBoardUiState { + const fallback: PersistedBoardUiState = { + query: "", + filter: "todos", + advancedFilters: EMPTY_ADVANCED_FILTERS, + pageByFilter: DEFAULT_PAGE_BY_FILTER, + tariffManagerOpen: false, + listManagerOpen: false, + scrollY: 0, + }; + + if (typeof window === "undefined") return fallback; + + try { + const raw = window.localStorage.getItem(BOARD_UI_STATE_KEY); + if (!raw) return fallback; + const parsed = JSON.parse(raw) as Partial; + const validFilters: BoardView[] = ["nuevos", "todos", "abiertos", "cerrados", "no_aprobados"]; + const nextFilter = validFilters.includes(parsed.filter as BoardView) + ? (parsed.filter as BoardView) + : fallback.filter; + + return { + query: typeof parsed.query === "string" ? parsed.query : fallback.query, + filter: nextFilter, + advancedFilters: { + country: typeof parsed.advancedFilters?.country === "string" ? parsed.advancedFilters.country : "", + brand: typeof parsed.advancedFilters?.brand === "string" ? parsed.advancedFilters.brand : "", + client: typeof parsed.advancedFilters?.client === "string" ? parsed.advancedFilters.client : "", + cm: typeof parsed.advancedFilters?.cm === "string" ? parsed.advancedFilters.cm : "", + }, + pageByFilter: Object.fromEntries( + Object.entries({ ...DEFAULT_PAGE_BY_FILTER, ...(parsed.pageByFilter || {}) }).map(([key, value]) => [ + key, + Math.max(1, Math.floor(Number(value) || 1)), + ]), + ) as Record, + tariffManagerOpen: parsed.tariffManagerOpen === true, + listManagerOpen: parsed.listManagerOpen === true, + scrollY: Number.isFinite(Number(parsed.scrollY)) ? Math.max(0, Number(parsed.scrollY)) : 0, + }; + } catch (error) { + console.warn("No se pudo restaurar la vista anterior del Tablero CDC:", error); + return fallback; + } +} + function initials(name: string | null | undefined, email: string | null | undefined): string { if (name) { const parts = name.trim().split(/\s+/); @@ -80,21 +147,18 @@ function initials(name: string | null | undefined, email: string | null | undefi } export default function BoardPage() { + const [initialUiState] = useState(loadBoardUiState); + const hasMountedFilterReset = useRef(false); + const hasRestoredScroll = useRef(initialUiState.scrollY <= 0); const [open, setOpen] = useState(false); const [editing, setEditing] = useState(null); - const [query, setQuery] = useState(""); - const [filter, setFilter] = useState("todos"); - const [advancedFilters, setAdvancedFilters] = useState(EMPTY_ADVANCED_FILTERS); - const [pageByFilter, setPageByFilter] = useState>({ - nuevos: 1, - todos: 1, - abiertos: 1, - cerrados: 1, - no_aprobados: 1, - }); + const [query, setQuery] = useState(initialUiState.query); + const [filter, setFilter] = useState(initialUiState.filter); + const [advancedFilters, setAdvancedFilters] = useState(initialUiState.advancedFilters); + const [pageByFilter, setPageByFilter] = useState>(initialUiState.pageByFilter); const [deletingProjectId, setDeletingProjectId] = useState(null); - const [tariffManagerOpen, setTariffManagerOpen] = useState(false); - const [listManagerOpen, setListManagerOpen] = useState(false); + const [tariffManagerOpen, setTariffManagerOpen] = useState(initialUiState.tariffManagerOpen); + const [listManagerOpen, setListManagerOpen] = useState(initialUiState.listManagerOpen); const [timeProject, setTimeProject] = useState(null); const [reviewingBriefId, setReviewingBriefId] = useState(null); @@ -185,16 +249,65 @@ export default function BoardPage() { const visibleEnd = Math.min(pageStart + PROJECTS_PER_PAGE, currentTotalItems); useEffect(() => { + if (!hasMountedFilterReset.current) { + hasMountedFilterReset.current = true; + return; + } setPageByFilter((prev) => ({ ...prev, [filter]: 1 })); }, [ query, - filter, advancedFilters.country, advancedFilters.brand, advancedFilters.client, advancedFilters.cm, ]); + useEffect(() => { + const saveUiState = () => { + try { + const nextState: PersistedBoardUiState = { + query, + filter, + advancedFilters, + pageByFilter, + tariffManagerOpen, + listManagerOpen, + scrollY: hasRestoredScroll.current ? window.scrollY : initialUiState.scrollY, + }; + window.localStorage.setItem(BOARD_UI_STATE_KEY, JSON.stringify(nextState)); + } catch (error) { + console.warn("No se pudo guardar la vista del Tablero CDC:", error); + } + }; + + saveUiState(); + const handlePageHide = () => saveUiState(); + const handleVisibilityChange = () => { + if (document.visibilityState === "hidden") saveUiState(); + }; + + window.addEventListener("pagehide", handlePageHide); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.removeEventListener("pagehide", handlePageHide); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [query, filter, advancedFilters, pageByFilter, tariffManagerOpen, listManagerOpen, initialUiState.scrollY]); + + useEffect(() => { + const savedScrollY = initialUiState.scrollY; + const currentViewLoading = isBriefReviewView ? briefInbox.loading : loading; + if (savedScrollY <= 0 || currentViewLoading || hasRestoredScroll.current) return; + + const timer = window.setTimeout(() => { + window.scrollTo({ top: savedScrollY, behavior: "auto" }); + hasRestoredScroll.current = true; + }, 120); + + return () => window.clearTimeout(timer); + }, [initialUiState.scrollY, isBriefReviewView, briefInbox.loading, loading]); + useEffect(() => { if (currentPage > totalPages) { setPageByFilter((prev) => ({ ...prev, [filter]: totalPages })); diff --git a/supabase_tablero_cdc_dashboard_metrics.sql b/supabase_tablero_cdc_dashboard_metrics.sql new file mode 100644 index 0000000..922ba98 --- /dev/null +++ b/supabase_tablero_cdc_dashboard_metrics.sql @@ -0,0 +1,519 @@ +-- ============================================================================ +-- TABLERO CDC — MÉTRICAS PARA DASHBOARD FUTURO +-- Fecha: 2026-08-16 +-- +-- Objetivo: +-- 1) Registrar usuarios/sesiones que realmente abren Tablero CDC. +-- 2) Registrar eventos relevantes de proyectos sin depender del frontend. +-- 3) Exponer un resumen listo para un futuro dashboard. +-- +-- Es idempotente: puede ejecutarse nuevamente sin duplicar el backfill. +-- ============================================================================ + +create extension if not exists pgcrypto; + +create table if not exists public.tablero_cdc_app_metrics_events ( + id uuid primary key default gen_random_uuid(), + event_type text not null, + event_key text not null unique, + actor_user_id uuid references auth.users(id) on delete set null, + actor_email text, + actor_name text, + project_id uuid references public.tablero_cdc_projects(id) on delete set null, + session_id text, + metadata jsonb not null default '{}'::jsonb, + occurred_at timestamptz not null default now(), + created_at timestamptz not null default now() +); + +create index if not exists tablero_cdc_metrics_event_type_idx + on public.tablero_cdc_app_metrics_events (event_type, occurred_at desc); + +create index if not exists tablero_cdc_metrics_actor_idx + on public.tablero_cdc_app_metrics_events (actor_user_id, occurred_at desc); + +create index if not exists tablero_cdc_metrics_project_idx + on public.tablero_cdc_app_metrics_events (project_id, occurred_at desc) + where project_id is not null; + +alter table public.tablero_cdc_app_metrics_events enable row level security; + +-- El dashboard futuro podrá leer las métricas únicamente con usuarios activos +-- que ya tienen permiso para ver/controlar el resumen económico del Tablero. +drop policy if exists "tablero_cdc_metrics_select_authorized" on public.tablero_cdc_app_metrics_events; +create policy "tablero_cdc_metrics_select_authorized" +on public.tablero_cdc_app_metrics_events +for select +to authenticated +using ( + exists ( + select 1 + from public.tablero_cdc_allowed_users u + where lower(u.email) = lower(coalesce(auth.jwt() ->> 'email', '')) + and u.is_active = true + and coalesce(u.can_control_pricing_summary, false) = true + ) +); + +-- No damos INSERT directo al cliente. Las aperturas se registran vía RPC y +-- los eventos de proyecto vía trigger, evitando que el navegador falsifique métricas. +revoke insert, update, delete on public.tablero_cdc_app_metrics_events from authenticated; + +grant select on public.tablero_cdc_app_metrics_events to authenticated; + +-- -------------------------------------------------------------------------- +-- Helper de identidad para los eventos automáticos. +-- -------------------------------------------------------------------------- +create or replace function public.tablero_cdc_metric_actor( + p_user_id uuid, + out actor_email text, + out actor_name text +) +language plpgsql +security definer +set search_path = public, auth +as $$ +begin + actor_email := null; + actor_name := null; + + if p_user_id is null then + return; + end if; + + select + u.email, + coalesce( + nullif(u.raw_user_meta_data ->> 'full_name', ''), + nullif(u.raw_user_meta_data ->> 'name', ''), + u.email + ) + into actor_email, actor_name + from auth.users u + where u.id = p_user_id + limit 1; +end; +$$; + +revoke all on function public.tablero_cdc_metric_actor(uuid) from public; + +-- -------------------------------------------------------------------------- +-- RPC: una apertura por sesión autenticada/autorizada. +-- -------------------------------------------------------------------------- +create or replace function public.tablero_cdc_track_app_open(p_session_id text) +returns void +language plpgsql +security definer +set search_path = public, auth +as $$ +declare + v_uid uuid := auth.uid(); + v_email text; + v_name text; + v_session text := nullif(trim(coalesce(p_session_id, '')), ''); +begin + if v_uid is null then + raise exception 'Usuario no autenticado'; + end if; + + if v_session is null or length(v_session) > 200 then + raise exception 'session_id inválido'; + end if; + + select actor_email, actor_name + into v_email, v_name + from public.tablero_cdc_metric_actor(v_uid); + + if not exists ( + select 1 + from public.tablero_cdc_allowed_users u + where lower(u.email) = lower(coalesce(v_email, '')) + and u.is_active = true + ) then + raise exception 'Usuario no autorizado para Tablero CDC'; + end if; + + insert into public.tablero_cdc_app_metrics_events ( + event_type, + event_key, + actor_user_id, + actor_email, + actor_name, + session_id, + metadata + ) values ( + 'app_opened', + 'app_opened:' || v_uid::text || ':' || v_session, + v_uid, + v_email, + v_name, + v_session, + jsonb_build_object('app', 'tablero_cdc') + ) + on conflict (event_key) do nothing; +end; +$$; + +revoke all on function public.tablero_cdc_track_app_open(text) from public; +grant execute on function public.tablero_cdc_track_app_open(text) to authenticated; + +-- -------------------------------------------------------------------------- +-- Trigger de proyectos: creación + cambios de estado relevantes. +-- -------------------------------------------------------------------------- +create or replace function public.tablero_cdc_capture_project_metric() +returns trigger +language plpgsql +security definer +set search_path = public, auth +as $$ +declare + v_actor uuid; + v_email text; + v_name text; + v_old_status text; + v_new_status text; + v_event text; +begin + v_actor := coalesce(new.updated_by, new.created_by, auth.uid()); + + select actor_email, actor_name + into v_email, v_name + from public.tablero_cdc_metric_actor(v_actor); + + if tg_op = 'INSERT' then + insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at + ) values ( + 'project_created', + 'project_created:' || new.id::text, + v_actor, + v_email, + v_name, + new.id, + jsonb_build_object( + 'title', coalesce(new.title, ''), + 'client', coalesce(new.client, ''), + 'brand', coalesce(new.brand, ''), + 'country', coalesce(new.country, '') + ), + coalesce(new.created_at, now()) + ) + on conflict (event_key) do nothing; + + return new; + end if; + + v_old_status := lower(trim(coalesce(old.status, ''))); + v_new_status := lower(trim(coalesce(new.status, ''))); + + if v_old_status is not distinct from v_new_status then + return new; + end if; + + v_event := case + when v_new_status = 'aprobado' then 'project_approved' + when v_new_status = 'no aprobado' then 'project_rejected' + when v_new_status in ('', 'activo') then 'project_reopened' + else 'project_closed' + end; + + insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata + ) values ( + v_event, + v_event || ':' || new.id::text || ':' || gen_random_uuid()::text, + v_actor, + v_email, + v_name, + new.id, + jsonb_build_object( + 'title', coalesce(new.title, ''), + 'previous_status', coalesce(old.status, ''), + 'new_status', coalesce(new.status, '') + ) + ); + + return new; +end; +$$; + +drop trigger if exists trg_tablero_cdc_project_metrics on public.tablero_cdc_projects; +create trigger trg_tablero_cdc_project_metrics +after insert or update of status +on public.tablero_cdc_projects +for each row +execute function public.tablero_cdc_capture_project_metric(); + +-- -------------------------------------------------------------------------- +-- Trigger de tiempo: permite medir uso real y horas registradas históricamente. +-- -------------------------------------------------------------------------- +create or replace function public.tablero_cdc_capture_time_metric() +returns trigger +language plpgsql +security definer +set search_path = public, auth +as $$ +begin + insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at + ) values ( + 'time_logged', + 'time_logged:' || new.id::text, + new.created_by, + new.created_by_email, + coalesce(nullif(new.created_by_name, ''), new.created_by_email), + new.project_id, + jsonb_build_object( + 'country', coalesce(new.country, ''), + 'task_name', coalesce(new.task_name, ''), + 'duration_minutes', coalesce(new.duration_minutes, 0), + 'work_date', new.work_date + ), + coalesce(new.created_at, now()) + ) + on conflict (event_key) do nothing; + + return new; +end; +$$; + +drop trigger if exists trg_tablero_cdc_time_metrics on public.tablero_cdc_project_time_entries; +create trigger trg_tablero_cdc_time_metrics +after insert +on public.tablero_cdc_project_time_entries +for each row +execute function public.tablero_cdc_capture_time_metric(); + +-- -------------------------------------------------------------------------- +-- Métricas de Briefs (siempre útiles para este Tablero): recibidos/aprobados/ +-- rechazados. La tabla ya forma parte del módulo CDC Brief de esta versión. +-- -------------------------------------------------------------------------- +create or replace function public.tablero_cdc_capture_brief_metric() +returns trigger +language plpgsql +security definer +set search_path = public, auth +as $$ +declare + v_actor uuid; + v_email text; + v_name text; + v_event text; +begin + if tg_op = 'INSERT' then + insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, metadata, occurred_at + ) values ( + 'brief_received', + 'brief_received:' || new.id::text, + null, + null, + null, + jsonb_build_object('brief_id', coalesce(new.brief_id, ''), 'title', coalesce(new.title, '')), + coalesce(new.created_at, now()) + ) + on conflict (event_key) do nothing; + return new; + end if; + + if old.review_status is not distinct from new.review_status then + return new; + end if; + + v_event := case + when new.review_status = 'approved' then 'brief_approved' + when new.review_status = 'rejected' then 'brief_rejected' + else null + end; + + if v_event is null then + return new; + end if; + + v_actor := coalesce(new.reviewed_by, auth.uid()); + select actor_email, actor_name + into v_email, v_name + from public.tablero_cdc_metric_actor(v_actor); + + insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata + ) values ( + v_event, + v_event || ':' || new.id::text || ':' || gen_random_uuid()::text, + v_actor, + coalesce(new.reviewed_by_email, v_email), + coalesce(new.reviewed_by_name, v_name), + new.approved_project_id, + jsonb_build_object( + 'brief_id', coalesce(new.brief_id, ''), + 'title', coalesce(new.title, ''), + 'review_status', new.review_status + ) + ); + + return new; +end; +$$; + +drop trigger if exists trg_tablero_cdc_brief_metrics on public.tablero_cdc_brief_inbox; +create trigger trg_tablero_cdc_brief_metrics +after insert or update of review_status +on public.tablero_cdc_brief_inbox +for each row +execute function public.tablero_cdc_capture_brief_metric(); + +-- -------------------------------------------------------------------------- +-- Backfill seguro: proyectos y briefs existentes para que el futuro dashboard +-- no empiece desde cero. No duplica si se ejecuta otra vez. +-- -------------------------------------------------------------------------- +insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at +) +select + 'project_created', + 'project_created:' || p.id::text, + p.created_by, + au.email, + coalesce(nullif(au.raw_user_meta_data ->> 'full_name', ''), nullif(au.raw_user_meta_data ->> 'name', ''), au.email), + p.id, + jsonb_build_object( + 'title', coalesce(p.title, ''), + 'client', coalesce(p.client, ''), + 'brand', coalesce(p.brand, ''), + 'country', coalesce(p.country, ''), + 'backfill', true + ), + coalesce(p.created_at, now()) +from public.tablero_cdc_projects p +left join auth.users au on au.id = p.created_by +on conflict (event_key) do nothing; + +insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at +) +select + case when lower(trim(p.status)) = 'aprobado' then 'project_approved' else 'project_rejected' end, + case when lower(trim(p.status)) = 'aprobado' + then 'project_approved:backfill:' || p.id::text + else 'project_rejected:backfill:' || p.id::text end, + p.updated_by, + au.email, + coalesce(nullif(au.raw_user_meta_data ->> 'full_name', ''), nullif(au.raw_user_meta_data ->> 'name', ''), au.email), + p.id, + jsonb_build_object('status', p.status, 'backfill', true), + now() +from public.tablero_cdc_projects p +left join auth.users au on au.id = p.updated_by +where lower(trim(coalesce(p.status, ''))) in ('aprobado', 'no aprobado') +on conflict (event_key) do nothing; + +insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at +) +select + 'time_logged', + 'time_logged:' || t.id::text, + t.created_by, + t.created_by_email, + coalesce(nullif(t.created_by_name, ''), t.created_by_email), + t.project_id, + jsonb_build_object( + 'country', coalesce(t.country, ''), + 'task_name', coalesce(t.task_name, ''), + 'duration_minutes', coalesce(t.duration_minutes, 0), + 'work_date', t.work_date, + 'backfill', true + ), + coalesce(t.created_at, now()) +from public.tablero_cdc_project_time_entries t +on conflict (event_key) do nothing; + +insert into public.tablero_cdc_app_metrics_events ( + event_type, event_key, actor_user_id, actor_email, actor_name, + project_id, metadata, occurred_at +) +select + 'brief_received', + 'brief_received:' || b.id::text, + null, + null, + null, + b.approved_project_id, + jsonb_build_object('brief_id', coalesce(b.brief_id, ''), 'title', coalesce(b.title, ''), 'backfill', true), + coalesce(b.created_at, now()) +from public.tablero_cdc_brief_inbox b +on conflict (event_key) do nothing; + +-- -------------------------------------------------------------------------- +-- Vista resumen para el dashboard futuro. +-- Los conteos de estado salen del estado ACTUAL de proyectos, evitando inflar +-- métricas cuando un proyecto cambia de estado varias veces. +-- -------------------------------------------------------------------------- +create or replace view public.tablero_cdc_dashboard_metrics as +select + (select count(distinct actor_user_id) + from public.tablero_cdc_app_metrics_events + where actor_user_id is not null) as usuarios_que_han_usado_app, + + (select count(*) + from public.tablero_cdc_app_metrics_events + where event_type = 'app_opened') as sesiones_registradas, + + (select count(*) + from public.tablero_cdc_app_metrics_events + where event_type = 'project_created') as proyectos_creados_historico, + + (select count(*) from public.tablero_cdc_projects) as proyectos_actuales, + + (select count(*) + from public.tablero_cdc_projects + where lower(trim(coalesce(status, ''))) in ('', 'activo')) as proyectos_activos_actuales, + + (select count(*) + from public.tablero_cdc_projects + where lower(trim(coalesce(status, ''))) = 'aprobado') as proyectos_aprobados_actuales, + + (select count(*) + from public.tablero_cdc_projects + where lower(trim(coalesce(status, ''))) = 'no aprobado') as proyectos_rechazados_actuales, + + (select count(*) + from public.tablero_cdc_app_metrics_events + where event_type = 'project_approved') as aprobaciones_registradas, + + (select count(*) + from public.tablero_cdc_app_metrics_events + where event_type = 'project_rejected') as rechazos_registrados, + + (select count(*) + from public.tablero_cdc_project_time_entries) as registros_de_tiempo, + + (select round(coalesce(sum(duration_minutes), 0)::numeric / 60.0, 2) + from public.tablero_cdc_project_time_entries) as horas_registradas, + + (select count(*) from public.tablero_cdc_brief_inbox) as briefs_recibidos, + + (select count(*) + from public.tablero_cdc_brief_inbox + where review_status = 'approved') as briefs_aprobados, + + (select count(*) + from public.tablero_cdc_brief_inbox + where review_status = 'rejected') as briefs_rechazados, + + now() as consultado_en; + +grant select on public.tablero_cdc_dashboard_metrics to authenticated; + +comment on table public.tablero_cdc_app_metrics_events is + 'Eventos de uso y negocio de Tablero CDC para dashboard futuro de aplicaciones GLM.'; + +comment on view public.tablero_cdc_dashboard_metrics is + 'Resumen actual de usuarios, proyectos, tiempos y briefs de Tablero CDC.';