Javis OS
Javis OS is an Agentic AI that assists entrepreneurs in business tasks, helping them grow their work.
AI translation
Digitalizador de partes técnicos de servicio
Translation pending
BusinessJavaScript · HTMLGitHub @sebaBartoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗PWA (app web instalable) para que el técnico cargue el parte de servicio directamente en el celular, el cliente firme en pantalla, y se envíen copias por mail a la oficina y al cliente.
SAT-20260723-143205-482) y envía por mail, por SMTP directo desde
la casilla propia de la empresa:
No usa cámara ni lectura automática de fotos — todo se carga a mano.
Después de loguearse, el técnico ve un panel principal con botones grandes:
admin.html directo.Si en el futuro se agregan más secciones grandes, van como botones
nuevos en este mismo panel principal (.panel-tile en styles.css,
agregando el botón en screen-home de index.html).
La sesión queda guardada en el celular por 6 horas, y se renueva
sola cada vez que se abre la app dentro de ese plazo — así no hace
falta escribir la contraseña cada vez que se cierra la app. Si pasan
6 horas seguidas sin abrirla, vuelve a pedir usuario y contraseña. En
el panel principal hay un botón "Cerrar sesión" para salir a mano
(por ejemplo, si varios comparten un mismo celular). El tiempo de la
sesión es la constante SESION_DURACION_MS al principio de la sección
de login en app.js — se puede ajustar fácil si 6 horas queda corto
o largo.
Cada técnico tiene su propia contraseña, pero ahora se administra
desde admin.html (pestaña "Técnicos"), no hace falta tocar código
para agregar, sacar o cambiar la clave de alguien:
admin.html, pestaña "Técnicos".Los nombres que cargues ahí son los que van a aparecer automáticamente en el selector "Técnico" del formulario (ya no hace falta editar el HTML para eso tampoco).
Cuando un técnico entra con su contraseña, la app autocompleta el campo "Técnico" del formulario con su nombre (se puede corregir a mano si hiciera falta, por ejemplo si alguien usa el celular de otro técnico).
También existe una contraseña general de respaldo (APP_PASSWORD_GENERAL,
en app.js, para oficina o pruebas) que entra sin asociarse a ningún
técnico — en ese caso el campo "Técnico" queda para elegir a mano.
Esta lista se guarda en el mismo repo privado de datos
(tecnicos.json), reutilizando las variables de entorno que ya
existen — no hace falta configurar nada nuevo.
Es una validación solo del lado del celular (no hay usuarios ni login con servidor) — sirve para que no cualquiera que tenga la URL cargue partes y para saber quién entró, pero no es seguridad fuerte (cualquiera que abra el código de la página puede ver las contraseñas). Si más adelante se necesita un login real y protegido, hace falta un backend con usuarios.
Es un menú desplegable con los técnicos fijos de SAT, más la opción
"Otro..." que habilita un campo de texto libre. Para agregar o quitar
técnicos de la lista, se edita directamente en index.html, buscando
el <select id="f_tecnico">.
Apenas un técnico envía con éxito el parte de un servicio, ese servicio desaparece del listado de "Servicios pendientes" en su celular — no hace falta esperar a que se actualice el listado del ERP. Si esa misma tarea está en el cronograma semanal, también queda marcada ahí con un cartel verde "RESUELTO", para que se note de un vistazo que ya se hizo.
Es una marca local, por celular (no se sincroniza entre técnicos) y dura hasta que se cargue un listado de servicios nuevo desde el servidor (tocando "↻ Actualizar", o automáticamente al iniciar sesión) — en ese momento se recalcula todo según los datos reales del ERP.
En admin.html hay un campo opcional más al cargar el listado:
"Fecha de ingreso" (cuándo se dio de alta el servicio en el ERP).
Si se mapea esa columna, en el listado de la app cada servicio que
lleva varios días sin resolverse se marca solo:
Los umbrales (3 y 7 días por defecto) se editan desde admin.html →
pestaña "Servicios pendientes" → arriba de todo ("Umbrales de servicio
estancado") — no hace falta tocar código. Se guardan en el mismo repo
privado de datos (config.json), reutilizando las variables de
entorno que ya existen. Si no se carga la fecha de ingreso de un
servicio, simplemente no aparece ninguna alerta para ese caso (no es
obligatorio).
La app tiene una pantalla de "Servicios pendientes" después del login, que
descarga un listado desde un endpoint propio (/api/servicios.js, función
serverless de Vercel) y precarga cliente/dirección/localidad/teléfono/tarea
en el formulario cuando el técnico toca un servicio. Si el servicio tiene
teléfono cargado, aparece un botón de llamada (📞) al lado del campo
Cliente en el formulario.
Los datos nunca quedan en un archivo público — se guardan en un repo
privado de GitHub aparte, y solo se sirven si se manda la clave secreta
correcta. La carga se hace a mano desde admin.html, subiendo el CSV o
Excel que exporta el ERP (2 veces por día alcanza). Si el archivo tiene
título o filtros arriba del encabezado real (común en reportes de ERP),
admin.html intenta adivinar en qué fila está el encabezado, pero se
puede corregir a mano.
En la app, la pantalla de servicios pendientes tiene un buscador arriba para filtrar por cliente, dirección, N° de servicio o tarea.
Crear un repo privado nuevo en GitHub, solo para los datos (ej.
sat-servicios-data). Que quede Private. Puede estar vacío.
Crear un Personal Access Token (fine-grained) en GitHub, distinto del que se usa para el repo de la app:
sat-servicios-data)Elegir una clave secreta larga (por ejemplo, generada con un
gestor de contraseñas). Nunca va en el código de la app ni de
admin.html: vive solo como variable de entorno del servidor. La app
y el panel usan un pase personal que les da el login (ver
lib/sesion.js y herramientas/cambiar-clave-servidor.ps1).
Cargar las variables de entorno en Vercel (Project Settings → Environment Variables):
SERVICIOS_API_TOKEN = la misma clave del paso 3GITHUB_DATA_TOKEN = el token del paso 2GITHUB_DATA_REPO = tu-usuario/sat-servicios-dataGITHUB_DATA_PATH = servicios.jsonEntrar a tu-app.vercel.app/admin.html, subir el CSV del ERP, y
asignar qué columna corresponde a cada dato (se recuerda para la
próxima vez). Al confirmar, ya queda disponible en la app de los
técnicos.
Hasta la v3.5.3 los mails los mandaba EmailJS — se dejó de usar porque
el plan tiene un límite mensual de envíos, y al mandar 2 mails por
cada parte (oficina + cliente) se llenaba rápido. Ahora los manda
directo la app, por SMTP, usando una casilla propia de la empresa
([email protected], alojada en DonWeb) — sin límite de
un tercero.
Cargar estas variables de entorno en Vercel (Project Settings → Environment Variables), y hacer un redeploy después:
| Variable | Valor |
|---|---|
SMTP_HOST | El servidor de correo saliente de tu cuenta (lo ves en DonWeb → tu casilla → "Correo Saliente" → Servidor; para SAT es ai000077.ferozo.com) |
SMTP_PORT | 465 |
SMTP_USER | La casilla que manda los mails ([email protected]) |
SMTP_PASS | La contraseña de esa casilla |
OFICINA_EMAIL | A qué mail fija llega SIEMPRE la copia de "oficina" de cada parte |
No hace falta tocar nada más — /api/enviar-mail.js ya tiene las
plantillas de oficina y de cliente incorporadas (mismo diseño que
tenían en EmailJS), y app.js ya llama a este endpoint en vez de
EmailJS.
La importación de Excel del stock (pestaña "SIMs", no la de
"SIMs instaladas") tenía un bug serio: subir una planilla
reemplazaba toda la tabla, incluidas las SIMs que los técnicos ya
tenían asignadas — si se guardaba así, se perdían esas asignaciones.
Ahora la importación se mezcla con lo que ya está cargado (por
número de línea): si el número ya existía, se actualiza; si es
nuevo, se agrega — nunca borra nada. También se sumó la columna
EMPRESA (además de EMPRESA PROVEEDORA) al reconocimiento de
encabezados, y se normaliza a Mayúscula inicial (Movistar/Personal/
Claro) para que quede igual que el resto de la app.
En "📋 Instalar en cliente" → "Otro (escribir todo a mano)", ahora se puede cargar, además del nombre del cliente, el N° de abonado y la dirección — así la línea queda con la ficha completa en el registro de instaladas desde el momento en que se carga, igual que las que vinieron de la planilla original. Elegir un servicio pendiente de la lista sigue autocompletando cliente y dirección (el N° de abonado no, porque los servicios no tienen ese dato) — y todo queda editable por si hace falta ajustar algo antes de confirmar.
Las claves cargadas vivían solo en la memoria de la página — si el técnico las cargaba, salía de la pantalla, y la app se cerraba o el celular la mataba en segundo plano (algo común en Android), se perdían. Ahora se guardan en el mismo borrador que ya usa el resto del parte: se puede abrir "Claves" en cualquier momento del servicio, cargar algo, salir, y va a seguir ahí hasta que se envíe el parte — se probó cargando solo una clave (sin tocar ningún otro campo del formulario) y confirmando que igual se guarda y se restaura completa al reabrir el mismo servicio. De paso se corrigió el mismo hueco para el número de presupuesto (opción "Por presupuesto"), que tenía el mismo problema.
Encontradas revisando la app con más cuidado (dos eran mías, del cambio anterior; una ya estaba desde antes):
Sumado a la dirección, ahora al elegir un cliente ya cargado para instalar una SIM nueva, también se toma solo el número de abonado de la base de Clientes (se muestra como texto justo antes del botón, para confirmar que se tomó el correcto) y se guarda junto con la SIM. De paso revisé el flujo de "reemplazar" (cambiar el chip físico de un cliente que ya tenía línea instalada) — ese ya heredaba bien el número de abonado de la SIM vieja, no necesitaba cambios.
Reemplacé el desplegable de cliente (incómodo con más de 1000 clientes) por un campo de texto con sugerencias, igual al patrón que ya usamos en Emergencias — escribís y completa solo (ignorando mayúsculas/tildes), mostrando "✓ Cliente encontrado en la base" cuando coincide.
De paso, esto corrige la causa real de un problema reportado: el desplegable viejo mezclaba nombres de la base de Clientes con nombres sueltos escritos en servicios pendientes (texto libre, sin garantía de coincidir) — si se elegía la versión "suelta", no enganchaba con los datos reales ni completaba nada. Ahora la lista sale solo de la base de Clientes, la fuente confiable.
A raíz de la consulta sobre si se autocompletan los datos del cliente al instalar una
SIM: la respuesta es sí, pero en admin.html pasaba en silencio (recién se aplicaba al
guardar, sin verse en pantalla) — podía generar dudas sobre qué datos se iban a usar
realmente. Ahora, apenas el nombre escrito coincide con un cliente real de la base
(ignorando mayúsculas/tildes), el número de abonado y la dirección se completan solos
en los campos, con un aviso "✓ Cliente encontrado en la base" — y si no coincide con
ninguno, avisa que es un cliente nuevo y se va a guardar tal cual se escribió.
Investigando un reporte de una SIM que parecía haber desaparecido al reinstalarla en un cliente: los datos nunca se perdieron — quedó guardada correctamente, pero bajo el nombre "Bartolozzi Sebastián" (con tilde, tal como se escribió a mano), mientras la búsqueda posterior fue sin tilde y no la encontraba por una comparación de texto exacta.
Dos correcciones: la búsqueda de líneas instaladas ahora ignora mayúsculas y tildes. Y,
más importante para que esto no vuelva a pasar — en admin.html, al instalar una SIM en
un cliente, el campo ahora es un buscador con sugerencias tomadas de la base completa de
Clientes (antes solo mostraba servicios pendientes, con "escribir a mano" como única
alternativa); si lo que se escribe coincide con un cliente real, se usa el nombre exacto
como está guardado en la base, en vez de lo que se haya tipeado.
Nueva pantalla ("Historial de stock", con permiso propio), que se completa sola a partir de lo que el técnico ya carga al terminar un parte — sin pantallas nuevas para cargar a mano. Cada movimiento queda registrado con cliente, dirección, fecha, hora, técnico, material, y cuánto se instaló / cuánto se retiró (puede ser una fila con las dos cantidades a la vez, por ejemplo un reemplazo).
Antes, "materiales retirados" era solo un campo de texto libre — le agregué el mismo selector estructurado (categoría/modelo/cantidad) que ya tenía "materiales utilizados", para que las cantidades queden confiables y no dependan de cómo cada uno lo escriba a mano.
Mismo sistema de "pasado a mi sistema" que ya armamos para el historial de partes (mismo permiso, misma confirmación antes de marcar, mismo fondo verde) — así se puede chequear de un vistazo qué movimientos de stock ya se cargaron en el sistema offline de la oficina.
Backend nuevo: tabla stock_movimientos (una fila por material que tuvo movimiento en
un parte), con protección contra duplicados si el mismo parte se reintenta guardar.
La pantalla de Historial ya no está limitada a los últimos 4 días — ahora tiene los mismos filtros que los demás dashboards (4 días / semana / mes / todo / fecha puntual).
Nuevo permiso ("Marcar partes como 'pasado a mi sistema'", pensado para uso de oficina): quien lo tenga ve un tilde en cada parte, que al tocarlo pide confirmación ("¿Confirmás que el parte N° X ya está pasado a tu sistema?") antes de marcarlo — así se evitan clics accidentales. Una vez marcado, la tarjeta pasa a fondo verde y muestra quién lo marcó, para poder chequear de un vistazo qué partes ya se cargaron a mano en el sistema offline de la oficina y cuáles faltan.
Nuevo en el backend: tres columnas (pasado_sistema_offline, pasado_sistema_por,
pasado_sistema_en) y un endpoint para marcar/desmarcar, con quién y cuándo, no solo
un tilde suelto.
Corrige un bug real reportado: al resolver un servicio desde el Cronograma, si el nombre del cliente estaba escrito en un orden distinto entre el cronograma y la lista de servicios pendientes (ej: "Marta De Simone" vs "De Simone Marta"), la app no lo reconocía como el mismo servicio — armaba el parte "suelto", sin el número real. Esto explicaba tres problemas juntos que se estaban viendo: el mail a oficina mostraba un número armado por la app en vez del número real del servicio, ese servicio seguía apareciendo como pendiente después de resuelto, y (probablemente relacionado) los materiales no quedaban registrados en el Historial de Stock.
Ahora el matching no exige que el nombre esté en el mismo orden — alcanza con que todas las palabras del nombre del cliente aparezcan en el texto de la tarea, en cualquier orden. Probado con el caso real (orden invertido), con espacios de más, y confirmando que sigue sin generar falsos positivos con nombres parecidos pero distintos.
A pedido: al entrar al Cronograma, ahora busca el día de hoy entre los que estén cargados y arranca ahí directo (con el chip correspondiente desplazándose a la vista si hace falta) — en vez de mostrar siempre el primer día disponible, más allá de que sea uno viejo. Si "hoy" no está cargado todavía, sigue mostrando el primero, como antes.
Sobre lo otro reportado (el cronograma no mostraba días después del 5 de agosto): se revisó el código completo — no hay ningún límite artificial de fechas ni de cantidad de días, se procesan todas las hojas del Excel que haya en el archivo de Drive. Es prácticamente seguro que el archivo en Drive todavía no tenía cargadas las hojas de esa semana — un tema de carga de datos en la oficina, no un bug de la app.
Encuesta de satisfacción: el mail que recibe el cliente al completarse un servicio
ahora tiene 5 caritas (😞🙁😐🙂😄) — con un toque, califica el servicio de 1 a 5. Cada
parte se puede calificar una sola vez (tocar el link de nuevo solo muestra "gracias,
ya habías calificado", sin cambiar el puntaje). No hace falta ningún login ni token —
es un link público pensado para clickearse directo desde el mail. Nueva pestaña
"Satisfacción" en admin.html, con el promedio general y el listado completo de
calificaciones recibidas, con cliente y técnico.
Animaciones: los botones y tiles principales tienen un efecto más marcado al tocarlos (un poco más de "hundido", con un resplandor sutil en los botones principales). Las tarjetas de todos los dashboards (Historial, Stock, Presencia, Vehículos, Herramientas, SIMs) ahora aparecen con una animación de entrada suave y escalonada — una tras otra, en vez de aparecer todas de golpe.
Backend nuevo: tabla encuestas_satisfaccion, con protección contra recalificar el
mismo parte. Probado de punta a punta: calificar sin token (como llega desde el mail),
intentar recalificar (avisa que ya calificó), los 5 links del mail bien armados con el
número de parte correcto, y el promedio calculado bien en admin.html.
A pedido: página nueva solicitar-servicio.html, pública y sin login — para
cuando un cliente escribe fuera de horario (WhatsApp, redes, etc.) y el mensaje se
puede perder. El cliente completa nombre, teléfono, dirección (opcional) y qué
necesita; al enviarlo:
admin.html) — marcado con
cargado_por: "Formulario web (cliente)" para distinguirlo de un lado
a simple vista.Backend nuevo: POST /api/emergencias/solicitud-publica en Cloudflare — a propósito
una ruta separada del endpoint que ya existía (ese espera y reemplaza la lista
completa, pensado solo para la app ya logueada; este solo agrega una fila nueva, sin
poder tocar ni borrar las que ya había — la única forma segura de exponerlo sin
login). En Vercel se sumó como una excepción puntual dentro de api/datos.js (ya se
había llegado al límite de 12 funciones), con el mismo patrón usado para la encuesta
de satisfacción.
Probado de punta a punta: envío exitoso (guarda + dispara el push con el mensaje
correcto), rechazo si faltan datos (sin llegar a tocar el backend), y que la
seguridad del resto de las rutas de api/datos.js sigue intacta.
Pendiente de tu lado: el link de WhatsApp al pie de la página (wa.me/...) tiene
un número de ejemplo — hay que reemplazarlo por el número real de la empresa antes de
compartir el link. También conviene configurar la respuesta automática de WhatsApp
Business fuera de horario, apuntando a este mismo link.
A pedido: ya no hace falta que el técnico busque y toque "Activar notificaciones" — se
dispara solo al entrar por primera vez (mostrando el cartel nativo del navegador para
pedir el permiso). Solo se pregunta una vez: si nunca se le preguntó antes
(Notification.permission === "default"), se intenta activar solo; si ya lo había
rechazado explícitamente, no se vuelve a insistir. El botón sigue estando disponible
siempre para activar/reactivar a mano cuando se quiera.
Nota técnica: los navegadores nunca permiten activar notificaciones push sin que el usuario apruebe el permiso nativo — no hay forma de saltarse ese paso, es una restricción de seguridad de todos los navegadores. Esto logra que, en la práctica, la mayoría termine con las notificaciones activadas sin buscar nada, en vez de estar "on" sin que nadie lo haya aprobado nunca (lo cual no es técnicamente posible).
A pedido: en "Agenda de emergencia", nuevo botón "📅 Ver ocupación de la semana" — muestra los próximos 7 días, combinando el Cronograma (🔧 lo que ya está programado por técnico y horario) y las emergencias ya agendadas (🚨), para poder ofrecerle al cliente una fecha/hora que realmente esté libre, sin pisarse con nada ya comprometido. Los días sin nada agendado se marcan claramente como "día libre".
Probado de punta a punta: combina correctamente ambas fuentes de datos, agrupa por día, y distingue visualmente cada tipo de ocupación.
Corrige un caso real: si el técnico escribía muy rápido apenas entraba al formulario, las sugerencias podían no aparecer aunque el cliente existiera — la lista se carga en segundo plano al loguearse, y el buscador solo revisaba si ya estaba lista, sin esperarla. Ahora espera activamente a que termine de cargar antes de decidir que no hay resultados (sin demora perceptible en el uso normal, donde ya está lista de antes). Probado simulando una red lenta.
A pedido: al cargar un parte "sin servicio" (o cualquier parte, en realidad), el campo Cliente ahora sugiere clientes ya cargados en la base a medida que se escribe — busca tanto por nombre como por dirección al mismo tiempo (por eso no es un desplegable nativo, que solo filtra por lo que ya se ve en el texto). Al elegir una sugerencia, se completa sola la dirección y localidad, y queda vinculado el número de cliente — lo mismo que ya beneficia al botón de INFO y a las claves guardadas por cliente. Si el técnico escribe el nombre exacto sin usar la sugerencia, también se vincula solo.
Probado de punta a punta: búsqueda por nombre, búsqueda por dirección (trayendo varios clientes de la misma calle), y el autocompletado completo al elegir una sugerencia.
A pedido: hasta ahora las claves cargadas en un parte quedaban atadas solo a ese parte puntual — no había forma de preguntar "¿cuál es la clave de este cliente?" sin ir a buscar en qué servicio se cargó. Ahora se van acumulando en un registro por cliente (usando el número de cliente, no el nombre — así no se pierde nada si el cliente cambia de titular): cada vez que se completa un parte con claves, se suman al registro de ese cliente, actualizando la que ya existía si el título coincide, o agregando una nueva si no.
Se puede consultar desde dos lugares:
admin.html: nueva pestaña "Claves por cliente", con lista buscable y
detalle completo por cliente.Backend nuevo: tabla clientes_claves (migracion_v19.sql). Probado de punta a
punta: guardar dos partes distintos para el mismo cliente (uno actualiza una clave
existente, el otro agrega una nueva), consultar el resultado combinado, y verificar
que se ve bien tanto en el botón de INFO como en la pestaña nueva de admin.html.
La cantidad ya funcionaba correctamente en modo "Otro (escribir el material)", pero quedaba visualmente separada del campo de texto — el selector de −/+ parecía asociado al desplegable de arriba, no al material que se estaba escribiendo. Reorganizado para que el selector de cantidad quede siempre en la misma fila que el campo que esté activo en ese momento (el desplegable, o el texto manual) — y se agregó un link "← Volver al catálogo" para salir del modo manual sin perder la categoría elegida.
Tres mejoras al parte técnico:
Botón de INFO corregido de raíz: el campo Cliente nunca guardaba el número de
cliente, solo el nombre — así que si un cliente cambiaba de titular (caso real:
"Massa Maria Victoria" pasó a llamarse "Inmobiliaria massa" entre dos visitas), la app
no podía relacionar el historial. Ahora se guarda numero_cliente con cada parte
(cuando viene de un servicio pendiente), y "última visita" compara primero por ese
número — estable aunque cambie el nombre — y solo cae al nombre como respaldo si no
hay número disponible. Probado con el caso real.
Hora de salida automática: al tocar "Continuar" hacia la firma, si el campo está vacío, se completa solo con la hora del celular en ese momento — sin pisar nada si ya tenía un valor cargado (por ejemplo, un borrador restaurado).
Cantidad de materiales sin tipear: tanto en instalados como en retirados (incluido el material cargado a mano), el campo de cantidad ahora es un selector con botones −/+ en vez de un teclado numérico — no se puede escribir directamente, arranca en 1 y no baja de ahí.
Backend nuevo: columna numero_cliente en la tabla de partes (migracion_v18.sql).
Probado de punta a punta: la vinculación por número de cliente, la hora automática, y
el stepper de cantidad (sumar, restar, tope en 1, y que vuelve a 1 después de agregar).
Corrige un bug real y bastante serio (caso Orieta Dario, parte N° 37445): cuando un parte queda encolado por falta de señal y se reintenta más tarde, los movimientos de Stock se armaban con el estado en vivo del formulario en ese momento — no con los materiales que realmente tenía guardado ese parte puntual. Si el técnico ya había empezado a cargar otro servicio distinto para cuando el reintento se disparaba solo, el material que llegaba a Stock era el del parte equivocado (o ninguno, si el formulario ya estaba vacío) — mientras que el mail sí mostraba lo correcto, porque ese usa los datos guardados del parte, no el estado en vivo.
Ahora los movimientos de Stock se arman siempre a partir de los materiales guardados en el parte que se está enviando, sin importar qué haya en pantalla en ese momento. Probado de punta a punta reproduciendo el escenario exacto: parte encolado con un material, el técnico empieza otro parte distinto antes de que se reintente el primero, y confirmando que Stock recibe el material correcto del parte original.
A raíz de un caso real (Sebastian veía datos de antes del arreglo de SIMs, aunque el
número de versión mostrado ya decía 3.62.1): Vercel no tenía ninguna configuración de
caché para sw.js (el Service Worker), así que el navegador podía guardarse una copia
vieja de ese archivo por su cuenta — y si nunca nota que cambió, jamás dispara el
mecanismo de actualización interno de la app (que si estaba bien armado). Ahora
sw.js se sirve siempre con Cache-Control: no-cache, obligando al navegador a
consultarlo de nuevo en cada visita — así detecta apenas hay una versión nueva y se
actualiza solo, sin depender de que alguien toque manualmente "Actualizar app".
Recordatorio: ya existe un botón "↻ Actualizar app a la última versión" en la pantalla de login, que fuerza una limpieza completa de caché — sirve como solución inmediata para cualquier caso viejo que haya quedado pegado de antes de este arreglo.
Hasheo de la contraseña: ADMIN_PASSWORD guardaba la contraseña en texto plano en
la configuración de Vercel — ahora se verifica contra un hash (scrypt, incluido en
Node, sin depender de ningún paquete externo), guardado como ADMIN_PASSWORD_HASH.
Ni con acceso a la configuración de Vercel se puede leer la contraseña real, solo
confirmar si un intento coincide. Se agregó generar-hash-admin.js — un script para
correr en la propia PC (node generar-hash-admin.js "TuContraseñaNueva") que genera
el hash sin que la contraseña real viaje a ningún lado. Mientras no se configure
ADMIN_PASSWORD_HASH, sigue funcionando con la ADMIN_PASSWORD vieja como respaldo,
para no dejar el panel sin acceso de un día para el otro.
Dominio propio para el formulario: pedido.sat365.com.ar (a configurar) apunta
directo a solicitar-servicio.html — así el link que se comparte con clientes no
muestra el dominio de Vercel. De paso, /admin.html y /index.html quedan
bloqueados específicamente en ese dominio (rebotan a la página del formulario) — una
capa extra, sin ser una protección completa por sí sola.
Corrige un bug real reportado (caso Siri Carlos): al instalar una SIM en un cliente que ya tenía una línea, la app no la detectaba — comparaba el nombre del cliente con el nombre guardado en la SIM instalada de forma exacta (letra por letra), así que cualquier diferencia mínima (mayúsculas, un espacio de más) hacía que nunca encontrara la coincidencia, y por lo tanto nunca ofrecía "reemplazar o agregar segunda línea".
Corregido con el mismo matching tolerante que ya se usa en el Cronograma — alcanza con que las palabras del nombre coincidan, sin importar mayúsculas, tildes, o espacios de más. Probado con el caso real (nombre guardado con espacios extra y en minúsculas): ahora detecta correctamente la línea existente y ofrece las dos opciones.
Corrige lo reportado: el campo de texto libre "Otros materiales" (para lo que no estuviera en las categorías) salía bien en el mail, pero nunca llegaba al Historial de Stock — solo lo agregado con el selector de categoría/modelo llegaba ahí.
Ahora el selector estructurado (tanto para instalados como para retirados) tiene una opción "Otro (escribir el material)" — al elegirla, aparece un campo para escribirlo a mano, y ese material sí queda registrado en Stock, marcado con una etiqueta "✎ Cargado a mano" bien visible en cada tarjeta, para poder controlarlo fácilmente de un vistazo. También se agregó el mismo aviso que ya existía para materiales/claves sin agregar, ahora también para el selector de retirados.
Backend nuevo: columna es_manual en stock_movimientos. Probado de punta a punta:
elegir "Otro", escribir un material, agregarlo, y confirmar que el movimiento final
queda marcado correctamente.
Rediseño grande a pedido, en base a un pedido detallado de reglas de negocio y a problemas reales encontrados en capturas del panel:
Corrección de datos: se encontraron y limpiaron 912 SIMs que estaban a la vez en el stock de un técnico/Oficina Y instaladas en un cliente (un número no puede estar en dos lugares) — venían de una carga masiva vieja que nunca chequeó contra las ya instaladas. La carga masiva ya está corregida para que esto no vuelva a pasar (salta automáticamente cualquier número que ya figure instalado).
Estado Físico (Disponible / Defectuosa / Extraviada): nuevo, en la app del técnico — al marcar una SIM como Defectuosa o Extraviada, sale del stock disponible para instalar hasta que se vuelva a marcar como Disponible.
Transferencia con confirmación: transferir una SIM a otro técnico ahora queda pendiente hasta que el destino la Acepte o Rechace — mientras tanto, ni el origen ni nadie más la puede usar. Transferir a "Oficina" sigue siendo inmediato (no tiene sentido pedirle confirmación al stock central). Nueva sección "Te están transfiriendo" en Mis SIMs, con los botones de Aceptar/Rechazar.
Formato dinámico por proveedor: en el alta desde admin.html, elegir Claro o
Personal fija el formato en "Estándar" automáticamente (deshabilitado) — solo Movistar
tiene la opción Mini.
Limpieza visual en admin.html: se sacó la columna "En uso/En stock" (redundante,
generaba confusión), y la columna "Asignado a" ahora muestra el cliente para las
SIMs instaladas en vez del técnico que la instaló históricamente — esa fila queda de
solo lectura (no tiene sentido editarla ni transferirla desde esta tabla; se retira
desde la app del técnico).
Probado de punta a punta: transferencia pendiente + aceptar + rechazar, estado físico
bloqueando instalación, limpieza masiva sin tocar SIMs sanas, y la tabla de
admin.html con los tres tipos de fila (Oficina/técnico/cliente) renderizando y
comportándose bien.
Tres mejoras a pedido, todas relacionadas con el trabajo en equipo en una instalación:
admin.html.El mail de resumen y la vista en admin.html ahora muestran, para cada día trabajado,
qué técnico específico lo marcó (no solo quién abrió la instalación en un principio).
Backend nuevo: tabla instalaciones con columna de observaciones, y un endpoint para
listar todas las instalaciones abiertas (usado para el "sumarme"). Probado de punta a
punta con dos técnicos distintos: uno abre y marca, el otro lo encuentra y se suma,
carga datos, intenta cerrar sin haber marcado salida (se lo frena sin perder nada),
marca salida, y cierra con éxito con todo intacto.
A raíz de un caso real reportado (un parte con claves cargadas que no aparecieron en el mail), se encontraron y corrigieron dos problemas independientes:
claves nunca se
mandaba — se usaba solo para el mail, pero jamás quedaba guardado. Esto significa que,
aunque el mail hubiera salido bien, esa información no era recuperable después. Ya
corregido — se guarda como corresponde, y ahora las claves se pueden ver
directamente en el Historial de la app, en cada parte que las tenga cargadas — así
quedan siempre accesibles, no solo en el mail del momento.Probado de punta a punta: guardar un parte con una clave y volver a leerlo trae la información intacta, y se ve correctamente en la tarjeta del Historial.
Corrige lo reportado: al recuperar una instalación abierta (por ejemplo, al entrar de nuevo para marcar el segundo día), el nombre del cliente no quedaba guardado en el campo correspondiente — así que al tocar "Marcar entrada de hoy" se mandaba vacío, y el backend correctamente lo rechazaba con "Falta el técnico o el cliente". Ahora, al recuperar la instalación abierta, también se restaura el nombre del cliente (y la dirección) — probado de punta a punta: abrir, marcar el día 1 completo, salir, volver a entrar, y confirmar que la entrada del día 2 se manda con el cliente correcto, sin error.
Nuevo: en admin.html → Instalaciones → detalle de una instalación abierta, aparece
un botón "🗑 Borrar esta instalación" (con confirmación antes de borrar). Borra todo lo
relacionado — días marcados, zonas, canales, y fotos en R2. Las instalaciones cerradas
quedan protegidas — no se puede borrar una una vez cerrada (representa trabajo ya
terminado), ni desde acá ni directamente contra el backend. Probado de punta a punta:
el botón aparece solo en abiertas, y el borrado limpia todo correctamente.
Corrige lo reportado: al volver a entrar a "Instalación" después de haber abierto una y salido de la pantalla, ahora se fija primero si el técnico tiene alguna instalación sin cerrar — si la hay, la retoma directo (con los días ya marcados, lista para seguir marcando o cerrar), en vez de mostrar siempre la pantalla de "abrir una nueva" como si no existiera. Probado de punta a punta: abrir, marcar un día completo, salir a la pantalla principal, volver a entrar, y confirmar que la recupera con el día visible.
Se detectó que el aviso semanal de guardia a Security24 no se estaba mandando — el disparador automático de Cloudflare no estaba corriendo, sin errores visibles en el código. Se agregó:
[email protected] y
[email protected].GET /api/guardias/diagnostico, en el backend): muestra en cualquier
momento, sin mandar nada, qué pasaría si el aviso corriera ahora — a quién le tocaría,
si ya se avisó esta semana, etc.POST /api/guardias/avisar-ahora, en el backend): dispara el aviso
real a mano, con la misma protección contra duplicados que el automático — para
cuando el disparador automático no corre y hay que mandarlo sin esperar al lunes
siguiente.Corregido: el tile "Instalación" no mostraba ícono — era un desajuste de nombres de
clase CSS que quedó de la fusión de tiles (tile-instalador en el HTML, el color solo
estaba definido para tile-instalacion).
Nuevo: al cerrar una instalación, se manda automáticamente un mail a
[email protected] con el resumen completo — días trabajados (con links de
ubicación de cada entrada/salida), zonas de alarma, canales de cámaras, y las fotos
adjuntas directamente al mail (no solo un link, para tener todo a mano sin entrar
al sistema). Probado de punta a punta: mail armado correctamente, con las fotos
adjuntas, todos los datos, y los links de mapa funcionando.
Corrige lo reportado: antes, al completar un parte, el servicio solo se escondía en el celular de quien lo completó — nunca se sacaba de la lista compartida, así que reaparecía apenas se actualizaba (y siempre estuvo visible para el resto del equipo). Ahora, al completar con éxito, además se trae la lista fresca del servidor, se saca ese servicio, y se guarda — así desaparece para todos, no solo localmente. No frena el flujo de "listo" (corre en paralelo); si por algún motivo falla, el servicio simplemente queda en la lista como antes, sin romper nada.
Tres ajustes a pedido:
admin.html → Instalaciones → detalle, cada día
marcado ahora tiene un link "📍 ver ubicación" (abre Google Maps con el punto exacto,
con la precisión en metros) tanto para la entrada como para la salida — o un aviso
claro de "(sin ubicación)" si el técnico no dio permiso de GPS ese día.Probado de punta a punta: el tile abre directo la pantalla (sin submenú), el nombre
libre funciona sin depender de la base de clientes, la geolocalización se sigue
capturando y mandando correctamente, y los links de mapa en admin.html apuntan a las
coordenadas correctas.
Si un técnico tiene una instalación de varios días abierta, la pantalla suelta de "Presencia en obra" ya no le muestra el botón de "Marcar llegada" (que antes le daba un error confuso) — en su lugar, le avisa claramente que tiene una instalación abierta en ese cliente y lo redirige a marcar desde ahí, evitando dos lugares distintos haciendo lo mismo.
Rediseño completo del flujo de Instalación, para instalaciones que llevan varios días:
En admin.html → pestaña Instalaciones, cada entrada de la lista muestra si está
abierta o cerrada, y el detalle ahora incluye también los días trabajados
(entrada y salida de cada uno), además de zonas, canales y fotos.
Backend nuevo: instalaciones tiene un campo de estado, y presencias_obra se puede
vincular a una instalación puntual (instalacion_id). Probado de punta a punta contra
una base real: 3 días completos, rechazo de cierre con una marca abierta, cierre
exitoso, y bloqueo de nuevas entradas después de cerrado — y también probado con clics
reales en la pantalla, con los mismos resultados.
A pedido: "Presencia en obra" e "Instalación" ya no son dos tiles sueltos en la pantalla principal — ahora hay un único tile "Instalador" (con permiso propio, apagado por defecto) que abre un submenú con las dos pantallas adentro, mismo patrón que ya usa "Dashboards". Los botones "Volver" de esas dos pantallas ahora vuelven a ese submenú en vez de al home.
Nueva pestaña "Instalaciones" en admin.html, para que la oficina revise el
detalle de cada instalación sin tener que entrar como técnico — lista buscable por
cliente o técnico, y al tocar una se ve el detalle completo: zonas de alarma, canales
de cámaras, y las fotos subidas.
Probado con Playwright de punta a punta: el tile no aparece sin el permiso, aparece
con el permiso, el submenú muestra las dos opciones, y volver desde cualquiera de las
dos lleva al submenú correctamente. También la pestaña nueva de admin.html: lista,
búsqueda con filtro, detalle completo con zonas/canales/fotos, y volver a la lista.
Geolocalización: al marcar llegada o salida en Presencia en obra, ahora se captura la ubicación GPS del celular (latitud, longitud, y precisión en metros) — sin bloquear el marcado si el técnico no dio permiso de ubicación, queda igual el registro, solo sin esa parte del dato.
Nuevo módulo — Instalación: pantalla aparte del parte técnico, para documentar en detalle una instalación de alarma/cámaras. El técnico busca el cliente (mismo buscador con autocompletado de siempre), inicia la instalación, y desde ahí puede:
Backend nuevo: tabla instalaciones (los datos generales) + instalaciones_items
(zonas y canales, mismo tipo de fila con un campo "tipo" para distinguir) + fotos en R2
bajo instalaciones/{id}/. Probado de punta a punta con Playwright: cliente con
autocompletado, iniciar instalación, agregar y guardar zonas y canales, y subir una
foto real — confirmando que todo queda guardado correctamente.
Nueva pantalla ("Presencia en obra") donde cada técnico marca su llegada y salida de cada sitio de trabajo — con buscador de cliente (mismo patrón que SIMs/Emergencias, ignora tildes y autocompleta la dirección si el cliente ya está en la base). Al marcar llegada o salida, se manda un aviso push a todo el equipo. El historial queda completo, con día y hora de ingreso y egreso, en qué cliente, y qué técnico — con los mismos filtros de período que el resto de los historiales, y una marca visual (fondo celeste) para quien todavía sigue en obra sin haber marcado salida.
No permite marcar una llegada nueva si ya hay una sin salida marcada — evita quedar con un registro "abierto" sin querer.
Backend nuevo: tabla presencias_obra, con protección contra doble llegada sin salida.
Probado de punta a punta: estado inicial, llegada con autocompletado, cambio de estado,
historial mostrando "sigue en obra", salida, y confirmación de que ambos avisos push se
disparan correctamente.
A pedido: cada movimiento ahora se ve con una etiqueta verde ("↑ Instaló xN") y/o una naranja ("↓ Retiró xN"), en vez de un texto plano — mucho más fácil de distinguir de un vistazo, sobre todo en los casos de reemplazo donde ambas aparecen juntas en la misma tarjeta.
La mejora de seguridad que habíamos evaluado y dejado pendiente: cada técnico puede activar su huella o Face ID (desde Credencial digital → "Seguridad de acceso") y de ahí en más entra tocando el sensor, sin escribir la contraseña nunca más — que sigue funcionando igual, como respaldo por si cambia de celular o quiere reactivarla en uno nuevo.
Usa el estándar WebAuthn/Passkeys (lo mismo que usan bancos y apps grandes) — la huella
en sí nunca sale del celular, el servidor solo guarda una clave pública que sirve para
confirmar la firma, nunca para "copiarla" a otro lado. Implementado con
@simplewebauthn/server, la librería más usada para esto, para no improvisar la parte
criptográfica.
Probado de punta a punta con clics reales y un autenticador virtual (simula una huella real con criptografía real, no un simulacro): activar la huella, cerrar sesión, elegir el usuario (aparece el botón de huella, se esconden el campo de contraseña y el botón de "Ingresar" de siempre), y entrar tocando huella — sin escribir nada — llegando correctamente a la pantalla principal.
api/planos.js ahora habla con el backend nuevo (R2) en vez de Google Drive — ya no
depende de un token que vencía a la hora. En admin.html, saqué el botón de subida
manual (hacía rato que ya no funcionaba, tiraba error) y lo reemplacé por un panel de
estado: "Última sincronización: hace X", con aviso en rojo si pasan más de 3 horas sin
novedades (por si la PC de la oficina se apagó o se cortó la conexión).
El sincronizador (sync-local-planos-r2) ahora se programa con el Programador de
tareas de Windows para correr solo, cada 1 hora — no hace falta acordarse de correr
nada a mano. Después de cada corrida, deja un archivo de estado en R2 que la app lee
para mostrar ese panel, y un log local (sync.log) para revisar en la PC si hiciera
falta. Instrucciones completas en INSTALAR.md dentro del proyecto del sincronizador.
A pedido tras probar en vivo: cada tarjeta ahora muestra el número de servicio y el número de cliente (buscado en la base de Clientes por el nombre, igual que ya se hace en SIMs y Emergencias) — para que quien pasa los movimientos al sistema offline de la oficina no se confunda de cliente. De paso, la base de Clientes ahora se precarga apenas el técnico entra a la app, para que esté lista en cualquier pantalla que la necesite.
El último y más delicado de toda esta migración. Encontrado en el camino: las contraseñas de los técnicos se guardaban y comparaban en texto plano en el sistema viejo (no como hash) — un problema de seguridad real, aunque separado de la migración en sí (no se mandaban al celular, pero sí quedaban legibles en el archivo de datos).
El login ahora prueba primero contra el backend nuevo (hash real, PBKDF2 con sal por usuario). Si no coincide (porque el técnico cambió su contraseña después de la migración original, o es alguien nuevo), prueba contra el sistema viejo como respaldo — y si ahí sí entra, aprovecha ese momento para guardar el hash correcto en el backend nuevo, sin que el técnico haga nada distinto ni note el cambio. La próxima vez que esa persona entre, ya pasa directo por el backend nuevo.
Si el backend nuevo no responde por algún motivo, el login sigue funcionando igual (cae al sistema viejo) — no hay forma de que esto deje a alguien afuera.
Probado de punta a punta: contraseña que coincide por el camino viejo activa el relleno correctamente; un segundo login con la misma contraseña ya entra directo por el nuevo; contraseña incorrecta se rechaza en los dos casos; y el sistema sigue andando aunque el backend nuevo esté caído.
Pendiente, como mejora aparte (no bloquea nada de lo anterior): agregar huella digital / Face ID como método de acceso más rápido y seguro, con la contraseña como respaldo — evaluado y acordado, para sumar cuando haya tiempo.
La pieza más sensible hasta ahora: es donde queda registrado cada trabajo completado (alimenta el dashboard, la "última visita" a un cliente, y el "RESUELTO" del Cronograma). El backend nuevo para esto ya estaba armado desde hace tiempo — solo faltaba que la app le hablara a él en vez de a GitHub.
Como los nombres de columna de la tabla real son distintos a los que usa la app
(id en vez de id_parte, tecnico_segundo en vez de tecnico2, descuento_tipo +
descuento_pct en vez de un solo campo descuento), armé la traducción en los dos
sentidos — la app sigue mandando y recibiendo exactamente los mismos nombres de
siempre, sin enterarse del cambio.
De yapa, esto elimina la lógica de reintentos por conflicto de escritura que tenía el sistema viejo (GitHub no permite dos escrituras al mismo archivo al mismo tiempo sin chocar) — D1 no tiene ese problema, cada parte se guarda de forma independiente.
Probado de punta a punta contra una base real: guardar y volver a leer un parte completo conserva todos los campos exactos, y si se reintenta guardar el mismo parte dos veces (por ejemplo, un doble toque sin querer), no se duplica.
El más grande y complejo de los tres recursos de recurso-uso.js — 7 acciones distintas
(usar, devolver, transferir, reemplazar, retirar de registro, migración de casos viejos,
y blanquear historial). Reconstruí todo calcado exacto de la lógica real, agregando una
tabla que nunca se había contemplado (el historial de SIMs vivía en un archivo aparte).
Probé las 7 acciones contra una base real, incluida la protección de "solo Sebastian o
Brenda pueden blanquear el historial" y el intercambio correcto de SIMs entre stock y
registro al reemplazar una línea.
A diferencia de vehículos y herramientas, esta vez corté de entrada tanto las acciones como la lectura del estado actual (stock e instaladas) — la vez pasada me había olvidado de esa segunda parte y hubo que corregirlo después.
De paso, limpié bastante código que había quedado sin usar en recurso-uso.js (las
versiones viejas de vehículo/herramienta que ya se habían reemplazado, y las funciones
de lectura/escritura de GitHub que ya no las necesitaba nadie) — el archivo bajó de 528 a
166 líneas.
Nuevo dashboard (permiso "dash_herramientas"), calcado del de vehículos: filtro por período (día/semana/mes/todo/fecha puntual) y por herramienta, con descarga a Excel. Cada tarjeta muestra, en una sola línea, quién hizo qué y con qué cliente cuando corresponde — tomó, devolvió, la transfirió a otro técnico, la dejó en un cliente, o la retiró de un cliente — dando la trazabilidad completa de una herramienta a lo largo del tiempo. Probado con los 5 tipos de movimiento y el filtro por fecha puntual.
Encontrado apenas se probó tomar una herramienta en vivo: corté las acciones de vehículos/herramientas (tomar/devolver/etc.) hacia el backend nuevo, pero me olvidé de cortar también la lectura de su "foto" actual (libre/tomada, quién la tiene) — esa seguía viniendo del archivo viejo de GitHub, que ya no se actualiza porque las acciones ahora escriben en la base nueva. Por eso una herramienta recién tomada seguía mostrándose en verde. Ya está corregido — probé el flujo completo (estado antes, tomar, estado después) y confirma que se actualiza bien.
Nueva quinta opción de período ("Fecha"), además de Día/Semana/Mes/Todo — al elegirla, aparece un selector de calendario para elegir cualquier día puntual (no solo "hoy"). Combinado con el filtro de vehículo que ya existía, permite ver de un vistazo quién usó un vehículo en concreto un día específico, con qué kilometraje, y qué eventos se cargaron ese día. Probado con un día con actividad (muestra los registros correctos) y uno sin nada (muestra la lista vacía, sin romper).
Ajuste pedido tras probar el corte de Vehículos: al tomar uno, ahora vuelve a la pantalla con el estado de todos los vehículos (como se esperaba), en vez de quedarse en el detalle de ese vehículo puntual mostrando ya las opciones de devolver/evento.
Encontrado apenas se probó el corte de Vehículos en vivo: la función que busca "quién
tiene el vehículo ahora" (encontrarRegistroAbierto) no distinguía un registro de
"evento" (que tampoco tiene hora de devolución, por diseño) de uno real de "tomar" — si
alguien reportaba un evento mientras tenía el vehículo tomado, la búsqueda podía terminar
encontrando el registro del evento en vez del de la toma real, mostrando su hora vacía
(NULL) en el mensaje de estado. Ya corregido para ignorar los registros de evento en
esta búsqueda puntual.
El más grande hasta ahora: la lógica de "tomar/devolver/evento" y "tomar/devolver/
transferir/dejar en cliente/retirar de cliente" vivía en recurso-uso.js, un archivo
aparte con su propia lectura/escritura directa a GitHub — no pasaba por donde veníamos
cortando hasta ahora.
Reconstruí esta lógica en el backend nuevo calcada exacta de la real (encontré que mi primer intento, hecho de memoria, le faltaban varias cosas: el "evento" de un vehículo como registro de historial aparte con tipo/km/monto propios, y el estado "dejado en cliente" de las herramientas). Confirmé contra la estructura real de las tablas antes de tocar código, y probé de punta a punta: un vehículo tomado por alguien no se lo puede tomar otro, reportar un evento mientras está tomado no rompe el ciclo de toma/devolución, y las herramientas dejadas en un cliente las puede retirar cualquier técnico (no solo quien las dejó) — todo igual que el sistema original.
SIMs queda sin tocar por ahora (es el más delicado, se deja para más adelante) — el
mismo archivo recurso-uso.js ahora atiende vehículo/herramienta con el backend nuevo, y
sim sigue exactamente como estaba, sin ningún cambio.
Los avisos push (evento de vehículo, herramienta dejada en cliente) los sigue mandando este lado de Vercel como siempre — el backend nuevo solo devuelve la información necesaria para decidir si corresponde avisar.
Estas dos tenían lógica propia (no un simple reemplazo de lista) que había que preservar con cuidado:
Encontrado en el camino, corregido de paso: el módulo que manda los avisos push
(lib/push-sender.js) leía las suscripciones directo de GitHub, sin pasar por nada
de esto — si no se corregía, cualquier técnico que se suscribiera de ahora en más nunca
iba a recibir avisos (quedaban guardados en un lugar que nadie leía). Ya apunta al
backend nuevo también, sin que haya hecho falta tocar nada de quien lo usa (vehículos,
cron diario, emergencias).
Probado de punta a punta: suscribirse guarda bien, el envío de avisos lee esa suscripción correctamente, cargar una emergencia nueva avisa una vez, y volver a mandar la misma lista no vuelve a avisar.
Encontrado apenas se probó el corte de Guardias en vivo: ese campo se guarda como texto (JSON) en la base nueva, y me olvidé de convertirlo de vuelta a lista antes de devolvérselo a la app — por eso no se veía ni el técnico de guardia actual ni los siguientes, y admin.html decía que no se pudo cargar la secuencia. Ya corregido y probado con el mismo formato exacto que devuelve la base real.
Segundo grupo cortado hacia el backend nuevo, mismo patrón que Clientes (la app no cambia en nada). Dos de estas necesitaron traducir la forma de los datos, porque en el backend nuevo quedaron guardadas distinto:
{categoria, modelos: [...]}),
en el backend nuevo es una fila por material — se arma/desarma automáticamente en cada
sentido, sin que admin.html note la diferencia.categoria/carpeta_drive_id
del lado viejo, nombre/manual_ref del lado nuevo) — se traducen también en los dos
sentidos.ultima_semana_notificada (para evitar mandar el aviso dos veces la misma semana) antes
de devolverlo — la app nunca lo necesitó.Importante de acá en más: como "config" ya vive en el backend nuevo, cambiar el
número de versión en app.js ya no alcanza por sí solo — ahora hay que actualizarlo
también ahí. Después de cada despliegue con una versión nueva, correr:
Invoke-RestMethod -Uri "https://sat-backend.gsigalovsky.workers.dev/api/config" -Method Post -Headers @{Authorization="Bearer <SERVICIOS_API_TOKEN>"; "Content-Type"="application/json"} -Body '{"dias_atencion":3,"dias_urgente":7,"app_version_actual":"3.38.0","felicitacion_semanal_activa":true}'
Probado con todo simulado: los seis casos (agrupar/desagrupar materiales, traducir categorías en los dos sentidos, y los tres reenvíos directos) funcionan bien.
Primer paso del corte hacia el backend nuevo (Cloudflare + D1), empezando por Clientes
(la colección más reciente y ya bien probada allá). La app en el celular no cambia en
nada — sigue pidiendo /api/datos?coleccion=clientes como siempre; del lado del
servidor, ese pedido ahora se reenvía al backend nuevo en vez de leer/escribir el
archivo en GitHub. El resto de las colecciones (técnicos, SIMs, servicios, etc.) sigue
en GitHub por ahora, sin tocar — se van a ir cortando de a una, probando cada una antes
de seguir con la próxima.
Un detalle traducido a propósito: el botón "borrar todos" de la pestaña Clientes en
admin.html manda un POST con lista vacía (así funcionaba con el sistema viejo) — el
backend nuevo tiene un DELETE aparte para eso, así que se traduce automáticamente del
lado del servidor, sin que la app tenga que cambiar nada.
Variables de entorno nuevas a cargar en Vercel: BACKEND_NUEVO_URL (la URL del
Worker) y BACKEND_NUEVO_TOKEN (el mismo token que ya usa el backend nuevo).
Probado con todo simulado: leer y guardar clientes van al lugar correcto, borrar todos se traduce bien a DELETE, y —importante— cualquier otra colección (probado con técnicos) sigue exactamente igual que antes, sin verse afectada por este cambio.
Dos pantallas ahora aprovechan la base de Clientes para no tener que escribir todo a mano:
Probado en los dos casos: aparecen los clientes correctos en las sugerencias, se autocompleta bien al elegir uno, y no se sobreescribe nada que ya estuviera cargado a mano.
Bug real encontrado apenas se activó la marca permanente: el fondo verde de esa tarjeta tenía tan poca opacidad (8%) que quedaba prácticamente transparente, dejando ver el fondo oscuro de la app detrás — con el texto oscuro de siempre, quedaba ilegible. Ahora usa un verde claro sólido, con contraste de sobra (16:1).
Dos problemas reales encontrados en el uso:
Importante: esto solo completa los partes que se carguen de ahora en más — los que ya están guardados en el historial (de antes de este cambio) van a seguir sin ese dato, porque nunca se guardó y no hay forma de recuperarlo retroactivamente.
Encontrado en el uso real: la marca "RESUELTO" en el Cronograma se guardaba solo en el celular de cada técnico (localStorage), y a propósito se borraba sola cada vez que se actualizaba el listado de servicios pendientes — un diseño pensado para durar poco. Ahora compara directo contra el historial real (¿existe un parte cargado con ese número de servicio?), así queda permanente sin importar cuándo se actualizó la lista, ni si se borraron datos del sitio en el navegador. Probados ambos casos: con historial coincidente (marca RESUELTO) y sin él (no marca nada).
Nueva "zona de riesgo" en la pestaña Clientes de admin.html, con un botón para borrar
todos los clientes cargados de una — útil, por ejemplo, si un import salió mal y se
prefiere arrancar de cero en vez de ir corrigiendo fila por fila. Pide dos
confirmaciones seguidas (no una sola), justo por ser una acción permanente y sin
vuelta atrás. No toca los planos en Drive ni las SIMs, solo esta base de referencia.
Probados los tres casos: cancelar la primera confirmación, confirmar la primera pero
cancelar la segunda, y confirmar las dos — solo en el último caso se borra de verdad.
Encontrado en el uso real: el import de la pestaña Clientes buscaba la columna "NOMBRE" con ese texto exacto (mayúsculas, sin variaciones) — a diferencia de "N° de cliente", que ya tenía varias alternativas previstas. Si el Excel real tenía el encabezado escrito apenas distinto (minúsculas, con espacios de más, u otra variante como "Nombre Cliente"), esa columna no se reconocía y quedaba vacía para todas las filas — lo que después hacía que buscar por nombre no encontrara nada (aunque buscar por número sí), y que Planos no pudiera relacionar el número del archivo con el nombre del cliente.
Ahora la lectura del Excel es mucho más permisiva en los seis campos (nombre, dirección, localidad, teléfono, número de abonado, además del número de cliente): no importa mayúsculas/minúsculas, tildes, ni espacios de más, y acepta varias formas conocidas de cada encabezado. Probado con tres variantes reales de planilla distintas (con espacios, con otra redacción, todo en minúscula) — las tres se leyeron bien.
Para vos: volvé a subir tu Excel de Clientes desde admin.html — como el import
actualiza en vez de duplicar (por número de cliente), esto va a completar los nombres que
quedaron vacíos, sin tocar nada de lo que ya estaba bien.
Encontrado en el uso real: si al cargar la base de Clientes la columna de número no
queda formateada como texto en el Excel, Excel le saca los ceros a la izquierda (guarda
"98" en vez de "00000098") — pero el archivo del plano en Drive se llama con ceros
(CLI_00000098.pdf). Antes de esto, esos dos no cruzaban (buscar por nombre no
encontraba el plano). Ahora se normaliza sacando ceros de los dos lados antes de
comparar, así "98", "098" y "00000098" se tratan como el mismo número, sin depender de
cómo haya quedado formateada la planilla.
Ajuste para el caso real: un cliente puede tener más de un plano (CLI_XXXXXX.pdf,
CLI_XXXXXX_2.pdf, CLI_XXXXXX_3.pdf...). Antes cada uno aparecía como un resultado de
búsqueda separado — ahora se agrupan en una sola tarjeta por cliente, con un botón "Plano
1", "Plano 2", etc. cuando hay más de uno (o simplemente "Ver" cuando hay uno solo, sin
cambiar la experiencia para el caso más común).
Este cambio acompaña al programa nuevo sync-local-planos-drive/ (aparte del proyecto,
para correr en la PC) — convierte los planos en Visio, organizados en subcarpetas de
Drive CLI_XXXXXX, a PDF con LibreOffice, y los sube a una carpeta plana lista para que
la app los lea. Instrucciones completas en su propio README.
Mientras se termina la migración a Cloudflare, los planos ahora se leen en vivo desde una
carpeta de Google Drive — mismo mecanismo que ya usa el Cronograma, reutilizando la misma
cuenta de servicio. Cada archivo tiene que llamarse CLI_XXXXXX.pdf (mismo número de
cliente que la pestaña Clientes de admin.html).
Variable de entorno nueva a cargar en Vercel: PLANOS_DRIVE_FOLDER_ID (el ID de la
carpeta de Drive — se ve en la URL al abrirla). Esa carpeta tiene que estar compartida con
el mismo mail de la cuenta de servicio que ya usa el Cronograma
(GOOGLE_SERVICE_ACCOUNT_EMAIL).
Búsqueda mejorada: como ahora existe la base de Clientes (número de cliente ↔ nombre),
el buscador de Planos cruza automáticamente el nombre del archivo (CLI_XXXXXX) con esa
base — se puede buscar por nombre del cliente O por número, y en los resultados se
muestra el nombre real en vez del código. Si un plano no tiene un cliente correspondiente
cargado en la base, se sigue mostrando con su nombre de archivo tal cual (no se rompe
nada).
La carga manual de un plano puntual desde admin.html queda discontinuada mientras la
fuente sea Drive — ahora se sube el PDF directo a la carpeta compartida.
Probado con datos simulados: búsqueda por nombre, por número, y el caso de un plano sin cliente correspondiente en la base (cae bien al nombre de archivo).
Las 12 pestañas de admin.html tenían todos los íconos del mismo gris uniforme —
funcional, pero hacía que el panel se sintiera un producto aparte de la app del técnico
(que sí usa un color distinto por sección desde el panel principal). Ahora cada pestaña
usa el mismo color que su tile equivalente allá (Guardias=el mismo azul acero nuevo,
SIMs=azul, Vehículos=verde, Planos=navy, Emergencias=rojo, etc.), más dos colores nuevos
para las dos secciones sin equivalente directo (Técnicos, Clientes). Ayuda a ubicarse más
rápido por color, y las dos pantallas se sienten parte del mismo producto.
De paso, mientras revisaba el dashboard general, encontré un detalle de robustez (no visual): si el gráfico de la parte de abajo falla por cualquier motivo, el mensaje de error que se muestra dice "no se pudo cargar el historial" — aunque el historial sí se haya cargado bien y el problema sea solo del gráfico. Queda anotado para una próxima pasada, no es algo urgente.
Pedido de revisar el frontend con criterio de diseño y mejorar lo que valiera la pena, sin rehacer nada de raíz (la identidad de la app — navy + ámbar, etiquetas tipo "eyebrow" — ya es distintiva, no había que cambiarla). Encontré y corregí 4 cosas puntuales, mostrando capturas antes de tocar nada:
Nuevo permiso configurable por técnico, independiente de ser administrador: quien lo
tenga ve, dentro de la app (no solo desde admin.html), el listado completo de SIMs de
todo el equipo — agrupado por técnico dueño — y puede operar cualquiera (transferir,
instalar, retirar) igual que si fuera propia. Sin este permiso, todo sigue exactamente
igual que antes (cada uno ve solo lo suyo).
La verificación real vive del lado del servidor (consultando la base de técnicos, nunca confiando en lo que mande el celular). De paso, encontré y corregí un bug real: al transferir una SIM ajena, el dato de "de quién era antes" (que usa el botón de revertir por error) se guardaba mal — quedaba el nombre de quien hacía la transferencia en vez del dueño real anterior.
Probado a fondo en los tres niveles: listado (con y sin permiso), detalle de una SIM ajena (queda operable, no de solo lectura), y el servidor (rechaza sin permiso, acepta con permiso, y guarda bien el dueño anterior).
Las dos pantallas (la app del técnico y admin.html) estaban fijadas a un ancho angosto
pensado para simular un celular incluso en una computadora — por eso se veían con mucho
espacio vacío a los costados en un monitor grande. Se agregaron reglas de CSS que solo se
activan a partir de cierto ancho de pantalla (celular no se ve afectado en absoluto):
admin.html: en compu, se ensancha bastante más (hasta 1100px) — tiene sentido
dado que hay tablas con varias columnas que se benefician del espacio extra.De paso, se corrigió un detalle cosmético menor que se notó al agregar el marco: el aviso tipo "toast" (mensajes flotantes) se ocultaba solo moviéndolo fuera de la pantalla, y en ciertos casos dejaba asomar un borde — ahora también se oculta por opacidad, así queda invisible de verdad sin importar su tamaño exacto.
Confirmado con capturas en celular (412px) y compu (1440px): el celular quedó pixel-por-pixel igual que antes, y la compu ahora aprovecha el espacio sin verse roto ni estirado de forma rara.
Encontré que la lógica y el dato ya existían (mostrarVisitaAnterior()), solo se
mostraban como un texto fijo, siempre visible en la pantalla principal. Se cambió la
presentación nada más: ahora es un ícono ⓘ chico junto al campo "Cliente" — gris y
deshabilitado si el cliente no tiene servicios anteriores, o habilitado si sí — que al
tocarlo abre una ventana chica con los 3 datos pedidos: qué se hizo, fecha, y qué técnico
lo hizo (siempre el más reciente, si hay varios). Ya no ocupa espacio permanente en la
pantalla.
El nombre del cobrador ahora viaja como una columna más (opcional) del Excel de servicios pendientes — no hace falta ninguna tabla nueva, se carga junto con el resto igual que número de cliente o de abonado. Se ve en dos lugares, como se pidió:
Si no hay cobrador cargado para ese servicio, se muestra "Sin cobrador asignado" en vez de dejarlo vacío. Probado en los cuatro casos: con cobrador, sin cobrador, servicio cargado desde la lista, y reporte manual (sin servicio de por medio).
Dos problemas reales encontrados después de un aviso tuyo de que no veías cambios recientes:
Sobre los colores de "servicio estancado": si después de una recarga a fondo seguís sin verlos, revisá que al importar el Excel de servicios pendientes se haya mapeado la columna de fecha de ingreso — hoy ese campo es opcional en el mapeo, así que si no se elige ninguna columna para él, ningún servicio va a tener colores (van a quedar todos iguales, que es justo lo que describiste).
Cuando corresponde un cambio de guardia semanal (los lunes, calculado automáticamente
desde la secuencia configurada en admin.html), además del aviso push que ya existía,
ahora también se manda un mail a [email protected] con el nombre
del técnico de guardia y el rango exacto (desde/hasta, siempre lunes 9:00 a lunes 9:00).
Un bug real que encontré y corregí en el camino: el cálculo de "9:00" mostraba "6:00" al principio — la función que calcula la hora argentina ya hace un ajuste interno de -3 horas, y mi código lo volvía a aplicar una segunda vez sin darme cuenta. Quedó probado de punta a punta (forzando que el sistema piense que es lunes) que ahora muestra la hora correcta tanto en el "desde" como en el "hasta".
Si el mail falla por algún motivo, no frena el aviso push — son dos avisos independientes.
En el listado de "SIMs instaladas" de admin.html, el estado (que ya existía como dato,
cargado desde el Excel) pasó de ser texto fijo a un botón — verde "Activa" / rojo
"Inactiva" — que al tocarlo cambia el estado, lo guarda, y actualiza la lista al instante,
sin salir de la pantalla ni recargar nada. No hizo falta agregar ningún campo nuevo a la
base de datos, el dato ya estaba, solo no se podía editar.
Nueva forma de identificar cada dirección de servicio de manera confiable — un número de cliente identifica un sitio puntual (una empresa con varias sucursales tiene un número distinto por cada una), en vez de depender de buscar por nombre (que puede tener errores de tipeo, mayúsculas distintas, etc.).
admin.html: importar/actualizar por Excel (número de
cliente, nombre, dirección, localidad, teléfono, número de abonado), y buscador.CLI_XXXXXX.pdf).Probado de punta a punta: la importación de clientes arma bien cada fila, la búsqueda filtra correcto, el botón de plano queda oculto cuando el servicio no trae número de cliente y aparece cuando sí, y al tocarlo pide exactamente el archivo correcto.
Revisión completa de los tres archivos principales (index.html, app.js, admin.html)
para reemplazar los emojis usados como íconos por SVG propios — más consistentes entre
celulares (los emojis se ven distinto en cada marca/sistema operativo).
admin.html no había recibido esta pasada todavía — los 12 íconos de pestañas ahora
son SVG (reusando, donde coincidían, los mismos íconos que ya tenía la app del técnico,
para que ambas pantallas se vean coherentes entre sí), más el botón de mostrar/ocultar
contraseña.index.html: búsqueda, notificaciones, llamar/WhatsApp, ver en mapa, foto,
galería, y varios botones más.textContent a innerHTML) para poder
mostrar un ícono real en vez de un emoji.Nuevo permiso configurable por técnico: "Agendar servicios de emergencia" (arranca apagado
para todos — hay que activarlo puntualmente desde admin.html → Técnicos para quien
corresponda). Con ese permiso activo, aparece un nuevo tile "🚨 Emergencia" en el panel
principal, pensado para cargar un servicio cuando no se puede acceder al sistema real de
oficina (fin de semana, fuera de horario): cliente, dirección, teléfono, motivo, fecha y hora
deseada.
Al cargar uno, se le avisa por notificación push a todo el equipo — se probó que esto pase solo cuando es una entrada genuinamente nueva (no se reenvía el aviso al simplemente marcar una como resuelta).
Dónde se revisan después (para pasarlos al sistema real):
admin.html, nueva pestaña "🚨 Emergencias" — lista completa, con botón para marcar cada
uno como "ya pasado" (o volver a marcarlo como pendiente si hizo falta), y para borrarlo si
ya no corresponde.Se probó a fondo: el permiso oculta/muestra el tile correctamente, el formulario rechaza
guardar si falta algún dato clave, marcar como revisado no vuelve a disparar el aviso push, y
las acciones de marcar/reabrir/borrar desde admin.html actualizan bien la lista.
Nueva sección "📐 Planos" en la app del técnico: buscá por nombre de cliente y abrí el PDF del plano/croquis de cableado directo desde el celular (se abre en una pestaña nueva, para poder hacer zoom con los gestos normales).
Con ~1000 archivos livianos (~450 MB en total) y creciendo, un repositorio de Git no es un buen lugar para guardarlos — cada vez que se reemplaza un plano, Git guarda la versión vieja Y la nueva para siempre, así que el repositorio solo crecería sin parar. En cambio, se guardan en Cloudflare R2 (almacenamiento de objetos, gratis hasta 10 GB, sin cobrar nunca por las descargas — de sobra para esto y para crecer bastante más).
A diferencia de las fotos de servicios (que no piden contraseña porque van embebidas en mails), los planos siempre exigen el token de acceso de la app — son datos más sensibles (muestran cómo está armada la seguridad de un cliente) y nunca se necesitan fuera de la app.
Variables de entorno nuevas a cargar en Vercel: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME.
admin.htmlNueva pestaña "📐 Planos" — se pueden subir varios PDF a la vez (se suben de a uno por detrás de escena, con una barra de progreso, para no toparse con los límites de tamaño de Vercel al mandar cientos de archivos juntos). Cada archivo se nombra con el nombre del cliente antes de subirlo — ese nombre es lo que después se busca desde la app. Si ya existe un plano con el mismo nombre, se reemplaza.
sync-local-planos/)Como carga inicial de ~1000 archivos sería muy lento uno por uno, se armó un programa aparte
que corre en la PC de oficina: vigila una carpeta y sube solo lo nuevo o modificado, sin
intervención manual. Como los archivos originales son una mezcla de PDF y Visio (.vsd/
.vsdx), el programa convierte los Visio a PDF automáticamente antes de subirlos (con
LibreOffice, gratis) — un celular no puede abrir un Visio directo. Instrucciones completas de
instalación en sync-local-planos/README.md.
Se probó a fondo: sube PDF y Visio correctamente, no vuelve a subir un archivo si no cambió de verdad, sube de nuevo solo lo que se modificó, y si falla la conversión de un archivo puntual, el resto sigue procesándose sin problema.
Revisión de seguridad de toda la app: manejo de datos, accesos y permisos, comunicación con servicios externos, y almacenamiento de información sensible.
Las contraseñas de todos los técnicos, y la contraseña de oficina/administración, estaban escritas en texto plano directo en el código — visibles para cualquiera que abriera el repositorio (que es público), sin necesidad de hackear nada. Esto venía desde hace mucho: toda esa historia queda en el registro de cambios de Git para siempre, así que aunque ya se sacaron del código actual, las contraseñas viejas deben tratarse como conocidas por cualquiera y cambiarse todas.
Se corrigió de raíz: ahora ninguna contraseña vive en el código ni se le entrega nunca a quien pregunta — se verifican del lado del servidor (usuario/clave del técnico, y la del panel), comparando contra lo guardado en el repositorio privado de datos o una variable de entorno en Vercel. El técnico y quien entra al panel siguen usando la app exactamente igual que antes (mismo formulario, mismos pasos) — el cambio es invisible para ellos, pero cierra el agujero. Se probó a fondo: login correcto/incorrecto de técnico, login correcto/incorrecto de administración, que la lista de técnicos ya no incluya contraseñas al consultarla, que dejar la contraseña en blanco al editar un técnico existente no se la borre, y que no se pueda crear un técnico nuevo sin ponerle contraseña.
SERVICIOS_API_TOKEN)
sigue siendo un único valor fijo, visible en el código — cualquiera que lo tenga puede usar la
API directamente. Esto es aceptable para un equipo chico e interno, pero si la empresa crece
o el riesgo percibido aumenta, lo correcto a futuro es una sesión por persona (en vez de una
clave compartida por todos), algo que requiere una revisión más de fondo del inicio de sesión.Claves y códigos ahora admite cargar varias, cada una con su propio título (para saber de qué es — ej: "WiFi del cliente", "Panel de alarma") — ya no son 3 campos fijos, sino una lista: se completa título (obligatorio) + usuario/clave/código (todos opcionales) y se agrega; el botón muestra cuántas hay cargadas. Sigue yendo solo al mail de oficina — se probó de nuevo con varias claves juntas y el cliente sigue sin recibir nada de esto.
Descuento: nueva opción "Por presupuesto" junto a Sin descuento/50%/100%/Otro. Al elegirla, aparece un campo opcional para el número de presupuesto — si se completa, queda como "Por presupuesto N° [número]"; si no, simplemente "Por presupuesto". No aplica ningún descuento automático al costo final (el precio ya viene definido por el presupuesto en sí).
Era un <span> sin ningún manejador de clic — de ahí que "no hacía nada". Ahora es un botón
real: al tocarlo, muestra el estado de sincronización (conectado o no, y si hay envíos
pendientes por una falta de conexión anterior) — reutilizando la información que la app ya
llevaba internamente, sin agregar nada nuevo que rastrear.
Al final del formulario (antes de pasar a la firma), nuevo botón "🔑 Claves" que abre una mini-pantalla para cargar usuario, clave, y código de verificación de producto (por ejemplo, de la central de alarma o el router instalado). El botón muestra un ✓ una vez que se cargó algo, para saber de un vistazo si ya se completó.
Esta información solo le llega a la oficina por mail — nunca al cliente. Se probó explícitamente: con los mismos datos, la plantilla de oficina muestra la sección completa (usuario, clave, código), y la plantilla de cliente no la incluye en absoluto — ni siquiera como sección vacía. Si no se carga nada en "Claves", la sección ni aparece en el mail de oficina.
Siguiendo pidiendo específicamente por los dashboards: se encontró que ni los botones ni los
selectores de período (Día/Semana/Mes) tenían ningún estilo de foco propio — dependían del
outline por defecto del navegador, inconsistente con la marca. Se agregó un anillo ámbar de
foco (visible solo al navegar con teclado, no con mouse/touch, usando :focus-visible) a
botones, tiles, y selectores de período en todos los dashboards — verificado que realmente se
renderiza tabulando con el teclado de verdad (no solo enfocando por código), esperando a que
termine la transición antes de medir.
También se agregó estado :hover a los selectores de período (no lo tenían), y se verificó que
los dashboards no generen scroll horizontal en ningún ancho de pantalla probado, desde 320px
(el más angosto habitual) hasta 768px.
Se hizo una revisión rigurosa del diseño, basada en medición objetiva (no en apreciación visual, ya que este entorno no permite juzgar estética a simple vista) — contraste real calculado según la fórmula de WCAG, tamaños de elementos, y distancia perceptual entre colores. Se investigaron también tendencias actuales de diseño de dashboards y apps de campo para técnicos antes de decidir qué tocar.
Encontrado y corregido:
admin.html tenían 4.23:1 — corregido a 5.33:1.Se revisó pero NO se tocó (para no arriesgar romper algo sin poder verlo): un caso que parecía tener mal contraste (el estado "En stock" de las SIMs) resultó ser un falso positivo al medir con una muestra de píxel real en vez de la fórmula ingenua — el contraste real es 5.13:1, está bien. Tampoco se hizo un reemplazo masivo de los ~140 valores de espaciado que no seguían la escala original, porque sin poder verificar visualmente el resultado, el riesgo de romper alineaciones sutiles no compensaba el beneficio.
El botón "Transferir" de cada fila apuntaba a /api/sim-uso — un
endpoint que ya no existe desde que se fusionó en /api/recurso-uso
(hace varias versiones). Es decir, transferir una SIM a otro
técnico u Oficina desde admin.html venía fallando en silencio
desde esa fusión. Ya está corregido.
En el selector "Transferir a" de cada fila de SIM, ahora hay una opción extra además de los técnicos y "Oficina": "📋 Instalar en cliente". Al elegirla, aparece:
Al confirmar, pasa por la misma acción real que usa la app del técnico ("usar") — así la SIM se migra correctamente al registro de instaladas y desaparece del stock al toque, en vez de quedar a mitad de camino como pasaba editando los campos "En uso" + "Cliente" a mano.
Si en admin.html se editaba una SIM directo en la tabla de stock
(poniéndola "en uso" + un cliente a mano, en vez de hacer el traspaso
desde la app) y se guardaba, quedaba "a mitad de camino": guardada,
pero sin pasar todavía al registro de instaladas — recién se migraba
la próxima vez que se abría esa pestaña. Ahora "Guardar cambios"
dispara la migración automática al toque, así el resultado se ve
correcto enseguida, sin tener que recargar nada.
En la pestaña "SIMs" de admin.html, "Guardar cambios" leía
literalmente las filas que estaban dibujadas en pantalla — si
había un filtro/búsqueda activo mostrando solo una parte de la
tabla, guardar en ese estado mandaba al servidor solo esas filas
visibles, reemplazando toda la lista y borrando todo lo que no
coincidía con el filtro. Ahora, si el buscador tiene algo escrito,
"Guardar cambios" se bloquea con un aviso explícito pidiendo borrar
la búsqueda antes de guardar — así nunca se puede guardar una versión
parcial sin querer.
Cualquier SIM que haya quedado marcada "en uso" con un cliente
dentro del stock (de antes de que existiera el registro de instaladas
separado) ahora se migra sola al registro apenas se abre la pestaña
"SIMs" en admin.html — no hace falta tocar nada a mano. Esas
líneas desaparecen del stock y pasan a figurar en "SIMs instaladas",
con el cliente y el técnico que las tenía asignadas (la fecha de
instalación real no se conserva para estos casos viejos, porque no
estaba guardada en ningún lado — queda con la fecha del día en que se
migran). Es seguro que corra cada vez que se entra a esa pestaña: si
no encuentra nada para migrar, no hace nada.
En "📇 Buscar SIM instalada / Retirar" (app del técnico), cada resultado de búsqueda ya mostraba compañía, número y cliente, pero no mostraba el Estado de línea (Activo/Inactiva) aunque el dato ya estaba guardado desde la importación. Ahora se muestra también — la búsqueda deja ver los cuatro datos: compañía, número de línea, cliente y estado de línea, tal como estaba en la planilla original.
Un técnico ahora solo ve sus propias SIMs en la app — nunca las de otro técnico, ni las que están sin asignar en "Oficina". Antes había dos formas en las que esto se filtraba:
admin.html) de paso también mostraba todas las SIMs de todos
dentro de la app — se separó: ese permiso ahora solo controla el
acceso al panel, la visibilidad de SIMs adentro de la app queda
siempre acotada a lo propio, para cualquier técnico.Tomar una SIM de "Oficina" (sin asignar) sigue siendo una acción
exclusiva de quien tenga el panel de administración — un técnico no
la puede ver ni tomar por sí mismo desde la app; se la asigna un
admin desde admin.html (columna "Transferir a" en la tabla de
stock). Desde su propio menú, un técnico puede: transferir una SIM
suya a otro técnico, instalarla en un cliente (lo que la mueve al
registro definitivo), y retirar una ya instalada si el cliente se dio
de baja.
Con ~900 SIMs ya instaladas en clientes (y solo 30-60 en stock activo entre todos los técnicos), el sistema de SIMs de antes — pensado para manejar todo junto en un solo archivo, con una tabla editable mostrando todo de una — dejaba de ser práctico. Se separó en dos categorías con lógica distinta:
sims-config.json, sin cambios): las 3-4 SIMs por
técnico que se mueven todo el tiempo — tomar, transferir, usar.
Sigue funcionando exactamente igual que antes.sims-instaladas.json, nuevo): las
~900 líneas ya funcionando en un cliente, que casi no se mueven.
Es un registro de consulta, no una tabla editable — se busca,
no se lista todo de una.admin.html → pestaña "SIMs instaladas")Se puede cargar (o actualizar) el registro completo subiendo una planilla con las columnas N° DE ABONADO, ESTADO DE LINEA, NOMBRE ABONADO, DIRECCION, FECHA DE ACTIVACION, NUMERO DE LINEA, EMPRESA. La importación es inteligente: si un N° de abonado ya existe en el registro, se actualiza con los datos nuevos; si es nuevo, se agrega — nunca duplica ni borra lo que ya estaba cargado (ya sea a mano o por una SIM instalada desde la app). Se probó la lectura y el mapeo de columnas contra un archivo real de 908 líneas, sin errores.
Se corrió el horario del recordatorio de devolver el vehículo, de 18-19hs a 17-18hs, tal como se pidió. Como ahora coincide con la franja de la felicitación semanal de los viernes, se aprovechó para unificar en un solo horario de cron de tarde (de lunes a viernes) en vez de tener dos por separado — los viernes a esa hora pueden salir los dos avisos (recordatorio de vehículo + felicitación, si corresponde), cualquier otro día de la semana sale solo el recordatorio de vehículo si aplica.
La felicitación semanal solo chequeaba "¿hoy es viernes?" — pero el cron de la mañana (que corre TODOS los días) también es viernes los viernes, así que se disparaba ahí en vez de esperar a la tarde. Ahora también chequea la hora (además del día), para que solo se dispare con la invocación de la tarde.
De lunes a viernes (no fines de semana ni feriados), entre las 18 y las 19hs aproximadamente, a los técnicos "en la calle" que todavía tienen un vehículo tomado a esa altura del día les llega un aviso para que se acuerden de devolverlo. Se agregó un tercer horario de cron para esto (ya usábamos dos: el de la mañana y el de los viernes).
Sobre la idea de usar geolocalización en vez de un horario fijo: lo evalué, pero no es viable de forma confiable con esta app tal como está armada hoy. Una PWA (como esta) no puede rastrear la ubicación del celular en segundo plano cuando la app está cerrada — eso requiere permisos y capacidades que solo tienen las apps nativas instaladas desde una tienda de aplicaciones, especialmente en iPhone, donde Safari es todavía más restrictivo con esto. Si en algún momento se quisiera geolocalización real (detectar "llegó a la oficina"), hay que evaluar convertir la app a una app nativa — es un cambio de fondo, no un ajuste. Por ahora, el aviso por horario fijo es la opción confiable.
Si un técnico "en la calle" ya tomó el vehículo y ya tiene al menos una herramienta en uso antes de que dispare el recordatorio de la mañana, no le llega el aviso — ya no hace falta. Si le falta cualquiera de las dos cosas (por ejemplo, tomó el vehículo pero todavía no tomó ninguna herramienta), el recordatorio le sigue llegando igual.
Las notificaciones push del celular ya sonaban con el sonido del sistema por defecto (nunca se mandaban en silencio) — se dejó explícito en el código para que quede garantizado. Además, el recordatorio a técnicos "en la calle" (ver v3.9.0) ahora se manda marcado como "importante": queda fijo en la pantalla hasta que lo toquen (no desaparece solo a los pocos segundos) y vibra más fuerte — para que sea difícil pasarlo por alto. Una aclaración honesta: el sonido en sí depende del sistema operativo del celular (si el técnico tiene el teléfono en silencio o modo "No molestar", ninguna app puede saltarse eso).
Los viernes, en algún momento entre las 17:00 y las 18:00hs (la app usa el mismo cron de un solo disparo diario que ya usábamos, pero con un segundo horario agregado solo para los viernes — Vercel gratis no garantiza el minuto exacto, pero sí que caiga dentro de esa hora), se manda un aviso a todo el equipo felicitando al técnico que más servicios resolvió esa semana (lunes a viernes). Si hay empate, se felicita a todos los que empataron. Si nadie resolvió nada esa semana, no se manda nada.
Se puede desactivar desde admin.html → pestaña Servicios
pendientes → hay un check "🏆 Mandar cada viernes..." — viene
activado por defecto, tal como pediste.
En la lista de "Servicios pendientes", cuando un servicio pasa el umbral de "urgente" (por defecto, 10+ días sin resolver), la tarjeta se marcaba en rojo pero con un fondo muy tenue (6% de opacidad) — el nombre del cliente, en texto oscuro, quedaba con muy poco contraste contra ese fondo casi blanco. Ahora esa tarjeta tiene un fondo rojo sólido, con el nombre del cliente y el resto del texto en blanco — contraste verificado en 5.44:1, por encima del mínimo de accesibilidad (4.5:1). De paso, la etiqueta "🔴 Hace X días" se invirtió (fondo blanco, texto rojo) para que siga destacando como una etiqueta propia sobre el nuevo fondo rojo, en vez de mezclarse con él.
Desde admin.html → Técnicos, cada técnico tiene un check "🚐 Está
en la calle". Los que lo tengan tildado reciben una notificación
push de lunes a viernes (no fines de semana ni feriados nacionales)
recordándoles tomar el vehículo y las herramientas que necesiten.
Pediste que sea a las 9:30 en punto — en el plan gratuito de Vercel, el horario de un cron job no se garantiza al minuto exacto, solo "en algún momento dentro de la hora indicada". Como ya usábamos el cron de las 9 en punto (hora Argentina) para el aviso de cambio de guardia, el recordatorio de vehículo/herramientas se agregó a ESE MISMO disparador (en vez de un cron nuevo) — así que en la práctica va a llegar en algún momento entre las 9:00 y las 10:00 AM, no exactamente a las 9:30. Si más adelante hace falta más precisión, hay que pasar a un plan pago de Vercel (o un programador externo), pero para un recordatorio matutino esta ventana debería alcanzar.
En vez de mantener a mano una lista de feriados argentinos (que cambian todos los años, con puentes y feriados trasladables por decreto), se consulta una API pública y gratuita (Nager.Date) que ya tiene el calendario completo de Argentina. Si por algún motivo esa consulta falla, se prefiere mandar el recordatorio igual antes que arriesgarse a no avisar por semanas por un problema de red pasajero.
Hasta ahora, cuando un técnico tocaba "Activar notificaciones", se guardaba el dato de su celular pero no a qué técnico pertenecía — por eso no había forma de mandarle un aviso a "solo estos técnicos". Ahora cada suscripción guarda también el nombre del técnico, y si el mismo celular pasa a otro técnico (cambia de mano), se actualiza solo. Esto habilita, a futuro, más avisos dirigidos a técnicos puntuales (no solo a todo el equipo).
Mismo mecanismo que ya existía para los partes de servicio: si el técnico carga datos en el formulario de Comodato y toca "Cancelar" (o cierra la app) antes de firmar y enviar, lo que ya había cargado no se pierde — queda guardado en el celular y se restaura solo la próxima vez que abra Comodato, con un aviso de "Se restauró un comodato que tenías sin terminar." A diferencia de los partes (que se guardan por número de servicio), acá alcanza con una sola clave fija, porque en la práctica hay como mucho un comodato en curso a la vez.
Antes, varias secciones estaban restringidas a nombres fijos en el
código (Dashboard financiero solo para Sebastián; Administración solo
para Sebastián y Brenda; ver el historial/las SIMs de todo el equipo,
también). Ahora todo eso se administra con casillas por técnico
desde admin.html → pestaña Técnicos — cada técnico tiene, debajo
de su nombre y contraseña, una grilla de 10 casillas:
Si un técnico no tiene ninguna tildada, no ve ese botón en el panel principal — un técnico nuevo arranca con todo apagado, y vos tildás lo que le corresponda antes de guardar.
Importante para la primera vez que uses esto: los técnicos que ya existían antes de esta actualización (Sebastián, Brenda, y el resto) todavía no tienen casillas guardadas explícitamente — mientras eso sea así, la app les mantiene el acceso que ya tenían (por nombre, como antes) para que nadie se quede afuera de golpe. Pero en cuanto entres a la pestaña Técnicos y guardes los cambios (aunque sea para agregar o editar a un solo técnico), se guardan casillas explícitas para todos los que aparezcan en esa lista — así que la primera vez, antes de guardar, repasá que Sebastián tenga todo tildado y que Brenda tenga al menos "Panel de Administración", para no sacarles acceso sin querer.
Nueva sección "📄 Comodato" en el panel principal, para cuando se le deja a un cliente equipos en préstamo (centrales, GPS, sensores, etc.). El técnico completa un formulario (cliente, dirección, representante, artículos — de un catálogo + opción "otro" —, abono mensual), firma con el mismo sistema de firma digital que ya usa la app (más aclaración, cargo y DNI, obligatorios para este contrato), y al confirmar se genera el PDF del contrato ya completo y firmado, que se manda por mail a la oficina y al cliente.
Vercel no puede convertir un Word a PDF (no tiene LibreOffice
disponible), así que en vez de rellenar el .docx original y
convertirlo, el PDF se genera directamente en código con la
librería pdf-lib (liviana, JS puro, sin dependencias externas) — el
texto legal es el mismo exacto que el contrato original en Word
(se migró cláusula por cláusula), con salto de línea y paginado
automáticos según el largo de cada campo. Esto es más confiable que
depender de un servicio externo de conversión.
lib/pdf-comodato.js: arma el PDF completo, con el encabezado real
de SAT (logo extraído del Word original, convertido de CMYK a RGB
porque si no se ve mal en el PDF) y la firma real de Alfredo
Thiesing (comodante) ya incrustada — la del cliente (comodatario) se
agrega dinámicamente en cada comodato.assets/comodato-encabezado.png y
assets/comodato-firma-thiesing.png) están sacados directamente del
Word original — si en algún momento cambia el logo o la firma de
Alfredo, hay que reemplazar esos dos archivos./api/comodato.jsNueva función (van 11 de 12 — queda 1 libre). Recibe los datos del formulario + la firma, arma el PDF, y manda dos mails (oficina y cliente, si se cargó su mail). El envío a oficina es el que importa: si por algún motivo no se puede confirmar que llegó, el comodato no se da por enviado — queda guardado en el celular y se reintenta solo (ver más abajo), igual que ya pasa con los partes de servicio.
La cola de reintento sin conexión que ya existía para los partes de
servicio (por si el técnico se queda sin señal) ahora también guarda
comodatos — cada elemento de la cola tiene un campo tipo
("parte" o "comodato") para saber cómo reintentarlo. Si el mail a
oficina falla o no se puede confirmar, el comodato queda guardado en
el celular del técnico y se reintenta solo apenas vuelva la conexión
— nunca se descarta sin que oficina lo haya recibido.
Al cargar una herramienta en admin.html, ahora se elige también un
tipo (Escalera, Computadora/Notebook, Herramienta eléctrica, u
Otro) — solo define qué ícono se le muestra en la app, para
reconocerla más rápido de un vistazo en la lista. El color de la
insignia sigue reflejando el estado (libre/en uso/en un cliente) como
antes; lo único que cambia con el tipo es el dibujo del ícono.
Nueva sección "🧰 Herramientas" en el panel principal, para llevar registro de elementos puntuales que se llevan un técnico a la vez — escaleras especiales, la computadora de diagnóstico, un taladro en particular, etc. (a diferencia de los vehículos, no se usan todos los días). Funciona igual que vehículos y SIMs:
admin.html → pestaña "Herramientas" — se
carga el nombre de cada una (el estado y quién la tiene lo va
actualizando la app sola).De regalo, se liberó otra función: en vez de crear un tercer
endpoint para herramientas (que nos hubiera dejado otra vez al
límite), se aprovechó para fusionar vehiculo-uso.js + sim-uso.js +
la lógica nueva de herramientas en un solo archivo
(recurso-uso.js) — las tres comparten el mismo patrón de "tomar /
devolver / transferir + historial", así que tenía sentido unificarlas.
Resultado: 10 funciones en total, con 2 de margen (antes
había 1). Se probó a fondo con pruebas automatizadas que vehículos y
SIMs siguen funcionando exactamente igual que antes a través del
endpoint nuevo, sin ningún cambio de comportamiento para el usuario.
Si un técnico abre un servicio, carga algunos datos, y toca "Volver"
(o cierra la app / cambia de pantalla) antes de firmar y enviar, lo que
ya había cargado no se pierde — queda guardado en el celular
(localStorage) y se restaura solo la próxima vez que abra ese
mismo servicio, con un aviso de "Se restauró lo que ya tenías
cargado en este servicio."
Al firmar, además del dibujo de la firma, el cliente (o quien firme) completa aclaración (nombre y apellido, tipeado — obligatorio, no deja avanzar sin cargarlo) y cargo (opcional, ej. "Encargado", "Propietario"). Los dos se agregan justo debajo de la firma en los dos mails, con una línea separadora, como una firma en papel de toda la vida. Si no se carga cargo, esa línea directamente no aparece.
www.sat365.com.ar) — si querés que también aparezca un
teléfono o WhatsApp, decime el número y lo agrego.nodemailer (gratis, sin límites propios — el único
límite real es el que tenga tu plan de hosting de correo en DonWeb,
normalmente mucho más generoso que un plan gratuito de EmailJS)./api/enviar-mail.js (no se leen de archivo aparte), con el
mismo formato {{variable}} que ya usaban en EmailJS — incluye los
bloques opcionales ({{#tecnico2}}...{{/tecnico2}}, etc.) para
"2° técnico", "imprevisto" y "foto", que solo se muestran si esos
datos vinieron cargados.email-templates/template_oficina.html y
template_cliente.html quedan como referencia de diseño, ya no se
usan para nada — si en algún momento hay que cambiarles el diseño,
hay que editar las plantillas incrustadas en enviar-mail.js
directamente (y opcionalmente, actualizar esos dos archivos también
para que seas conserva la referencia al día).El técnico puede sacar o elegir una foto al completar el parte. Es
opcional — si no carga nada, no pasa nada. La foto no se manda como
adjunto de mail (los adjuntos pesados complican el envío y no todos
los clientes de correo los muestran bien), sino que se sube al mismo
repo privado de GitHub que ya se usa para el listado de servicios
(carpeta fotos/), y el mail de la oficina recibe un link que la
muestra. El mail al cliente nunca incluye la foto ni el link.
(Antes probamos con Google Drive usando una cuenta de servicio, pero Google no permite que las cuentas de servicio suban archivos a carpetas de Drive personales — piden Google Workspace con Shared Drives. Por eso se pasó a guardar las fotos en GitHub, reutilizando la infraestructura que ya estaba armada para los servicios pendientes.)
El link no es un link directo al repo (que es privado) — pasa por un
endpoint propio (/api/foto.js). No lleva ningún token ni clave
visible: cada foto recibe un identificador aleatorio de 16 caracteres al
subirse, y ese identificador funciona como la clave — sin conocerlo
exacto no hay forma de ver ni listar las fotos. El link queda corto y
simple, por ejemplo tu-app.vercel.app/api/foto?id=9f3ac21b7e4d5210.
No hace falta nada nuevo — reutiliza las variables GITHUB_DATA_TOKEN
y GITHUB_DATA_REPO que ya configuraste para el listado de servicios.
Los técnicos pueden ver, desde el listado de servicios, un botón "Ver cronograma semanal" con el cronograma completo del equipo — todos ven las tareas de todos, con un filtro opcional por técnico y pestañas por día.
Al tocar una tarea del cronograma, la app intenta encontrar el servicio pendiente correspondiente (comparando el nombre de cliente mencionado en la tarea contra el listado de servicios) y abre el formulario ya precargado con esos datos, igual que si se hubiera elegido directamente del listado. Si no encuentra una coincidencia, igual abre el formulario extrayendo cliente/dirección/localidad del texto de la tarea como mejor esfuerzo (formato esperado: "Servicio: Nombre" en la primera línea, "Dirección - Localidad" en la segunda).
Cuando un parte vinculado a un servicio pendiente se completa y el mail
a la oficina se envía con éxito, ese servicio queda marcado en verde en
el listado (sin desaparecer) hasta que se cargue un listado nuevo desde
admin.html o se sincronice de nuevo — así se puede verificar de un
vistazo cuáles ya se resolvieron en el día.
Se sincroniza solo desde un archivo de Google Drive cada vez que un técnico abre esa pantalla — no hace falta subir nada a mano. El formato esperado es el de siempre: una hoja por día, técnicos en columnas (fila 3), franjas horarias de 30 minutos en filas, y celdas combinadas cuando una tarea dura más de una franja (se respetan para calcular la duración real). Funciona tanto si el archivo en Drive es un Excel subido tal cual como si es una Hoja de cálculo de Google nativa.
Como respaldo por si falla la conexión con Drive en el momento, cada
sincronización exitosa guarda una copia en el repo privado de datos
(cronograma.json). También queda disponible la carga manual desde
admin.html (pestaña "Cronograma semanal") como alternativa si hiciera
falta.
Reutiliza las que ya existen de la función de fotos
(GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY) y
de servicios pendientes (SERVICIOS_API_TOKEN, GITHUB_DATA_TOKEN,
GITHUB_DATA_REPO). Solo hace falta agregar una nueva:
CRONOGRAMA_DRIVE_FOLDER_ID = el ID de la carpeta de Drive donde
se sube el archivo (la parte de la URL después de /folders/, por
ejemplo en https://drive.google.com/drive/folders/XYZ456... el ID
es XYZ456...).Se usa la carpeta y no un archivo puntual porque el archivo cambia de ID cada vez que se actualiza (se sube uno nuevo en vez de editarse el mismo) — la función siempre busca el archivo modificado más reciente dentro de esa carpeta, así no importa que el ID cambie.
La carpeta (no un archivo individual) tiene que estar compartida con el
email de la cuenta de servicio
([email protected]) con permiso
de Lector — no hace falta Editor, solo lectura. Compartiendo la
carpeta entera, cualquier archivo nuevo que se suba ahí queda accesible
automáticamente, sin tener que compartir cada uno.
(Como alternativa, si en algún momento se necesitara apuntar a un
archivo fijo puntual en vez de una carpeta, sigue existiendo la
variable CRONOGRAMA_DRIVE_FILE_ID como respaldo — pero no es lo
recomendado dado que el ID cambia con cada actualización.)
Cuando un técnico envía un parte con éxito, la app revisa si ya completó todas las tareas que tenía agendadas para hoy en el cronograma (comparando cada tarea contra el listado de servicios pendientes, igual que al tocar una tarea del cronograma). Si no le queda nada agendado para hoy, en la pantalla de "enviado" aparece una lista con los servicios pendientes más cercanos al que acaba de terminar — tocando uno, abre el formulario directo con esos datos.
Límites a tener en cuenta:
/api/geocode.js),
así que la primera vez que se prueba con direcciones nuevas puede
tardar unos segundos.Desde el formulario hay un botón "📍 Ver en mapa" que muestra la ubicación de la dirección cargada (Leaflet + OpenStreetMap, sin costo ni API key), y marca en naranja cualquier otro servicio pendiente a menos de 500 metros — útil para agrupar visitas cercanas.
Las direcciones se convierten a coordenadas con Nominatim (el
geocodificador gratuito de OpenStreetMap) a través de /api/geocode.js,
que guarda cada dirección ya buscada en el repo privado de datos
(geocode-cache.json) para no tener que volver a buscarla — así se
respeta el límite de uso gratuito (1 pedido por segundo) y las
consultas repetidas son instantáneas. No hace falta ninguna variable de
entorno nueva: reutiliza SERVICIOS_API_TOKEN, GITHUB_DATA_TOKEN y
GITHUB_DATA_REPO que ya existen.
Si hay muchas direcciones nuevas sin geocodificar todavía (la primera vez que se cargan muchos servicios de golpe), puede tardar unos segundos o requerir tocar "Ver en mapa" más de una vez, ya que cada pedido procesa como máximo 6 direcciones nuevas por vez (para no exceder el tiempo máximo de ejecución de la función serverless).
SERVICIOS_API_TOKEN, GITHUB_DATA_TOKEN, GITHUB_DATA_REPO,
GITHUB_DATA_PATH.index.html → pantallas (formulario / firma / enviando / listo)
styles.css → diseño visual
app.js → lógica: formulario, firma en canvas, ID único, envío doble por mail
manifest.json → metadata de instalación como PWA
sw.js → cachea la app para que abra rápido / offline
icons/ → ícono de la app
Desde el listado de servicios hay un botón "📊 Ver dashboard" con:
/api/historial.js (guardado en el mismo
repo privado de datos, archivo historial.json). El historial
arranca a contar desde que se activó esta función — no hay datos de
partes anteriores a esto./api/geocode.js,
con su misma caché). No es una ruta real calculada por calles, así
que va a ser menor a la distancia que efectivamente se recorre
manejando.Hay un segundo dashboard, con acceso restringido: el botón "💰 Ver dashboard financiero" solo aparece si el técnico logueado es Sebastian Bartolozzi (según la contraseña usada para entrar). Como con el resto de la app, esto es una restricción del lado del celular — no un permiso real de servidor — pero alcanza para que no lo vea cualquiera que use la app.
Muestra, con las mismas pestañas Día / Semana / Mes que el otro dashboard:
Usa el mismo historial que el otro dashboard (historial.json), al que
ahora también se le suman el importe, descuento, costo final y forma de
pago de cada parte completado — por eso, igual que con el otro
dashboard, solo va a tener datos a partir de que se activó esta
función, no de partes anteriores.
Arriba del formulario hay un check "Es una INSTALACIÓN". Si se tilda:
Si al enviar un parte no hay conexión (o el envío falla), la app no lo pierde: lo guarda en el celular y avisa "el parte quedó guardado y se va a reintentar solo cuando vuelva la conexión". El reintento pasa solo, sin que el técnico tenga que hacer nada, en estos momentos:
Los partes pendientes se guardan en el propio celular (localStorage)
— si se desinstala la app o se borra el caché del navegador antes de
que se puedan enviar, se pierden. Por eso, si un técnico ve el aviso de
"pendiente de enviar" durante mucho tiempo, conviene asegurarse de que
tenga conexión antes de cerrar la app.
Importante: esta cola es por celular, no centralizada — el aviso solo lo ve la persona en cuyo celular quedó atascado el envío (no hay forma de verlo desde otro dispositivo). El texto del aviso muestra de qué técnico y cliente es cada parte pendiente (ej. "1 parte(s) pendiente(s) de enviar — de: Cristian Rossetti (Comercial Norte SRL)"), para identificarlo de un vistazo si hace falta.
En "Materiales utilizados" el técnico elige una categoría (ej. "Sensores infrarrojos"), después un modelo dentro de esa categoría, carga la cantidad, y toca "+ Agregar" — se puede agregar varios materiales distintos a un mismo parte. Debajo sigue habiendo un campo de texto libre para cualquier otro material que no esté en el catálogo. Al enviar, se combina todo en un solo campo de texto.
Las categorías y modelos se administran desde admin.html → pestaña
"Materiales":
Se guarda en el mismo repo privado de datos (materiales-catalogo.json),
reutilizando las variables de entorno que ya existen — no hace falta
configurar nada nuevo.
Al lado del botón de llamar (📞) hay un botón verde de WhatsApp (📲), que también aparece solo si el servicio tiene teléfono cargado. Al tocarlo, abre WhatsApp con un mensaje ya escrito ("Hola! Soy <técnico>, técnico de SAT... estoy en camino...") — el técnico solo tiene que tocar "Enviar".
No es un envío 100% automático (usamos un link wa.me, no la API de
WhatsApp Business, que tiene costo por mensaje y requiere verificación
de Meta) — pero no tiene ningún costo ni trámite, y solo requiere ese
toque extra.
Importante sobre el formato del teléfono: el link arma el número
asumiendo Argentina (agrega 549 adelante si el teléfono no viene ya
con código de país). Si los teléfonos que carga el ERP tienen otro
formato, puede que el link no abra el chat correcto la primera vez —
conviene probarlo con un número real y, si hace falta, ajustar la
función limpiarTelefonoWhatsapp en app.js.
En la pantalla de login hay un botón "↻ Actualizar app a la última versión". Borra toda la caché del navegador y el service worker de la app, y recarga la página — así siempre se puede forzar a traer la última versión publicada sin tener que desinstalar y reinstalar el acceso directo del celular. Después de usarlo, puede tardar un instante en volver a aparecer la pantalla de login (está bajando todo de nuevo).
Aviso de versión nueva disponible: la app compara su propia versión
(APP_VERSION en app.js) contra app_version_actual en
/api/config.js cada vez que se abre el login. Si el servidor tiene un
número mayor, aparece un cartel arriba recomendando actualizar. Esto
funciona incluso si el celular tiene una versión vieja cacheada,
porque la comprobación en sí es un pedido de red que no se cachea.
Para que esto funcione bien, cada vez que se sube una versión nueva
hay que actualizar los dos lugares: APP_VERSION en app.js y
app_version_actual (dentro de CONFIG_DEFAULT) en api/config.js —
tienen que quedar iguales entre sí en cada publicación.
Al loguearse con la contraseña de Sebastian Bartolozzi o de Brenda
Thiesing, en el panel principal aparece un botón "🛠 Panel de
administración" que abre admin.html directo (en una pestaña nueva) —
sin tener que escribir la URL a mano. Ese panel sigue teniendo su
propia contraseña (la de ADMIN_PASSWORD en admin.html), este botón
es solo un atajo, no la reemplaza. (El dashboard financiero sigue
siendo exclusivo de Sebastian Bartolozzi — este acceso directo al
panel de administración es lo único que también ve Brenda.)
Desde el listado hay un botón "🤖 Consultas (manuales)": el técnico elige una categoría (ej. "Alarmas"), escribe una pregunta en lenguaje natural, y la app busca los manuales en PDF de esa categoría (en Drive) y le pide a una IA (Google Gemini, plan gratuito) que responda basándose solo en el contenido de esos manuales.
Ver el manual directo: apenas se elige una categoría, aparece debajo la lista de manuales en PDF de esa categoría — tocando uno, se abre en una pestaña nueva del navegador para leerlo completo, sin pasar por la IA. Reutiliza la misma conexión a Drive (no hace falta nada nuevo), y el PDF viaja protegido por el mismo token de la app — no se comparten los archivos públicamente en Drive.
admin.html → pestaña "Consultas
(IA)", cargá cada categoría con el link (o ID) de la carpeta de
Drive donde están los manuales PDF de esa categoría. Compartí cada
carpeta con [email protected]
(permiso Lector) — la misma cuenta de servicio que ya se usa
para el cronograma.GEMINI_API_KEY.Botón "🪪 Credencial" en el panel principal — solo aparece si el técnico logueado tiene una credencial cargada (los que no tienen todavía, o el login general de oficina, no lo ven). Muestra un carnet con foto, nombre, cargo, DNI, teléfono de contacto y fecha de vigencia, para que el técnico lo presente ante el cliente como método extra de identificación al ingresar a su domicilio.
Se administra desde admin.html → pestaña "Credenciales":
Cada técnico ve solo la suya, nunca las de sus compañeros — se
filtra según con qué usuario entró a la app. No hace falta ninguna
variable de entorno nueva, se guarda en el mismo repo privado de datos
(credenciales-config.json).
Pantalla completa al tocarla: tocando la credencial, se agranda, gira 90° (queda apaisada, como un carnet real) y ocupa toda la pantalla, con los datos en letra más grande — ideal para mostrársela al cliente. Se intenta también activar el modo pantalla completa real del navegador (oculta la barra de direcciones) donde el dispositivo lo permita; en iOS puede que no oculte la barra por completo, pero el giro y el agrandado funcionan igual. Se vuelve a tocar (la credencial o afuera, en el fondo oscuro) para salir de ese modo.
Diseño con más elementos de seguridad visual (para que no sea tan fácil de imitar con una captura editada), con una paleta más sobria y elegante (negro/azul profundo, gris, y un dorado apagado en vez de naranja vivo):
SAT-XXXXXX), que se
genera solo a partir del nombre y el DNI — no hace falta cargar
ningún dato extra en admin.html, siempre da el mismo código para la
misma persona.www.sat365.com.ar, para
que el cliente pueda verificar por su cuenta que la empresa existe y
es la que dice ser. Se genera en el momento con una librería gratuita
(qrcodejs, cargada por CDN en index.html), sin ningún servicio
externo ni costo.manifest.json
(nombre que se muestra al instalarla en el celular).Toda la app (no solo el panel principal) usa ahora el mismo fondo azul oscuro que la pantalla de login, con los botones "Volver"/"Actualizar" en texto claro para verse bien sobre ese fondo. Las tarjetas que ya tenían fondo blanco propio (servicios, cronograma, dashboards, historial, credencial, guardias, etc.) se dejaron igual — se ven como tarjetas claras flotando sobre el fondo oscuro. El panel principal además tiene el logo de SAT arriba de todo.
Hay tres avisos que le llegan a todo el equipo, incluso con la app cerrada (como cualquier notificación de celular):
En el panel principal hay un botón "🔔 Activar notificaciones" — al tocarlo, el celular va a pedir permiso una sola vez. Cada uno lo activa en su propio teléfono.
Hace falta cargar tres variables de entorno nuevas en Vercel:
| Variable | Valor |
|---|---|
VAPID_PUBLIC_KEY | BHqngzDxmtV7PiUQO0zMKMaysybsccUB1ibD6UK7Kj2G0EICqt6ET-4RFV9mBU4PSxD10I6krHzrIFB2Ndxq_60 |
VAPID_PRIVATE_KEY | yflpkyfuCwhh_N9nW-GDuehhw4gTZzjX1l-L1mV6srI |
CRON_SECRET | 8051bd14886db539252c20216e0f9200f6489ce411b20c99 |
(VAPID_SUBJECT es opcional — si no se carga, usa
mailto:[email protected] por defecto.)
Después de cargarlas, hacé un redeploy. Vercel va a registrar solo
el cron diario (vercel.json) que corre una vez por día (plan
gratuito) y agrega automáticamente el header de autorización con
CRON_SECRET — no hay que hacer nada más para que ande.
web-push del lado del servidor.push-subscripciones.json (en el repo privado de datos) — se agrega
sin borrar las de los demás./api/cron-diario.js) corre una vez por día, revisa
si es lunes (avisa la guardia) y si algún vehículo cruzó un umbral
nuevo (guarda el último estado avisado en
notificaciones-estado.json, para no repetir el mismo aviso todos
los días).Instalar una SIM desde el propio formulario del parte: en la
sección de materiales del formulario, si el técnico logueado tiene
alguna SIM en stock, aparece un check "Instalé una SIM de mi stock en
este servicio" — al tildarlo, elige cuál (de las que tiene en stock)
en un desplegable. Al enviarse el parte con éxito, esa SIM sale sola
del stock del técnico y queda asignada al cliente del servicio (igual
que si la hubiera marcado como "usada" desde la pantalla de SIMs) —
queda registrado en sims-historial.json, y también se agrega un
renglón en el campo de materiales del parte ("SIM Movistar Estándar
341...").
Si el login de oficina/general carga el parte (sin stock propio), o el técnico no tiene ninguna SIM en stock, esta opción directamente no aparece.
Botón "📶 SIMs" en el panel principal. Cada técnico tiene un stock de chips (Movistar Estándar/Mini, Personal, Claro), identificados por número de teléfono.
Visibilidad en la app: cada técnico ve solo las suyas en el listado principal. Sebastian Bartolozzi, Brenda Thiesing, y el login general de oficina siguen viendo las de todo el equipo, agrupadas por técnico dueño (las propias primero). El buscador de arriba de la lista ("qué línea tiene un cliente") es la única excepción — busca siempre entre las de todo el equipo, sin importar quién esté logueado, para poder coordinar aunque el listado principal esté restringido.
Tocando una SIM que es tuya:
admin.html.Las SIMs de otros técnicos (cuando se ven, según lo de arriba) no se pueden tocar — solo actúa quien la tiene en ese momento.
Administración — tabla completa: en admin.html → pestaña "SIMs"
hay una sola tabla con todas las líneas (de todos los técnicos y
las que están en "Oficina"), con un buscador arriba (número, cliente,
empresa o técnico). Cada fila tiene:
Cada movimiento hecho desde la app o desde esta tabla (usar, devolver,
transferir) queda en un historial aparte (sims-historial.json),
igual que con los vehículos. No hace falta ninguna variable de entorno
nueva.
Excel de SIMs: mismos botones de siempre, con las columnas que pediste — "NUMERO DE LINEA", "EMPRESA PROVEEDORA", "ASIGNADO A" (nombre de un técnico, o "Oficina" para stock general), más Tipo, Estado y Cliente si hace falta. Al subir un archivo, reemplaza toda la tabla (después hay que tocar "Guardar cambios" para publicarlo). Todo pasa en el navegador, con SheetJS — no hace falta ningún servidor ni variable de entorno nueva.
Buscar qué línea tiene un cliente: en la pantalla "SIMs" de la app, arriba de la lista hay un buscador — escribiendo el nombre de un cliente, muestra qué SIM(s) tiene asociadas (número, compañía, y qué técnico la tiene), sin importar de qué técnico sea ni de las restricciones de visibilidad de arriba. Si el campo de búsqueda está vacío, vuelve a la vista normal (agrupada por técnico, o solo la propia según quién esté logueado).
Revertir una transferencia por error: si transferís una SIM y te equivocaste (o el que la recibe se dio cuenta de que no correspondía), en el detalle de esa SIM aparece un botón "↩ Revertir a [nombre]" que la manda directo de vuelta a quien te la dio — la app recuerda quién fue el último en transferírtela.
Dashboard de SIMs: dentro de "📊 Dashboards" hay una cuarta opción, "Dashboard de SIMs", con todos los movimientos (usar, devolver, transferir, reemplazar) — arranca mostrando el último mes, con pestañas de Día/Semana/Mes/Todo igual que los demás dashboards, y su propio botón de descarga a Excel (mismo criterio de acceso restringido a Sebastian Bartolozzi y oficina).
Gráficos de torta por compañía: arriba de la lista de movimientos hay un gráfico general (Movistar/Personal/Claro, todo el equipo) y uno más por cada técnico que tuvo movimientos en el período elegido — todos se recalculan solos al cambiar de pestaña de período. Solo cuentan como "uso" las SIMs que se instalaron en un cliente (usar y reemplazar) — transferir y devolver son movimientos de stock, no se grafican como uso.
Blanquear el historial: botón "🗑 Blanquear historial de
movimientos" — borra todo el historial de movimientos (usar,
devolver, transferir, reemplazar) de forma permanente, pidiendo
confirmación antes. No toca qué SIM tiene cada técnico ahora mismo,
solo el historial que alimenta este dashboard. Solo lo ven Sebastian
Bartolozzi, Brenda Thiesing, y el login general de oficina — para
que funcione, Brenda tiene que estar cargada en admin.html → pestaña
"Técnicos" con el nombre exacto "Brenda Thiesing".
Cuando un técnico envía con éxito el primer parte del día (no tenía ningún otro registrado hoy en el historial), la app revisa si tiene algún vehículo de la empresa tomado en ese momento. Si no figura ninguno, en la pantalla de "enviado" aparece un aviso: "¿No tomaste un vehículo hoy?", con dos opciones:
No deja seguir sin elegir una de las dos opciones. Se basa en el técnico que quedó cargado en el parte (no en quién inició sesión), y en el registro de "tomar/devolver" vehículos que ya existía — no hace falta ninguna variable de entorno nueva.
Se elevó la identidad visual de toda la app manteniendo la marca de SAT (navy + ámbar) — no se reemplazó por colores genéricos, se refinó la ejecución:
styles.css (:root): colores,
radios, sombras y tiempos de transición como variables, en vez de
valores sueltos repetidos por todo el archivo.chartjs-plugin-datalabels
para mostrar el % directo en las tortas y donas (antes solo se
veía al tocar) — el color del texto se adapta solo al fondo de cada
porción para que siempre se lea bien. Tipografía y colores de
tooltips/leyendas unificados en un solo lugar (antes cada gráfico
definía los suyos).Dashboards — más información, no solo más lindo:
.panel-tiles-grid / .panel-tile-grid) en vez de estar atados
solo a la pantalla principal — para poder aplicarla a futuras
pantallas sin repetir código.Bug reparado: si el CDN de Leaflet (la librería del mapa) no llegaba a cargar por cualquier motivo — sin conexión, bloqueado por algún firewall corporativo, caída puntual del CDN — una línea de configuración del mapa que corría directo al abrir la app explotaba y cortaba en seco la ejecución de todo el resto del script, dejando funciones sin relación alguna con el mapa (como el reintento de envíos sin conexión) rotas silenciosamente. Ahora esa configuración está protegida: si Leaflet no cargó, el resto de la app sigue funcionando normal — sólo el mapa en sí queda no disponible hasta que haya conexión, con un aviso claro en vez de romperse.
Rediseño completo, con más foco que la vuelta anterior — específicamente lo que pediste: tamaño de botones, tipografía, colores e íconos.
admin.html tenía un estilo bastante plano (tarjetas blancas sin
sombra, botones cuadrados, pestañas de solo texto) que había quedado
afuera de la vuelta de diseño anterior. Ahora tiene:
Sigue con fondo claro a propósito (a diferencia de la app, que es oscura) — es una herramienta de oficina, no algo que se use todo el día como la app del técnico, así que no hacía falta convertirla a tema oscuro para lograr el efecto "más profesional" pedido.
Colores por compañía: cada SIM (en la app y en la tabla de
admin.html) muestra una franja de color a la izquierda y un
puntito junto al nombre de la compañía — Movistar verde, Personal
celeste, Claro rojo (colores reales de cada marca, sin usar sus
logos, para no reproducir marcas registradas de terceros dentro de la
app). Se ve de un vistazo qué proveedor es cada línea sin tener que
leer el texto. Los mismos colores se usan en los gráficos de torta del
Dashboard de SIMs, para que todo sea consistente.
Comparación contra el período anterior: en el Dashboard general ("Resueltos", "Instalaciones", "Servicios técnicos") y en el financiero ("Monto total generado"), debajo del número aparece un indicador tipo "▲ 12% vs. anterior" o "▼ 8% vs. anterior" — compara el período elegido (día/semana/mes) contra el tramo equivalente inmediatamente anterior. La opción "Todo" no tiene comparación posible, así que ahí no aparece nada.
Valores directo en las barras: el gráfico "Resueltos por técnico" ahora muestra el número arriba de cada barra, sin tener que tocarla. El gráfico de monto por día (financiero) se dejó sin esto a propósito — tiene muchos puntos (uno por día del período) y saturaría la lectura; ahí conviene seguir usando el tooltip al tocar.
Botón "🚗 Vehículos" en el panel principal, con la flota que se cargue
en admin.html (arranca con 3: Renault Kangoo Blanca, Renault Kangoo
Gris, Moto — pero se pueden agregar, editar o borrar libremente
desde ahí, no están fijos en el código). Cada uno muestra si está libre
o en uso (por quién y desde qué hora). Al entrar a uno:
Registrar un evento sin devolver el vehículo: mientras lo tengas
tomado, además de poder devolverlo, aparece una sección aparte
"Registrar un evento sin devolver el vehículo" — sirve para anotar una
carga de combustible, una parada en la gomería, en el mecánico, o en
el lavadero, sin cortar el uso del vehículo. Se carga el tipo de
evento, el kilometraje actual (obligatorio — también actualiza el
kilometraje del vehículo, igual que al devolverlo), el monto del
ticket (opcional), y un detalle opcional. Queda registrado en el mismo
historial que las tomas/devoluciones (vehiculos-historial.json), y
se ve tanto en el Dashboard de vehículos como en su descarga a Excel.
Al registrarlo, aparece un aviso confirmando ("Evento registrado:
[tipo].") y vuelve solo a la pantalla de selección de vehículos.
Administrar la flota: en admin.html → pestaña "Vehículos":
⚠️ Ojo con renombrar un vehículo que en ese momento esté "tomado" por un técnico — el historial de uso queda ligado al nombre que tenía en el momento de tomarlo, así que renombrarlo a mitad de un uso activo puede generar confusión (mejor esperar a que esté libre).
Alertas de mantenimiento: se carga el kilometraje actual de cada vehículo y se pueden agregar umbrales configurables (cualquier cantidad, no solo los ejemplos de arranque) — cada uno con nombre, tipo (kilómetros o fecha), el valor límite, y con cuánta anticipación avisar (km o días antes). Ejemplos: "Cambio de aceite" a los 50.000 km avisando 1.000 km antes, "VTV" con fecha límite avisando 30 días antes, etc. Cuando se entra al detalle de un vehículo en la app, si algún umbral está cerca o vencido, aparece un aviso destacado (naranja si se acerca, rojo si ya se pasó).
No hace falta ninguna variable de entorno nueva — reutiliza las que ya
existen. Se guarda en vehiculos-config.json (configuración y km
actual) y vehiculos-historial.json (registro de cada toma/devolución),
en el mismo repo privado de datos.
Los tres dashboards (general, financiero y de vehículos) tienen un
botón "⬇ Descargar Excel del período" que arma un .xlsx al vuelo del
lado del navegador (con SheetJS) — no hace falta ningún servidor ni
variable de entorno nueva.
admin.html (antes tenía los 3
nombres fijos de memoria, lo cual se desactualizaba si se
agregaba/renombraba un vehículo).Botón "📜 Historial" en el panel principal. Muestra, en una lista, los
servicios completados en los últimos 4 días (hoy + los 3 días
anteriores). Cada técnico ve solo los suyos (o los que hizo junto a
otro, si participó como segundo técnico) — solo Sebastian Bartolozzi
(y el login general de oficina) ven el listado completo de todo el
equipo. Un texto arriba de la lista aclara qué vista está activa. Cada
tarjeta muestra N° de servicio, cliente, dirección, localidad, fecha y
horario de entrada/salida. Usa el mismo historial que ya alimenta los
dashboards (/api/historial.js), así que no hace falta ninguna
variable de entorno nueva.
Botón "🚨 Guardias" en el panel principal. Muestra quién está de guardia en ese momento (con botón de llamada y de WhatsApp para contactarlo directo), y los próximos turnos.
La guardia rota semanalmente, de lunes 9:00 a lunes 9:00. Se
administra desde admin.html → pestaña "Guardias":
No hace falta ninguna variable de entorno nueva — se guarda en el
mismo repo privado de datos (guardias-config.json).
admin.html)
tienen un botón de "ojito" (👁) al lado — al tocarlo, muestra la
contraseña en texto plano para revisar que esté bien escrita; vuelve
a ocultarse tocándolo de nuevo. Por defecto siempre arranca oculta.El plan gratuito de Vercel permite como máximo 12 funciones
serverless por proyecto (cada archivo dentro de /api cuenta como
una). Ya llegamos a ese límite una vez — por eso varios endpoints
chicos que hacían básicamente lo mismo (leer/guardar un archivo de
configuración) se unificaron en uno solo:
/api/datos.js — reemplaza a los antiguos config.js,
tecnicos.js, materiales-catalogo.js, consultas-categorias.js,
guardias-config.js y credenciales.js. Se usa con un parámetro
?coleccion=<nombre> (config, tecnicos, materiales,
consultas-categorias, guardias, credenciales, vehiculos,
push-subscripciones, sims).
Quedan 11 funciones en total (servicios, cronograma, geocode,
historial, foto, consultas, datos, recurso-uso,
cron-diario, enviar-mail, comodato) — con 1 función libre
antes de volver a tocar el límite del plan gratuito de Vercel.
recurso-uso.js reemplaza a los antiguos vehiculo-uso.js y
sim-uso.js (fusionados), y además maneja la lógica de herramientas
— las tres comparten el mismo patrón de "tomar / devolver /
transferir + historial" (ver ?recurso=vehiculo|sim|herramienta).
Antes de eso, ya se había liberado una función fusionando foto.js
(mostrar una foto) y upload-foto.js (subir una foto) en un solo
archivo. La próxima vez que haga falta un endpoint nuevo, sumarlo como
colección dentro de datos.js si es posible — o si hace falta liberar
otra función, vehiculo-uso/sim-uso ya viven fusionados así que el
próximo candidato natural sería revisar si historial.js puede
sumarse a datos.js como una colección con lógica especial.
Se hizo una pasada de revisión general de toda la app: sintaxis de
los 12 archivos de /api, de app.js, index.html, admin.html y
styles.css; búsqueda de variables/funciones declaradas más de una
vez; cruce de todos los getElementById contra los id reales del
HTML (274 referencias, ninguna rota); y una recorrida automatizada
por las pantallas principales (login, servicios, cronograma,
manuales, dashboards, guardias, historial, vehículos, SIMs) y por el
flujo completo de cargar un parte y firmarlo (incluida la validación
nueva de aclaración obligatoria) — sin encontrar errores nuevos.
De paso, se aprovechó para fusionar foto.js + upload-foto.js y
liberar una función (ver más abajo).
historial.json) va a ir
creciendo con el tiempo — para un uso de varios años podría convenir
archivarlo o rotarlo periódicamente, aunque para el volumen actual de
la empresa no debería ser un problema en el corto/mediano plazo.