Synaplan
기업을 위한 오픈소스 AI 플랫폼
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Demo de PWA para restaurantes pequeños: pedidos por mesa, para llevar y domicilio, pedidos en línea por enlace, cobro en efectivo y Nequi, fiado, gastos y cierre del día. Funciona sin conexión (Next.js + Supabase).
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗PWA mobile-first para dueños de locales pequeños de comida rápida y carnes a la plancha en Colombia: pedidos por mesa / para llevar / domicilio, cobro en efectivo y Nequi, fiado, gastos y cierre del día con resumen por WhatsApp. Es una demo para enseñar en visitas: todos los datos son ficticios.
Se diseñó primero para 360×640 y 390×844 (celular Android de gama media/baja, una mano, poca luz o sol):
objetivos táctiles de 48 px, texto de 16 px, acciones abajo, sin hover, sin scroll horizontal. En pantallas
grandes solo se centra una columna.
El cliente no necesita cuenta ni app: abre el enlace del restaurante (/pedir/<nombre-del-local>), arma su pedido,
elige recoger o domicilio y pone su nombre, celular y dirección. Después sigue su pedido en /seguimiento/<código>.
Cliente envía ──▶ "Por confirmar" ──▶ el dueño ACEPTA (con minutos) ──▶ Plancha ──▶ listo ──▶ entregado
(público) alerta sonora o RECHAZA con motivo plato por plato (el cliente lo ve)
/plancha): pedidos por preparar, del más viejo al más nuevo, con la espera en colores (verde/amarillo/rojo),
tipo (mesa, domicilio, para llevar) y cada plato como un botón grande. Cuando todos los platos están listos, el pedido
pasa solo a "listo". Mantiene la pantalla encendida y suena con cada pedido nuevo, y también cuando a un pedido que
ya estaba le agregan platos (quedan marcados Agregado). Deshacer corrige un error de dedo. Usa todo el ancho de
la pantalla: en tableta o con el celular acostado los pedidos van lado a lado.20261005000014_cocina_sin_bebidas_y_quitar_plato.sql).platos-fotos), porque el menú lo ve cualquiera con el enlace. Solo el dueño
sube, y solo a la carpeta de su restaurante.Requisitos: Node 22 y npm.
npm install
Crea el archivo .env.local (no se sube al repositorio) con una sola línea:
DEMO_BD=local
npm run dev # http://localhost:3000
El modo local corre las mismas migraciones y funciones de supabase/ en una base embebida (PGlite) guardada
en .local/. Solo funciona fuera de producción. Para empezar de cero, borra la carpeta .local/ (también se
recrea sola si cambian las migraciones).
La app sigue funcionando cuando se va la señal y sube todo sola cuando vuelve:
/sc, una página estática), que usa los mismos componentes con la copia
local. Al volver la señal la cola se sube en orden. Cada operación lleva una clave única (aplicar_operacion en la
base la ejecuta una sola vez aunque llegue repetida) y la fecha en que ocurrió: un cobro hecho a las 11 p. m. sin
señal cuenta en ese día aunque suba al siguiente (máximo 7 días).npm run e2e abre un Chrome real, corta la red y verifica de punta a punta (necesita Chrome instalado;
CHROME_PATH si está en otra ruta). Para probar a mano: DevTools → Network → Offline.El día no cambia a medianoche: cambia a las 4:00 a. m. (hora de corte) o cuando el dueño lo cierra. Un local nocturno ve en Inicio las ventas de toda la noche a la 1:44 a. m., y puede cerrar la caja al terminar el turno.
fecha): el día de Bogotá corrido por la
hora de corte del restaurante (restaurantes.hora_corte, 4 por defecto; hoy solo se cambia en la base).20261005000016_dia_de_trabajo.sql.Desde la app: Más → Reiniciar demo. Borra lo creado (pedidos, gastos, cobros, fiado, cambios al menú) y recarga el menú de 25 platos, 6 mesas, 8 clientes con fiado y 7 días de ventas, gastos y cierres. Solo lo permite la base en restaurantes de demostración y solo al dueño. Las fotos que les hayas tomado a los platos de ejemplo se conservan (se reconocen por el nombre del plato).
PIN de reinicio (opcional): en la demo todo el mundo entra con el mismo usuario, así que cualquiera con el enlace podría reiniciarla en plena visita. Para evitarlo, ponle un PIN desde el SQL Editor de Supabase (nunca desde la app, para que un visitante no pueda ponerlo ni cambiarlo):
select public.fn_definir_pin_reinicio('4821'); -- ponerlo o cambiarlo (4 a 8 dígitos; usa el tuyo)
select public.fn_definir_pin_reinicio(null); -- quitarlo
Con PIN, Reiniciar demo lo pide; se guarda cifrado en una tabla que nadie lee y cinco intentos fallidos bloquean el reinicio 5 minutos. Reiniciar no quita el PIN.
Modo local: borra la carpeta .local/.
Supabase: vuelve a ejecutar supabase/seed.sql (es idempotente).
Los totales se ven "vivos" porque los datos se generan relativos a hoy (sin azar: dos reinicios dan lo mismo).
Nada de esto lo hace el código por ti, y no pegues claves en el chat. La
service_rolede Supabase no se usa en esta demo: solo hace falta la URL y laanon key.
Supabase → New project. Guarda la contraseña de la base en tu gestor de contraseñas.
SQL Editor → pega todo supabase/esquema-completo.sql → Run. (Es la unión de las migraciones; se
regenera con npm run esquema.)
Authentication → Users → Add user: crea el usuario de la demo (correo + contraseña, con Auto Confirm User).
Abre supabase/seed.sql, cambia el correo de la línea marcada por el del usuario que acabas de crear y ejecútalo
en el SQL Editor. Crea el restaurante ficticio y le carga los datos de ejemplo.
Authentication → Providers → Email: desactiva Allow new users to sign up (que nadie se registre solo). En Authentication → Sign In / Providers, enciende Allow anonymous sign-ins: así entra el equipo con su código de invitación (cada celular recibe una sesión sin correo; sin un código válido no ve nada de ningún restaurante). Sin esto, "Unirme al equipo" dice que las invitaciones no están encendidas.
Project Settings → API: copia la Project URL y la anon public key.
Vercel → New Project (importa el repositorio, o vercel desde la terminal) y define las variables de entorno:
| Variable | Valor |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | la Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | la anon key (pública por diseño; la seguridad la da RLS) |
DEMO_EMAIL / DEMO_PASSWORD | el usuario del paso 3 (solo servidor, nunca con NEXT_PUBLIC_) |
NEXT_PUBLIC_MARCA_NOMBRE, NEXT_PUBLIC_MARCA_LEMA | opcional: marca y lema |
NEXT_PUBLIC_CONTACTO_WHATSAPP | opcional: tu WhatsApp (ej. 573001234567); activa "Quiero esta app" |
No definas DEMO_BD en Vercel (el modo local está bloqueado en producción de todos modos).
Despliega, abre la URL en el celular, Chrome → menú → Instalar aplicación.
Si la demo ya estaba en Supabase (de antes de los pedidos en línea): en el SQL Editor ejecuta solo
supabase/migrations/20261005000010_pedidos_clientes.sqlANTES de desplegar la versión nueva. Agrega el enlace público de cada restaurante, el estado "por confirmar", el marcado de platos y las funcionespublico_*que puede ejecutar un visitante sin cuenta (y nada más: ninguna tabla se abre).Lo mismo con las migraciones siguientes, en orden y cada una antes de desplegar la versión que la usa:
20261005000011_avisos_push.sql,20261005000012_limpieza_avisos.sql,20261005000013_enlaces_anteriores_y_platos_quitados.sql,20261005000014_cocina_sin_bebidas_y_quitar_plato.sql,20261005000015_mesas_y_pin_de_reinicio.sql,20261005000016_dia_de_trabajo.sql,20261005000017_carta_del_cliente.sqly20261005000018_demo_al_dia.sql. Las cinco últimas van juntas (la 18 redefine los datos de ejemplo con todo lo de las cuatro anteriores); después, Más → Reiniciar demo para que la demo tome las descripciones de los platos y Bebidas quede fuera de la plancha. Luego20261005000019_permisos_por_area.sql(roles del equipo y permisos por área; al dueño no le cambia nada) y20261005000020_invitaciones_equipo.sql(códigos de invitación, quitar del equipo, avisos por área y que reiniciar la demo deje el equipo como nuevo), y enciende Allow anonymous sign-ins (paso 5). Después20261005000021_dia_sin_cerrar.sql(avisa del día que quedó sin cerrar y deja cerrarlo tarde) y20261005000022_restaurante_de_verdad.sql(dar de alta restaurantes de verdad, ver abajo).
La demo la abre cualquiera con un toque, así que cualquiera es su dueño. Un restaurante de verdad no: su dueño entra
con su correo y su contraseña en /entrar (en la portada: «Ya tengo mi restaurante: entrar»), y su equipo con
códigos de invitación.
select public.fn_crear_restaurante('[email protected]', 'Nombre del local', 'Nombre del dueño');
Crea el restaurante vacío (sin platos ni mesas: los arma el dueño en Más → Menú y Mesas), con los roles de siempre y
los pedidos en línea apagados. Devuelve el enlace para sus clientes.20261005000023_primeros_pasos.sql): arma tu menú, agrega mesas,
invita a tu equipo, toma tu primer pedido y recibe pedidos por internet. Cada paso se marca solo con lo que ya hay
y la guía se va cuando todo está hecho o con «Ya no mostrar» (en ese celular). En la demo no aparece.| Comando | Qué hace |
|---|---|
npm run dev | Servidor de desarrollo |
npm run build / npm start | Compilar y servir en producción |
npm run lint | ESLint |
npm run typecheck | TypeScript sin emitir |
npm test | Pruebas (Vitest) |
npm run verificar | Lint + tipos + pruebas (lo que debe estar en verde) |
npm run esquema | Regenera supabase/esquema-completo.sql desde las migraciones |
npm run iconos | Regenera los íconos de la PWA (public/icons) |
npm run e2e | Prueba de punta a punta del trabajo sin conexión (Chrome real; ~3 min) |
npm run e2e:clientes | Prueba de punta a punta de pedidos en línea y plancha (3 celulares simulados; ~3 min) |
src/app/(app)/ Pantallas con barra de navegación: Inicio, Pendientes, Fiado, Más, Gastos, Menú, Mesas, Equipo, Pedidos en línea
src/app/(tarea)/ Pantallas de una sola tarea, sin barra: Tomar pedido, Cobrar, Cierre, Nuevo gasto, Ficha de cliente, QR para imprimir
src/app/(cocina)/ La plancha, a todo el ancho (tableta o celular acostado)
src/app/(publico)/ Lo que ve el CLIENTE sin cuenta: /pedir/<local> (menú y pedido) y /seguimiento/<código>
src/app/api/ Consultas de las pantallas "en vivo": seguimiento (público, por código), pulso y plancha (con sesión)
src/app/acciones/ Acciones del servidor: sync.ts (aplicarOperacion/obtenerSnapshot), clientes.ts, plancha.ts, publico.ts; validan con Zod y llaman funciones SQL
src/app/sc/ App sin conexión: una página estática con los mismos componentes y la copia local
src/lib/sync/ Trabajo sin conexión: tipos, vista local, motor de la cola, acciones del celular
src/app/api/foto/ Sirve las fotos de gastos solo con sesión y solo del propio restaurante
src/components/ ui (botón, hoja inferior, teclado numérico…), layout, fiado, gastos, sesión
src/lib/ dinero.ts (TODOS los cálculos), areas (qué ve cada rol), fechas (día de trabajo), horario (de atención), whatsapp, teclado, esquemas/ (Zod), datos/ (lecturas)
src/lib/local/ Modo local de desarrollo (PGlite)
supabase/migrations/ Tablas, RLS, funciones de lectura/escritura, demo, permisos, almacenamiento
supabase/seed.sql Carga de la demo en Supabase (idempotente)
public/ sw.js (service worker: estáticos + app sin conexión), offline.html, íconos
src/lib/dinero.ts, con pruebas;
otra prueba verifica que den lo mismo que la base de datos.restaurante_id y RLS; las llaves foráneas son
compuestas, así que ninguna fila puede apuntar a datos de otro restaurante.20261005000019_permisos_por_area.sql): las áreas son
pedidos, plancha, caja, fiado, gastos y cierre. Cada restaurante tiene sus roles (arranca con Mesero, Cocina y
Caja) y elige qué áreas abre cada uno; una persona puede tener varios roles y ve la suma. El dueño lo ve todo. El
candado está en la base: sin el área, la función de escritura falla y las tablas de plata no se leen. El dueño los
arma en Más → Equipo; la barra de abajo, Inicio, Más y los botones de cada pantalla muestran a cada quien solo
lo suyo, y una pantalla que no le toca explica «No tienes permiso» (src/lib/areas.ts).20261005000020_invitaciones_equipo.sql): en Más → Equipo, Agregar a
alguien pide el nombre y los roles y da un código de 8 letras y números (un uso, 48 horas) con su QR y su enlace
para WhatsApp. En el celular de esa persona se abre /unirme (el QR y el enlace ya traen el código): la app le abre
una sesión anónima de Supabase y canjea el código. Sin correo ni contraseña. El dueño la puede quitar del equipo y
su celular pierde el acceso al instante. Reiniciar demo saca al equipo de la visita anterior.node_modules/next/dist/docs/
(ver )..env.example solo trae nombres. .env.local está en .gitignore.X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy.src/test/aislamiento.test.ts, Postgres real): un
usuario del restaurante A no puede leer, insertar, modificar, anular ni subir fotos en el B, por ninguna tabla ni
función; sin sesión no se ejecuta ninguna función ni se lee ninguna tabla.src/test/acceso-directo.test.ts recorre TODAS las tablas y TODAS las funciones que existan (también
las que se agreguen después). Nadie escribe tablas directo; lo secreto (códigos de invitación, avisos push, PIN) no lo
lee ni el personal; sin local no se ve ni una fila; y cada función, llamada con datos vacíos o con ids de otro local,
no cambia nada ni deja ver datos ajenos. Además, la app nunca le entrega al navegador la dirección ni la llave de
Supabase: todo pasa por el servidor./pedir/<local>/datos, enlazada al pie del
menú). La base no recibe un pedido sin esa aceptación y guarda cuándo se dio (pedidos.datos_autorizados_at,
migración 20261005000025_autorizacion_datos.sql): es la prueba que pide la ley. Al crear un cliente de fiado, la
pantalla le recuerda al personal pedirle permiso. Si cambia el texto de la política, cambia también su fecha
(lib/publico/datos.ts). Pendiente: que un abogado revise el texto, y los datos de contacto de cada restaurante
(dirección, teléfono o correo), que la app todavía no guarda.lib/push/servicios.ts): Google, Mozilla, Apple y Microsoft.
Una dirección inventada se rechaza al guardarla y, si alguien la metió directo en la base, el servidor no le manda
nada y la borra. Así nadie puede usar el servidor para hacerle peticiones a otro sitio.publico_* (publico_menu, publico_crear_pedido,
publico_ver_pedido, publico_cancelar_pedido y las dos de avisos push); una prueba falla si aparece otra ejecutable por anon y ninguna
tabla se puede leer sin sesión. El seguimiento se consulta por un código aleatorio (uuid) imposible de adivinar, que
devuelve y nunca datos internos (quién lo atendió, cobros, otros pedidos). El enlace no se indexa
() y no sale en el hacia otros sitios.Facturación electrónica, recetas/inventario con descuento, gráficas, WhatsApp automático u oficial, impresión térmica, pagos en línea y multi-sede. Tampoco: costo de domicilio por zona, seguimiento del repartidor en mapa ni CAPTCHA (el antiabuso va en la base). (Sin conexión funciona el trabajo del mostrador; el cierre, el menú, las mesas, las anulaciones (incluido quitar un plato) y los pedidos en línea esperan a tener señal.)
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, DEMO_EMAIL, DEMO_PASSWORD) o el usuario no existe.supabase/seed.sql con el correo del usuario demo.npm run esquema (una prueba falla si el archivo unido no está al día) y vuelve a
pegarlo; las funciones nuevas deben terminar con select public.fn_aplicar_permisos();.20261005000024_pulso.sql/api/pulso, lib/pulso.ts): la huella de
los pedidos (32 letras que cambian con cualquier pedido nuevo, plato agregado, quitado o listo, cobro, entrega o
anulación) y el resumen del aviso de pedidos en línea. Pendientes, la plancha y la copia sin conexión solo traen
sus datos si la huella cambió.lib/ritmo.ts): rápido cuando algo acaba de cambiar o alguien toca la pantalla, y cada vez
más espaciado mientras todo sigue igual, hasta un tope: plancha 4 → 8 s, aviso 5 → 20 s, Pendientes 8 → 20 s. Si no
puede llegar un pedido en línea (apagados o fuera del horario) y no hay ninguno esperando, el aviso pregunta cada
minuto. La plancha se trae completa al menos cada minuto (para que avancen las esperas) y la copia sin conexión
cada 5 minutos (el menú y los clientes no tienen huella).src/proxy.ts, el que renueva la sesión): el pulso, la plancha y el seguimiento se lo
saltan, para que cada consulta sea una sola ejecución en Vercel y no dos. El pulso y la plancha renuevan la sesión
ellos mismos (la plancha pasa el día en la misma pantalla y no la pierde).20261005000013_enlaces_anteriores_y_platos_quitados.sql).NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY solo servidor, VAPID_SUBJECT); sin ellas la app
funciona igual con la alerta en pantalla. En iPhone solo con la app agregada a la pantalla de inicio. Hay un botón
Probar aviso para verificar que llega. Migración: 20261005000011_avisos_push.sql.
Los avisos no se quedan para siempre en celulares de visitas anteriores (migración 20261005000012_limpieza_avisos.sql):
salir de la sesión da de baja ese celular, reiniciar la demo da de baja todos menos el celular desde el que
se reinicia, y con el tope de 30 celulares lleno entra el nuevo y sale el más antiguo./?ir=/plancha, /?ir=/pedidos, y
/pedir/<local> (cliente, sin cuenta). Ojo: /?ir= entra como DUEÑO (sirve para abrir la plancha en una tableta
durante una visita). Para que alguien use la app con SU rol (mesero, cocina…), invítalo desde Más → Equipo.npm run e2e:clientes abre tres celulares simulados en Chrome (cliente, dueño y plancha) y recorre
pedir → alerta → aceptar → marcar platos → entregado, más rechazo, cancelación, cerrado y permisos.AGENTS.mdnoindexRefererplatos-fotos): el menú lo ve cualquiera con el enlace. Solo
el dueño sube, solo a la carpeta de su restaurante, y nadie puede listar el bucket ni editar o borrar.fn_definir_pin_reinicio, que ningún
usuario de la app puede ejecutar), se guarda cifrado en una tabla sin lectura para nadie y lo comprueba la base, no
la pantalla. Cinco intentos fallidos bloquean el reinicio 5 minutos.