Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
이 앱은 이발소·레스토랑·패스트푸드·의류 매장을 위한 예약·재고·일일 매출·비용·마감 관리 기능을 제공한다
Aplicación para barberías, restaurantes, comidas rápidas y tiendas de ropa: turnos, inventario, ventas del día, gastos y cierre de caja.
비즈니스TypeScript · JavaScriptGitHub @snakedevsoft2온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Aplicación web para barbería, restaurante, comidas rápidas y tienda de ropa. Cada dueño tiene su propio usuario y su propio espacio de trabajo: registra sus ventas del día, sus gastos, cierra su caja y administra su propio catálogo de productos o servicios. Los datos de un negocio nunca se cruzan con los de otro. Hecha con Next.js 15 (App Router), Prisma y PostgreSQL. Lista para desplegar en Vercel y totalmente responsive (celular, tablet y computador).README 전문 보기 ↗
Aplicación web para barbería, restaurante, comidas rápidas y tienda de ropa. Cada dueño tiene su propio usuario y su propio espacio de trabajo: registra sus ventas del día, sus gastos, cierra su caja y administra su propio catálogo de productos o servicios. Los datos de un negocio nunca se cruzan con los de otro.
Hecha con Next.js 15 (App Router), Prisma y PostgreSQL. Lista para desplegar en Vercel y totalmente responsive (celular, tablet y computador).
/reservar/tu-negocio./reservar/... de otro tipo de negocio responde 404 (la
tienda de ropa se va a su catálogo), y la acción pública de reserva rechaza cualquier intento que
no venga de una barbería, aunque le cambien el negocio al formulario.Pensado para el local que vende por talla y color y necesita saber qué le queda.
S, M, L, XL y los colores, y quedan todas de una vez.+ y − mueven una unidad
sin abrir formulario./catalogo/<tu-negocio>: foto, precio, tallas disponibles y un botón que
abre WhatsApp con el mensaje escrito. Solo salen las prendas que marques para el catálogo.Desde Ventas, cada venta tiene un botón Factura que arma el PDF en el mismo navegador (no pasa por el servidor ni queda guardado en ninguna parte):
La factura lleva el logo y los datos del negocio, el número, la fecha, el cliente, quién atendió, el detalle con talla y cantidad, el total y la forma de pago.
Cada dueño le pone la cara de su marca a su aplicación, sin tocar la de los demás:
Cuando un cliente separa un turno, al dueño le llega el aviso con la hora, el nombre, el teléfono, el servicio y la nota. El número que recibe los mensajes se configura en Personalizar. Hay tres formas de enviarlo:
| Modo | Qué necesita | Cómo funciona |
|---|---|---|
| Solo enlace | Nada | Al terminar la reserva, el cliente ve un botón que abre WhatsApp con el mensaje ya escrito para el negocio. |
| Automático gratis | Una clave de CallMeBot, se pide una sola vez | La aplicación manda el aviso sola. |
| WhatsApp Business oficial | Token y número de Meta | Envío por la API oficial. |
Todo queda registrado en la sección Avisos, con el estado de cada mensaje y un botón para reenviarlo a mano si algo falló. Desde la agenda también hay un botón para escribirle al cliente por WhatsApp con la confirmación ya redactada.
httpOnly firmada con JWT (HS256). No se puede editar desde el navegador./panel/* sin sesión válida.userId de la sesión. Por ejemplo, editar un
producto usa updateMany({ where: { id, userId } }), así que un id ajeno simplemente no
coincide y no se toca nada.findFirst({ where: { id, userId } }) y devuelven 404 si el
registro es de otro negocio.userId del negocio. Si al barbero le
quitan el acceso o lo desactivan, su sesión deja de valer en la siguiente petición.npm test) que crean dos negocios con datos iguales y
comprueban que ninguno alcanza una sola fila del otro: ni leyendo, ni buscando por id, ni
editando, ni borrando. Corren contra la base de datos de verdad y limpian lo suyo al terminar.Las pruebas se probaron rompiendo el aislamiento a mano —insertando una línea de venta con el dueño equivocado por SQL directo, saltándose la aplicación— y comprobando que fallan. Una prueba que nunca falla no protege nada.
/admin)Para el dueño de Todoen1, no para los clientes. Muestra quién se registró, quién sigue entrando y qué se le puede prender o apagar a cada cuenta.
Tu correo va en la variable ADMIN_EMAILS del .env (varios separados por coma):
ADMIN_EMAILS="[email protected]"
Va en una variable de entorno y no en una columna de la base a propósito: así no hay ninguna pantalla, ninguna acción y ninguna consulta que pueda volver administrador a nadie. Para dar ese poder hay que entrar al servidor y cambiar la configuración. Si la variable está vacía, no hay administrador y el panel no existe para nadie — esa es la posición segura.
Quien no sea administrador y escriba /admin no ve un "no tienes permiso": vuelve a su panel,
como si la dirección no existiera. En Vercel se pone la variable en Settings → Environment
Variables.
| Se ve | No se ve |
|---|---|
| Nombre del negocio, tipo, correo, teléfono | Sus ventas, sus clientes, sus deudores |
| Cuándo se registró y cuándo entró por última vez | Sus precios, sus fotos, su catálogo |
| Cuántas ventas/gastos/productos tiene | Qué vendió, a quién y por cuánto |
| Qué apartados abre y cuántos días | Nada de su contenido |
A los clientes se les prometió que nadie ve lo suyo. Un panel de administración no es una excusa para romper esa promesa: es justo donde más fácil sería romperla sin darse cuenta. Por eso el límite está escrito en el código y comprobado con pruebas.
Apagarle un apartado a una cuenta. Desaparece de su menú y no lo puede volver a prender desde adentro. Tres estados: De fábrica (lo que le toca por su oficio), Apagado, Prendido (para estrenarle algo antes que a los demás).
Suspender una cuenta. Nadie de ese negocio puede entrar, ni el dueño ni sus empleados. No borra nada y se deshace cuando quieras. Pide un motivo y escribir el nombre del negocio, para que no pase de un clic distraído. Al cliente se le explica y se le da el WhatsApp de soporte.
Quitarle el acceso a una persona dentro de una cuenta, sin tocar el resto del negocio.
Reponerle la clave a quien no puede entrar. Es la salida para cuando el correo de recuperar
no llega: se equivocaron al escribirlo, cayó en spam, o todavía no hay RESEND_API_KEY. Funciona
igual para el dueño y para cada barbero o empleado con usuario propio.
No se le pone una contraseña y se le dicta: se le genera el mismo enlace de un solo uso que manda la pantalla pública, y la persona escribe la suya. Así el administrador nunca llega a saber la clave de un cliente. Se hacen las dos cosas a la vez: le sale el correo a la persona y el enlace queda en pantalla para copiarlo, porque si el correo no salió hay que poder pasárselo por WhatsApp. El correo dice que lo generó soporte y no «pediste cambiarla», que a quien no pidió nada solo lo asustaría. A una cuenta suspendida no se le genera: primero se reactiva, porque un enlace no le serviría de nada.
Ni el administrador puede saltarse la regla de los oficios: no se le puede dar la agenda por hora a un restaurante desde aquí. Tampoco se pueden apagar Resumen, Ajustes ni Soporte, ni suspender tu propia cuenta.
npm test — 56 pruebas, 11 de ellas del panel: que apagarle algo a una cuenta no toque a las
demás, que el interruptor del administrador mande sobre el del cliente, y que suspender no borre
nada. Otras 14 son de recuperar la contraseña (tests/recuperar.test.ts).npm run verificar:admin — recorre el panel en un navegador de verdad (necesita npm start
corriendo): que un cliente cualquiera no alcance /admin ni la ficha de otro negocio, que una
cuenta suspendida quede por fuera y que su sesión abierta se caiga.npm run verificar:clave-admin — reponer la clave desde la ficha de una cuenta: que el enlace
generado sirva de verdad para volver a entrar, que el de un barbero vaya a su correo y no al del
dueño, que una cuenta suspendida no reciba ninguno, y que un cliente cualquiera no alcance nada
de esto.La aplicación tiene veinte apartados porque sirve para negocios muy distintos. Nadie los necesita todos, así que el menú no es fijo.
Module, BusinessTypeModule), no en el código. De
ahí salen el menú, el configurador y la guía, así que los tres no pueden desfasarse entre sí.WorkspaceConfig.De fábrica, una tienda de ropa ve 13 apartados; con el arreglo Lo esencial quedan 7.
El quinto tipo, para el que no encaja en los otros cuatro: una ferretería, una papelería, un taller, una veterinaria.
Va al revés que los demás: en los otros se entrega poco de fábrica y la persona prende lo que le falte. Aquí no podemos adivinar el oficio, así que se le entrega casi todo —inventario, catálogo, ventas, gastos, caja, portafolio, reportes, equipo— y el primer paso del instructivo es justamente apagar lo que le sobre. Cartera, cuentas abiertas, proveedores, avisos y el asistente le quedan disponibles pero apagados.
Lo único que no lleva es la agenda por hora: esa sigue siendo de barbería.
Su catálogo arranca con dos productos llamados "(cámbiame)", para que se vea de una que hay que reemplazarlos, en vez de inventarle platos o cortes que no tienen nada que ver con lo suyo.
/panel/bienvenida) — cuatro pasos la primera vez: qué es esto,
qué necesitas ver, ajusta tu menú, tu primer paso. Tiene Saltar por ahora en todos los pasos
y retoma donde se dejó si se sale a mitad./panel/guia) — buscador para una duda suelta. Entiende cómo habla la gente: buscar
fiado encuentra Cartera, que en ninguna parte dice "fiado". No exige tildes.Las tres se pueden volver a abrir desde Soporte.
Se anota una sola fila por apartado, por persona y por día (ModuleEvent): solo para poder
decir "llevas dos meses sin abrir Proveedores, ¿lo apagas?" en vez de adivinar. No se guarda
cuánto tiempo, ni qué se tocó, ni qué se escribió.
npm install
Copia .env.example a .env. Si tienes Docker, la base de datos de pruebas se levanta sola:
npm run db:up # PostgreSQL en el puerto 55432
npx prisma migrate deploy
npm run db:modulos # el catálogo de apartados (obligatorio: de ahí sale el menú)
npm run seed # datos de ejemplo (opcional)
npm run dev
npm run db:modulos se puede correr las veces que sea: actualiza lo que cambió, crea lo nuevo y
borra lo que ya no aplica. En Vercel corre solo dentro de npm run build, así que un texto
corregido llega sin tocar la base a mano.
Para correr las pruebas de aislamiento:
npm test
Si prefieres una base en la nube, pon su cadena de conexión en DATABASE_URL y sáltate
npm run db:up.
Genera el secreto de sesión con:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
Abre http://localhost:3000 y crea tu cuenta en Crear cuenta.
npm run seed crea tres cuentas, todas con la contraseña demo1234:
| Correo | Negocio |
|---|---|
| [email protected] | Barbería El Estilo |
| [email protected] | Restaurante Doña Rosa |
| [email protected] | Comidas Rápidas El Punto |
| [email protected] | Segundo barbero de Barbería El Estilo (Andrés López) |
La página pública de reservas de la barbería de ejemplo queda en
/reservar/barberia-el-estilo.
Para apagar la base de datos local: npm run db:down.
La contraseña guardada no se puede recuperar: en la base solo vive su hash de bcrypt, que va en un solo sentido a propósito. Ni tú, ni nadie con acceso a la base, puede leerla. Lo que se hace es reemplazarla, y hay cuatro caminos:
1. Desde la app, con un enlace al correo. Es el camino normal. En la pantalla de ingreso, en
¿Olvidaste tu contraseña?, se escribe el correo y llega un enlace para poner una nueva. Sirve
igual para el dueño del negocio y para un barbero o empleado con usuario propio. Necesita
RESEND_API_KEY (ver Correo para recuperar la contraseña);
sin esa llave, el enlace de la pantalla sigue mandando al WhatsApp de soporte.
2. Si la recuerdas y solo quieres cambiarla, se hace en Ajustes dentro del panel, que pide la actual. El dueño además le puede poner una clave nueva a cualquier barbero desde Barberos, sin saber la que tenía.
3. Desde el panel de la plataforma, si tú administras Todoen1. En la ficha de la cuenta
(/admin/<id>), Reponer clave le manda el enlace a la persona y te lo deja en pantalla para
copiarlo, por si el correo no sale. Sirve para el dueño y para cada empleado con usuario propio, y
tú nunca ves su contraseña. Está explicado en Lo que se puede hacer.
4. Desde la terminal, cuando no hay correo configurado o la cuenta quedó trancada. Con la base prendida:
node scripts/cambiar-clave.mjs [email protected] "la-clave-nueva"
Sirve igual para la cuenta del negocio y para el usuario de un empleado o barbero.
Vale la pena saberlo, porque es la única puerta de la aplicación que se abre sin saber la contraseña:
PasswordReset no puede entrar con lo que ve. Va con SHA-256
y no con bcrypt porque el token lo generamos nosotros —no hay nada que adivinar a fuerza bruta— y
hace falta poder buscarlo de una por su huella.Las reglas de arriba están fijadas en tests/recuperar.test.ts (14 pruebas, con npm test). El
recorrido completo en un navegador de verdad —el enlace de la pantalla de ingreso, el enlace muerto,
guardar la clave nueva y entrar con ella, y la vista de celular— se comprueba con
npm run verificar:recuperar, que necesita npm start corriendo.
El panel de la plataforma (/admin) no tiene contraseña propia: se entra con la cuenta de
siempre, y aparece solo si ese correo está en ADMIN_EMAILS. Para dar o quitar ese permiso hay que
tocar la configuración del servidor, no una pantalla.
Entra a neon.com, crea una cuenta gratis y un proyecto. Copia la cadena de conexion.
Importante: usa la cadena directa, la que no lleva
-pooleren el servidor. Con la agrupada las migraciones pueden fallar durante el despliegue.
Se ve asi:
postgresql://usuario:[email protected]/neondb?sslmode=require
En tu computador:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
Guarda esa cadena larga. Firma las sesiones, asi que no la compartas con nadie.
Entra a vercel.com y crea la cuenta con tu GitHub.
Toca Add New y luego Project.
Elige el repositorio de esta aplicacion y toca Import.
Antes de desplegar, abre Environment Variables y agrega estas dos:
| Nombre | Valor |
|---|---|
DATABASE_URL | La cadena directa de Neon del paso 1 |
AUTH_SECRET | La cadena larga del paso 2 |
Toca Deploy y espera unos minutos.
No hay que tocar nada mas. El comando de compilacion ya crea las tablas solo:
prisma generate && prisma migrate deploy && next build
Entra a tu dominio, por ejemplo https://tu-proyecto.vercel.app, y toca Crear cuenta. Crea
una cuenta por cada negocio con su propio correo. Cada uno entra solo a lo suyo.
Despues, en Personalizar, cada dueno pone su color, su logo y el numero de WhatsApp que recibe los avisos.
El enlace publico de reservas de la barberia queda en
https://tu-proyecto.vercel.app/reservar/tu-negocio. Ese enlace se comparte por WhatsApp y el cliente no necesita crear cuenta.
Cada vez que subas cambios a GitHub, Vercel despliega solo. Las migraciones nuevas de la base de datos tambien se aplican solas.
prisma/
schema.prisma Modelo de datos
migrations/ Migraciones de PostgreSQL
seed.ts Datos de ejemplo
src/
actions/ Server Actions: auth, recuperar clave, catálogo, turnos, cuentas, ventas,
gastos, caja, ajustes, marca y avisos
app/
page.tsx Presentación
login, registro Acceso
recuperar/ Recuperar la contraseña olvidada (pedir enlace y ponerla nueva)
panel/ Panel privado de cada negocio (incluye personalizar y avisos)
reservar/[slug]/ Página pública de reservas (solo barbería)
catalogo/[slug]/ Catálogo público con fotos (solo tienda de ropa)
logo/[slug]/ Sirve el logo del negocio como imagen cacheada
components/ Interfaz reutilizable
lib/ Sesión, fechas, dinero, horarios, consultas, equipo, temas, WhatsApp,
correo (Resend) y enlaces de recuperación
middleware.ts Protección de rutas
La aplicación se probó de punta a punta en un navegador real (Microsoft Edge, en vista de escritorio y de celular) contra la compilación de producción y una base PostgreSQL real. Las 71 comprobaciones pasan, más 8 de la subida de logo: ingreso y registro, reserva de turno por el cliente, bloqueo de la hora ya tomada, rechazo de horas fuera del horario y del domingo, cierre de venta del turno, venta directa, gastos, cierre de caja con su diferencia, cuenta por mesa con descuento, edición del catálogo, reportes, aviso de WhatsApp al reservar, cambio de color y tema, subida y borrado del logo, navegación en celular, cierre de sesión y, sobre todo, que un negocio no alcance los datos ni la marca de otro.
El equipo de barberos se probó igual, en Edge contra la compilación de producción: alta de un barbero con su usuario, ingreso del barbero, menú recortado y redirección al resumen cuando intenta entrar a Barberos, Personalizar o Avisos, agenda en columnas con los dos barberos, reserva del cliente eligiendo barbero, cierre de venta a nombre del barbero, paso de un turno a otro barbero con su venta incluida, y la tabla de medición por barbero en Reportes.
Cada negocio tiene su propia página en /catalogo/<tu-negocio>, que se comparte por enlace o por
QR. Es lo mismo para la barbería que para la tienda de ropa: cambia lo que se muestra, no la idea.
/panel/portafolio: portada, titular, presentación, nota del pedido, mostrar u
ocultar los precios, y un interruptor para publicarlo o apagarlo./qr/<tu-negocio>), para pegar en la vitrina o el
mostrador. Se imprime del tamaño que sea sin pixelarse. Sale de la misma librería que ya lee los
códigos de barras, así que no agrega peso.El apartado del catálogo para quien compra en cantidad. Se arma en /panel/portafolio, en
Promociones al por mayor, y viene apagado hasta que el dueño lo encienda.
Las cuentas viven en src/lib/wholesale.ts, aparte de la base de datos
porque las usan el servidor y el navegador, y están cubiertas en
tests/mayoristas.test.ts.
Un chat en /panel/asistente que responde sobre el negocio y sobre cómo se hace algo en la app.
Para activarlo hay que poner GEMINI_API_KEY. Sin ella el apartado avisa que falta la clave y el
resto de la app sigue igual. Opcionalmente GEMINI_MODEL cambia el modelo; por defecto
gemini-2.0-flash.
Google AI Studio no se instala: es una página web. Solo hay que entrar y copiar una clave.
AIza.... Cópiala en ese momento, porque después se muestra
tapada.En tu computador, en el archivo .env de la raíz del proyecto:
GEMINI_API_KEY=AIza...tu-clave
En Vercel, en Settings → Environment Variables: nombre GEMINI_API_KEY, valor la clave, y
márcala para Production, Preview y Development. Después hay que volver a desplegar para que
la tome.
El plan gratuito da un límite de peticiones por minuto y por día que le sobra a un negocio: cada pregunta del asistente son unos 600 tokens. Si algún día se pasa, la app lo dice con un mensaje ("Se acabaron las consultas gratuitas por hoy") y no cobra nada ni se rompe.
No compartas esa clave ni la subas al repositorio. El .env ya está en .gitignore. Si se te
escapa, en la misma página de AI Studio la puedes borrar y crear otra.
Lo que lo hace útil no es el modelo, es el contexto: antes de cada respuesta se arma un resumen con las cifras reales del negocio — ventas de hoy y del mes, comparación con los 30 días anteriores, lo más vendido, inventario con las tallas en rojo, agenda y cartera. Por eso puede decir "te estás quedando sin la M" en vez de "revisa tu inventario". El resumen pesa unos 480 tokens.
Decisiones que importan:
temperature en 0.3 y una regla dura en el prompt: usar solo los números del resumen y decir
"eso no lo tengo a la mano" antes que inventar. Un consejo con cifras inventadas hace más daño que
no dar consejo.Para lo que fiaste y todavía no te han pagado. En /panel/cartera.
/api/recordatorios — como mucho uno
cada 3 días por deuda, para no acosar al cliente.La decisión que importa: no contar la plata dos veces. Al anotar la deuda se pregunta si la venta ya se registró en Ventas:
CARTERA.Borrar un abono deshace también la venta que generó, si la hubo. Una deuda con abonos no se borra: se anula, para no perder el rastro de la plata que sí entró.
Tiene su propio juego de estilos (.auth-* en globals.css), aparte del resto de la aplicación. Es
deliberado: el panel es de bloques marcados porque se usa a diario y se lee de lejos; la puerta de
entrada busca lo contrario — sobria, con aire, bordes de un píxel y sombras que casi no se ven.
maxAge y el navegador la
borra al cerrarse. Es lo que se espera en el computador del local, donde entra más de una persona./recuperar, con el correo ya escrito si alcanzó a
escribirlo. Si no hay RESEND_API_KEY, ese mismo enlace cae al WhatsApp de soporte: preferimos
eso a un botón que promete un correo que nunca llega.Es lo que hace que quien olvidó su clave se destranque solo, sin escribirle a nadie. Se manda con
Resend, con fetch contra su API y sin instalar el paquete: es una sola petición con un JSON, y
no valía la pena una dependencia más por diez líneas.
Sin RESEND_API_KEY no se manda nada y el botón ¿Olvidaste tu contraseña? sigue cayendo al
WhatsApp de soporte. Nada se rompe: simplemente no está esa puerta.
re_ y después no se vuelve a mostrar.Aquí está el único paso que no es de un minuto. Resend presta [email protected] para probar,
pero con ese remitente solo deja mandarle al correo con el que abriste la cuenta. Sirve para ver
que todo funciona; no sirve para tus clientes.
Para escribirle a cualquiera hay que verificar un dominio propio: en Domains → Add Domain se
pone el dominio y Resend da unos registros DNS (SPF y DKIM) que se copian donde lo compraste. Cuando
quede verificado, el remitente va en MAIL_FROM:
RESEND_API_KEY=re_...tu-llave
MAIL_FROM="Todoen1 <[email protected]>"
En Vercel, en Settings → Environment Variables, las mismas dos. No hace falta APP_URL: la
dirección del enlace se saca de la misma petición, así que funciona igual en tu computador y en
producción.
El motivo queda en los registros del servidor: cuando Resend responde con error, se anota el código
y el texto que devolvió. Los dos casos de siempre son dominio sin verificar (con
[email protected] solo llega a tu propio correo) y llave mala o vencida. Del lado de quien
espera, lo primero es mirar en spam.
Además del correo y contraseña, se puede entrar con Google. Está hecho a mano contra el protocolo de Google (no con una librería de autenticación completa) porque la app ya tiene su propia sesión, y una librería de esas querría mandar en todo eso.
Para activarlo hay que crear un cliente OAuth en Google Cloud y poner dos variables:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
En el cliente OAuth, la URI de redirección autorizada es https://tu-dominio/auth/google/callback
(y http://localhost:3000/auth/google/callback para probar en local).
Sin esas variables el botón simplemente no aparece y todo lo demás sigue funcionando igual.
Quien entra con Google entra al mismo sitio y con los mismos permisos que entraría con su contraseña: se busca su correo entre los dueños y entre los empleados. Si el correo no está en ningún negocio, lo mandamos a crear la cuenta con el correo y el nombre ya puestos, porque para abrir un negocio hacen falta datos que Google no da.
Al reservar, el cliente decide si quiere que le recuerden su turno por WhatsApp — viene marcado, pero se puede quitar. Es su teléfono, así que la decisión es suya.
/api/recordatorios los manda solo. Está pensado para
Vercel Cron una vez al día y se protege con CRON_SECRET. Solo marca como enviado lo que salió de
verdad: si falla, queda pendiente en Turnos para mandarlo a mano.Staff.tourDoneAt, por persona y no por negocio./panel/soporte, con el WhatsApp directo. El mensaje sale ya escrito con
el nombre del negocio y de quien escribe, y hay seis motivos frecuentes que lo redactan solos.
El número vive en un único sitio: src/lib/support.ts.sep=; y marca UTF-8, que es lo que hace que
Excel en español lo abra en columnas y con las tildes bien.Cada negocio elige su propio color, así que la personalidad no puede venir del color: viene de la tipografía, la forma y la estructura, que sí son iguales para todos.
next/font desde el mismo dominio (sin pedirle nada a Google en producción). Toda la plata va con
números tabulares, así que las columnas de pesos alinean.border-edge, un token que el motor de temas genera
junto al resto de la paleta) y sombra dura desplazada. La sombra se reserva para lo que se toca o
se mira primero — las cifras del día y los botones — porque si todo flotara no destacaría nada.Stat va en la negra a 32px con una raya de color debajo, para leerla
desde el otro lado del mostrador.Casi todo esto vive en globals.css y tailwind.config.ts, así que las pantallas que no se tocaron
heredan el estilo solas.
YYYY-MM-DD calculado en la zona horaria del
negocio, así el corte del día nunca se corre por UTC.src/lib/theme.ts deriva toda la paleta y la
escribe como variables CSS, así que cambiar el color de marca repinta la aplicación entera sin
recompilar nada.