Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
App de gestão de pedidos da Torteria Sandra Zampoli (Next.js + Supabase). O app é instalável de verdade — no Android o Chrome oferece "Instalar app" (WebAPK, abre em tela cheia), e não apenas um atalho do navegador. As peças: Para trocar o logo: substitua public/logo.jpg e rode Os PNGs precisam ser opacos (sem transparência) — o iOS descarta um apple-touch-icon com canal alfa e mostra um monograma no lugar do ícone.檢視完整 README ↗
App de gestão de pedidos da Torteria Sandra Zampoli (Next.js + Supabase).
O app é instalável de verdade — no Android o Chrome oferece "Instalar app" (WebAPK, abre em tela cheia), e não apenas um atalho do navegador. As peças:
| Arquivo | Papel |
|---|---|
public/manifest.webmanifest | nome, display: standalone, start_url, cores e ícones |
public/sw.js | service worker — requisito do Chrome para o WebAPK e casca offline; não guarda página de dados em cache (ver "Salvar pedido") |
src/components/InstalarApp.tsx | registra o SW e mostra o convite de instalação |
public/icon-*.png, public/apple-touch-icon.png | ícones gerados a partir de public/logo.jpg |
Para trocar o logo: substitua public/logo.jpg e rode
node scripts/gerar-icones-pwa.mjs
Os PNGs precisam ser opacos (sem transparência) — o iOS descarta um
apple-touch-icon com canal alfa e mostra um monograma no lugar do ícone.
Quando a requisição ao Supabase não chega a ser respondida (celular sem sinal,
DNS, projeto fora do ar), o supabase-js não lança exceção: devolve um erro
cujo message é o texto cru do fetch — TypeError: fetch failed no servidor,
TypeError: Failed to fetch no navegador. Esse texto aparecia direto na tela.
| Arquivo | Papel |
|---|---|
src/lib/erros.ts | mensagemErro() traduz falhas de rede/timeout para português; erros do banco continuam com o texto original |
src/lib/supabaseFetch.ts | prazo de 15s nas requisições dos clientes Supabase, para a tela não ficar pendurada |
src/components/AvisoConexao.tsx | faixa de aviso nas telas de lista — sem ela um erro de rede vira lista vazia e R$ 0,00, que parece dado real |
src/components/PainelSemConexao.tsx | telas de detalhe: em falha de rede mostra "tentar de novo" em vez de 404 ("pedido não encontrado") |
src/app/error.tsx | rede de segurança para o que escapar, com botão de tentar de novo |
O seletor "Cliente → Existente" do formulário de pedido lê a mesma tabela da
tela de Clientes, mas por outra rota. Um cliente cadastrado em /clientes não
aparecia no seletor: revalidatePath("/clientes") não alcança /pedidos/novo,
e o navegador continuava servindo o payload que o roteador do Next guardou na
primeira visita àquela rota.
| Arquivo | Papel |
|---|---|
src/app/clientes/actions.ts | revalidarTelasComClientes() revalida /clientes e /pedidos/novo ao criar/editar; listarClientesAction() devolve a lista para o formulário |
src/app/pedidos/PedidoForm.tsx | relê a lista ao abrir o formulário e oferece "Recarregar" — cobre também o cadastro feito em outra aba ou outro celular |
Sintoma relatado: em alguns aparelhos a pessoa salvava o pedido e ele sumia. Eram três causas somadas, todas invisíveis para quem usa:
startTransition(async …) sem try/catch: quando a requisição nem
completava (sinal caindo, função derrubada por tempo), a rejeição não era
tratada, o botão voltava ao normal e nada aparecia na tela.insert em itens_pedido não tinha o
erro conferido: o pedido nascia sem item nenhum e com valor zerado.| Arquivo | Papel |
|---|---|
public/sw.js | navegação só da rede; cache apenas para estáticos com hash na URL |
src/app/pedidos/PedidoForm.tsx | try/catch com mensagem na tela, estado próprio de salvamento (o isPending da transição não cobre o await, então o botão não travava o segundo toque) e "Abrir pedido" quando o pedido existe mas terminou com aviso — tentar de novo criaria um segundo |
src/components/FabPedido.tsx | mesmo tratamento no "Pedido Rápido" |
src/app/pedidos/actions.ts | erro do insert dos itens devolvido junto com o id do pedido criado |
Os dois campos do formulário de pedido eram <select> nativos: no celular
viravam uma roda com dezenas de nomes em ordem alfabética, sem busca e sem o
telefone à vista para distinguir dois clientes de mesmo nome. Agora os dois são
seletores com busca, e o que foi escolhido fica resumido em um cartão com botão
de "Trocar" — o resto da lista sai da frente da quantidade e do preço.
| Arquivo | Papel |
|---|---|
src/app/pedidos/SeletorCliente.tsx | busca por nome ou telefone no mesmo campo (acentos ignorados); lista sem busca mostra os 30 primeiros; sem resultado leva ao cadastro já com o que foi digitado |
src/app/pedidos/SeletorProduto.tsx | busca por nome e descrição (é na descrição que fica o sabor) mais chips de categoria; cada linha mostra unidade e preço padrão |
src/app/pedidos/novo/page.tsx | passa produtos com a categoria (categorias_produto(nome, ordem)) e a lista de categorias para os chips |
src/app/produtos/ProdutosClient.tsx | mesma busca e chips no catálogo, que não tinha filtro nenhum |
Antes só o rascunho podia ser apagado; o pedido lançado por engano ficava para sempre, e o cadastro de cliente não tinha como sair da lista.
| Regra | Onde |
|---|---|
| Pedido em rascunho ou novo pode ser excluído de vez (itens, imagens e ficha de topper vão junto). De "produzindo" em diante o caminho é cancelar, que preserva o histórico | excluirPedidoAction() em src/app/pedidos/actions.ts, botão em src/app/pedidos/[id]/StatusActions.tsx |
Cliente só pode ser excluído sem nenhum pedido vinculado — pedidos.cliente_id é on delete set null, então apagar um cliente com pedidos não daria erro: deixaria pedidos órfãos, sem histórico e sem telefone. Com pedidos, a ficha mostra o motivo no lugar do botão | excluirClienteAction() em src/app/clientes/actions.ts, botão em src/app/clientes/[id]/ExcluirCliente.tsx |
O dashboard mostrava a agenda só como lista de dias com pedido — para saber quanto trabalho cai numa semana era preciso rolar a tela e contar. A "Agenda de Entregas" põe mês e semana em grade, com a quantidade de pedidos em cada dia; tocar num dia abre quem é e em que etapa está.
| Decisão | Por quê |
|---|---|
Busca própria no page.tsx, com .neq("status", "cancelado") | a lista do dashboard traz só pedidos ativos (sem entregue nem cancelado). Reaproveitá-la deixaria os dias já entregues com contagem zero, que parece dia livre. Só cancelado fica de fora — não é trabalho a fazer nem histórico de entrega |
Recorte PedidoCalendario em vez de select("*") | a grade precisa de data, etapa, tipo, hora e nome; a busca não tem recorte de data (a pessoa navega para qualquer mês), então vale trazer poucas colunas de todos os pedidos |
| Tom do número cresce com o volume (1–2, 3–4, 5+) e a semana ganha barra proporcional | a carga do período aparece antes de ler número por número |
| Contagem do rodapé ignora as sobras das semanas vizinhas | "53 pedidos no mês" conta o mês, não os dias de outro mês que completam a primeira e a última linha da grade |
| Arquivo | Papel |
|---|---|
src/lib/calendario.ts | dias do mês/semana, navegação, rótulo do período e agrupamento por data_entrega — dividido com o calendário da tela de Pedidos, que fazia as mesmas contas inline |
src/app/DashboardCalendario.tsx | a grade em si: alternância mês/semana, atalho "Hoje" quando o período não contém o dia atual e o painel do dia escolhido |
src/app/page.tsx | busca do calendário junto das outras (mesmo Promise.all, mesmo aviso de falha de conexão) |
Sintoma relatado: "quando há mais de um bolo não permite alterar os dois,
apenas um". A precificação do "Feito" era um par único no pedido — peso ×
preco_por_kg —, então um pedido com dois bolos só tinha onde registrar o peso
real de um. O peso do outro continuava sendo o combinado na venda, e o valor
saía errado sem nada na tela indicando isso.
Agora, quando o pedido tem itens lançados, a precificação é item a item: cada linha tem o seu peso (ou quantidade) real e o seu preço, e o pedido recebe a soma — que é de onde o financeiro lê.
Os três campos de cada linha — quantidade, preço unitário e valor — são a mesma conta vista de ângulos diferentes, e qual deles se sabe primeiro muda com o pedido: às vezes o bolo é pesado e o preço do kg é o de tabela (sai o valor), às vezes o valor já foi combinado com a cliente e o que falta é saber em quanto ficou o kg. Preencher dois preenche o terceiro, nas duas direções.
| Decisão | Por quê |
|---|---|
Colunas novas (quantidade_real, preco_real, valor_real) em vez de sobrescrever quantidade/preco_unitario/valor_total | o que foi combinado na venda continua sendo a referência da regra dos 300g — sem ele não há como saber se o bolo passou do pedido |
| Regra dos 300g aplicada por item | o teto é o peso daquele bolo; somar os pesos do pedido deixaria um bolo muito acima compensar outro abaixo |
| Só itens por peso entram na regra | doce vendido por cento ou unidade não tem folga de peso: o que foi apurado é o que se cobra |
Soma gravada em valor_calculado (sem corte) e preco_corrigido (com corte) | são as mesmas colunas da precificação antiga, então financeiro, dashboard e valor estimado seguem lendo o mesmo lugar |
preco_por_kg do pedido fica nulo com mais de um item | não existe "o preço por kg" de um pedido com dois bolos de preços diferentes; o preço de cada um está na linha dele |
| Pedido sem item mantém a tela antiga | pedido lançado antes do catálogo (ou rascunho) não tem item para precificar |
| Lista de itens mostra o real quando existe | depois de registrar 1,650 kg, a lista continuar mostrando 1,500 kg faria a tela contradizer o que acabou de ser gravado |
| Preço e valor se recalculam nas duas direções, com o peso como base | era só peso × preço = valor: quem fechava o preço pela cliente ("esse sai por R$ 150") tinha que dividir de cabeça para achar o preço do kg |
ancora guarda o último dos dois que foi digitado | corrigir o peso depois precisa recalcular o outro campo; sem isso, ajustar o peso sobrescreveria o valor que a pessoa acabou de combinar |
Preço unitário derivado com 4 casas (numeric(10,4)) | R$ 150 em 1,8 kg dá R$ 83,3333/kg — arredondar para centavos faria o valor de volta sair R$ 150,01 |
| Campo apagado não apaga o outro | quem limpa o valor para redigitar ainda tem o preço na tela, e o item segue valendo o que valia até o número novo chegar |
| Arquivo | Papel |
|---|---|
supabase/migrations/013_add_precificacao_itens.sql | as três colunas novas em itens_pedido — precisa ser executada no SQL Editor do Supabase |
src/lib/precificacao.ts | a conta por unidade de medida, a volta dela (precoUnitarioDoValor) e a regra dos 300g, agora num lugar só (PedidoForm e ItensForm tinham cópias da mesma conta) |
src/app/pedidos/[id]/PrecificacaoForm.tsx | formulário por item com os três campos ligados, aviso de corte em cada linha e a soma no rodapé; os formulários de pedido sem item (bolo e doce) usam os mesmos campos ligados |
src/app/pedidos/[id]/actions.ts | salvarPrecificacaoItensAction grava os itens primeiro e só então o total do pedido |
Revisão da tela inteira. O ponto de partida era a pergunta "pedido cancelado entra nas contas?" — entrava em um lugar, e a conferência puxou o resto.
Receita, ticket médio, a receber e a evolução mensal já saíam limpos: as buscas
filtram status = 'entregue' e status = 'feito', e cancelado não é nenhum dos
dois. O furo estava no topper.
| Onde | O que acontecia | O que passa a valer |
|---|---|---|
| "A pagar fornecedores" | a busca de toppers_pedido não olhava o pedido: o topper de um pedido cancelado continuava cobrado como dívida em aberto — e a tela de Toppers já não mostra esse pedido desde 143c4c5, então não havia nem como quitá-la | pedidos(status) vem junto e o cancelado sai da conta; o que saiu aparece em uma linha de nota, para o total não mudar sem explicação |
| "Pago no período" | idem | continua contando: o dinheiro saiu antes do cancelamento. Quando há valor assim, a nota diz quanto e por quê |
| Toda a tela | o cancelado sumia sem deixar rastro — mês com dois cancelamentos parecia só um mês fraco | bloco "N pedidos cancelados" (fechado por padrão) com o que deixou de entrar, listando os pedidos |
| Correção | Por quê |
|---|---|
| Ticket médio divide pelas entregas com valor | dividir por todas as entregas jogava a média para baixo a cada pedido sem valor registrado, como se o preço de venda tivesse caído |
| Margem aparece com receita > 0 | exigia custo lançado: o período sem gasto nenhum — quando a margem é a melhor possível — ficava sem margem na tela |
Entrega com valor 0 conta como "sem valor" | é o que o campo vale quando ninguém preencheu; mostrar "R$ 0,00" com o ✓ de conferido escondia justamente a entrega a acertar |
| Aviso de entregas sem valor diz o efeito e o caminho | antes só contava os pedidos; agora diz que receita e ticket estão menores que o real e onde corrigir |
| "A Receber" avisa que não depende do período | somar "a receber" à receita do período não bate com nada — são recortes diferentes, e nada na tela dizia isso |
| Correção | Por quê |
|---|---|
Título e vazio seguem o período (periodo.label) | eram fixos em "Custos do Mês" / "este mês", mas a lista obedece ao seletor lá de cima — em um recorte de 6 meses o rótulo mentia |
| Data do formulário nasce dentro do período | com período passado na tela, o custo lançado "hoje" era gravado e sumia no mesmo instante, com cara de gravação falhada |
| Aviso quando a data escolhida cai fora do período | lançar gasto de outro mês é legítimo; sumir sem explicação não |
| Exclusão pede confirmação e mostra erro | um toque no × ao lado do valor apagava o lançamento sem volta, e a falha de rede era descartada em silêncio |
| Card de custos mostra a composição | o KPI soma lançamentos + toppers pagos, e o bloco de baixo só os lançamentos: dois totais diferentes com nomes parecidos |
| Arquivo | Papel |
|---|---|
src/app/financeiro/ListaFinanceira.tsx | listas de pedidos com corte em 8 linhas e "ver os N restantes" — em 6 meses passavam de cem linhas e empurravam toppers e evolução para fora do alcance |
src/app/financeiro/CanceladosSection.tsx | bloco recolhível dos cancelados do período |
src/app/pedidos/[id]/actions.ts | revalidatePath("/financeiro") ao cancelar, andar/voltar status e gravar preço ou valor de entrega — são exatamente as ações que mudam receita e a receber, e sem elas o financeiro ficava mostrando o total anterior |
src/app/toppers/actions.ts | idem para salvar ficha, marcar etapa e registrar/desfazer pagamento do topper |
/cardapio)Página para clientes, sem menu do app e fora dos buscadores (noindex). Não há
link para ela no app: só chega quem recebe o endereço, que fica em
Configurações > Cardápio Online com botão de copiar. O endereço mostrado é o
curto, /c (redireciona para /cardapio, em next.config.mjs), pensado para a
bio do Instagram.
Em Produtos > Exportar Cardápio (PDF) o app baixa o cardápio em PDF (A4).
O PDF é gerado no navegador: a página /cardapio?pdf=1 (seções abertas, tema
claro e fotos do carrossel em grade) é carregada num iframe escondido,
capturada com html2canvas e paginada com jspdf sem cortar itens ao meio;
os links do WhatsApp e do Instagram continuam clicáveis no PDF.
O layout é o do modelo aprovado, sem alterações. Do banco vêm só:
peso_kg, preço em R$/kg e a descrição do cadastro;cento, preço em R$/cento;Produtos por unidade não entram. Os textos fixos (regras de pedido, endereço, WhatsApp, Instagram) continuam no código, como no modelo.
| Arquivo | Papel |
|---|---|
supabase/migrations/014_add_cardapio_fotos.sql | tabela cardapio_fotos e bucket cardapio-fotos — precisa ser executada no SQL Editor do Supabase |
supabase/migrations/015_seed_cardapio_fotos.sql | cadastra as 12 fotos do modelo, servidas de public/cardapio/fotos/ (já em WebP, 600px) — rodar depois da 014 |
src/app/cardapio/ | página, CSS do modelo (preso a .cardapio-sz), fonte Babang e imagens do cabeçalho em public/cardapio/ |
src/app/api/cardapio-fotos/route.ts | recebe a foto, reduz para 600px e converte para WebP (mantém o fundo transparente dos recortes) |
src/app/configuracoes/FotosCardapioSection.tsx | link do cardápio e cadastro das fotos |
src/app/produtos/ExportarCardapioPdf.tsx | botão e geração do PDF do cardápio |
src/components/AppShell.tsx | tira navegação e convite de instalação das páginas públicas |