From 14a36b68b57027fb30df7fc5aa7e7cf709c6e1ac Mon Sep 17 00:00:00 2001 From: Welton Moura Date: Wed, 22 Jul 2026 13:58:38 -0300 Subject: [PATCH] ajustes --- prisma/schema.prisma | 12 ++ src/controllers/order.controller.ts | 41 +++- src/routes/order.routes.ts | 7 + src/schemas/order.schema.ts | 16 ++ src/services/order.service.ts | 175 ++++++++++++++++- web/src/main.ts | 4 +- web/src/style.css | 28 +++ web/src/views/checkout.ts | 241 ++++++++++------------- web/src/views/kitchen.ts | 35 +++- web/src/views/orders.ts | 295 ++++++++++++++++++++++++++-- 10 files changed, 676 insertions(+), 178 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4434f18..a2ce683 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -45,6 +45,7 @@ model Comanda { nomeCliente String? status String @default("ABERTA") // ABERTA, FECHADA, PAGA, CANCELADA total Float @default(0.0) + valorPago Float @default(0.0) desconto Float @default(0.0) acrescimo Float @default(0.0) taxaServico Boolean @default(false) @@ -53,6 +54,7 @@ model Comanda { criadoEm DateTime @default(now()) fechadoEm DateTime? itens ItemPedido[] + pagamentos Pagamento[] } model ItemPedido { @@ -67,3 +69,13 @@ model ItemPedido { comanda Comanda @relation(fields: [comandaId], references: [id]) criadoEm DateTime @default(now()) } + +model Pagamento { + id String @id @default(uuid()) + comandaId String + comanda Comanda @relation(fields: [comandaId], references: [id]) + valor Float + formaPagamento String // DINHEIRO, CARTAO_CREDITO, CARTAO_DEBITO, PIX + observacao String? + criadoEm DateTime @default(now()) +} diff --git a/src/controllers/order.controller.ts b/src/controllers/order.controller.ts index e1c4ad3..d0d1cd6 100644 --- a/src/controllers/order.controller.ts +++ b/src/controllers/order.controller.ts @@ -1,5 +1,5 @@ import { FastifyRequest, FastifyReply } from 'fastify'; -import { createOrderSchema, addItemOrderSchema, updateItemStatusSchema, updateOrderSchema, payOrderSchema } from '../schemas/order.schema'; +import { createOrderSchema, addItemOrderSchema, updateItemStatusSchema, updateItemObservacaoSchema, updateOrderSchema, payOrderSchema, partialPaymentSchema } from '../schemas/order.schema'; import * as orderService from '../services/order.service'; import { z } from 'zod'; @@ -23,6 +23,23 @@ export async function listOrdersController(request: FastifyRequest, reply: Fasti return reply.send(orders); } +export async function listPaymentsController(request: FastifyRequest, reply: FastifyReply) { + const querySchema = z.object({ + dataInicio: z.string().optional(), + dataFim: z.string().optional(), + }); + + const query = querySchema.parse(request.query); + const filter: any = {}; + if (query.dataInicio) filter.dataInicio = query.dataInicio; + if (query.dataFim) filter.dataFim = query.dataFim; + + const payments = await orderService.listPayments( + (filter.dataInicio || filter.dataFim) ? filter : undefined + ); + return reply.send(payments); +} + export async function createOrderController(request: FastifyRequest, reply: FastifyReply) { const data = createOrderSchema.parse(request.body); const order = await orderService.createOrder(data); @@ -59,6 +76,17 @@ export async function updateItemStatusController(request: FastifyRequest, reply: return reply.send(item); } +export async function updateItemObservacaoController(request: FastifyRequest, reply: FastifyReply) { + const { id } = z.object({ id: z.string().uuid() }).parse(request.params); + const data = updateItemObservacaoSchema.parse(request.body); + try { + const item = await orderService.updateItemObservacao(id, data); + return reply.send(item); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } +} + export async function payOrderController(request: FastifyRequest, reply: FastifyReply) { const { id } = z.object({ id: z.string().uuid() }).parse(request.params); const body = request.body && Object.keys(request.body as object).length > 0 @@ -72,6 +100,17 @@ export async function payOrderController(request: FastifyRequest, reply: Fastify } } +export async function registerPaymentController(request: FastifyRequest, reply: FastifyReply) { + const { id } = z.object({ id: z.string().uuid() }).parse(request.params); + const data = partialPaymentSchema.parse(request.body); + try { + const pagamento = await orderService.registerPayment(id, data); + return reply.status(201).send(pagamento); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } +} + export async function updateOrderController(request: FastifyRequest, reply: FastifyReply) { const { id } = z.object({ id: z.string().uuid() }).parse(request.params); const data = updateOrderSchema.parse(request.body); diff --git a/src/routes/order.routes.ts b/src/routes/order.routes.ts index 3f0b103..e3173bc 100644 --- a/src/routes/order.routes.ts +++ b/src/routes/order.routes.ts @@ -6,6 +6,9 @@ export async function orderRoutes(app: FastifyInstance) { // Listagem geral (qualquer autenticado) app.get('/comandas', { preHandler: [authenticate] }, orderController.listOrdersController); + // Listagem de pagamentos (qualquer autenticado) + app.get('/pagamentos', { preHandler: [authenticate] }, orderController.listPaymentsController); + // Comandas (Garçom, Caixa, Admin) app.post('/comandas', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.createOrderController); app.post('/comandas/:id/itens', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.addItemToOrderController); @@ -13,11 +16,15 @@ export async function orderRoutes(app: FastifyInstance) { // Pagamento (Caixa, Admin) app.post('/comandas/:id/pagar', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.payOrderController); + app.post('/comandas/:id/pagamentos', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.registerPaymentController); // Painel Cozinha / Bar app.get('/painel/itens', { preHandler: [requireRoles(['COZINHA', 'BARMAN', 'ADMIN'])] }, orderController.listKitchenBarController); app.patch('/painel/itens/:id/status', { preHandler: [requireRoles(['COZINHA', 'BARMAN', 'ADMIN'])] }, orderController.updateItemStatusController); + // Editar observação de item (Garçom, Caixa, Admin) + app.patch('/comandas/itens/:id/observacao', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.updateItemObservacaoController); + // Excluir item de comanda (apenas CAIXA e ADMIN) app.delete('/comandas/itens/:id', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.deleteItemFromOrderController); } diff --git a/src/schemas/order.schema.ts b/src/schemas/order.schema.ts index ca2e532..17f732b 100644 --- a/src/schemas/order.schema.ts +++ b/src/schemas/order.schema.ts @@ -29,9 +29,25 @@ export const payOrderSchema = z.object({ formaPagamento: z.enum(['DINHEIRO', 'CARTAO_CREDITO', 'CARTAO_DEBITO', 'PIX']), }); +export const partialPaymentSchema = z.object({ + valor: z.number().min(0).optional(), + formaPagamento: z.enum(['DINHEIRO', 'CARTAO_CREDITO', 'CARTAO_DEBITO', 'PIX']), + observacao: z.string().optional(), + itemIds: z.array(z.string().uuid()).optional(), +}).refine( + (data) => (data.itemIds && data.itemIds.length > 0) || (data.valor !== undefined && data.valor > 0), + { message: 'Informe um valor positivo ou selecione itens para pagamento' } +); + export type CreateOrderInput = z.infer; export type AddItemOrderInput = z.infer; +export const updateItemObservacaoSchema = z.object({ + observacao: z.string().optional(), +}); + export type UpdateItemStatusInput = z.infer; +export type UpdateItemObservacaoInput = z.infer; export type UpdateOrderInput = z.infer; export type PayOrderInput = z.infer; +export type PartialPaymentInput = z.infer; diff --git a/src/services/order.service.ts b/src/services/order.service.ts index 343b3a9..315f5b4 100644 --- a/src/services/order.service.ts +++ b/src/services/order.service.ts @@ -1,5 +1,5 @@ import { prisma } from '../lib/prisma'; -import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateOrderInput, PayOrderInput } from '../schemas/order.schema'; +import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateItemObservacaoInput, UpdateOrderInput, PayOrderInput, PartialPaymentInput } from '../schemas/order.schema'; export interface ListOrdersFilter { statuses?: string[]; @@ -7,6 +7,42 @@ export interface ListOrdersFilter { dataFim?: string; } +export interface ListPaymentsFilter { + dataInicio?: string; + dataFim?: string; +} + +export async function listPayments(filter?: ListPaymentsFilter) { + const where: any = {}; + + if (filter?.dataInicio || filter?.dataFim) { + where.criadoEm = {}; + if (filter.dataInicio) { + const [y, m, d] = filter.dataInicio.split('-').map(Number); + where.criadoEm.gte = new Date(y, m - 1, d, 0, 0, 0, 0); + } + if (filter.dataFim) { + const [y, m, d] = filter.dataFim.split('-').map(Number); + where.criadoEm.lte = new Date(y, m - 1, d, 23, 59, 59, 999); + } + } + + return prisma.pagamento.findMany({ + where, + orderBy: { criadoEm: 'desc' }, + include: { + comanda: { + select: { + id: true, + identificador: true, + nomeCliente: true, + status: true, + }, + }, + }, + }); +} + export async function listOrders(filter?: ListOrdersFilter) { const where: any = {}; @@ -30,7 +66,8 @@ export async function listOrders(filter?: ListOrdersFilter) { include: { itens: { include: { produto: true } - } + }, + pagamentos: true, } }); } @@ -116,6 +153,28 @@ export async function updateItemStatus(itemId: string, data: UpdateItemStatusInp }); } +export async function updateItemObservacao(itemId: string, data: UpdateItemObservacaoInput) { + const item = await prisma.itemPedido.findUnique({ + where: { id: itemId }, + include: { comanda: true }, + }); + + if (!item) { + throw new Error('Item do pedido não encontrado'); + } + + if (item.comanda.status !== 'ABERTA') { + throw new Error('Não é possível editar itens de uma comanda fechada/paga/cancelada'); + } + + return prisma.itemPedido.update({ + where: { id: itemId }, + data: { + observacao: data.observacao ?? null, + }, + }); +} + export async function payOrder(comandaId: string, data?: PayOrderInput) { const comanda = await prisma.comanda.findUnique({ where: { id: comandaId }, @@ -128,15 +187,110 @@ export async function payOrder(comandaId: string, data?: PayOrderInput) { if (comanda.status !== 'ABERTA') { throw new Error('Comanda não está aberta para pagamento'); } - - return prisma.comanda.update({ + + const totalFinal = calcularTotalFinal(comanda); + const restante = Math.max(totalFinal - comanda.valorPago, 0); + + if (restante <= 0) { + throw new Error('Comanda já foi paga integralmente'); + } + + const [, comandaAtualizada] = await prisma.$transaction([ + prisma.pagamento.create({ + data: { + comandaId, + valor: restante, + formaPagamento: data?.formaPagamento ?? 'DINHEIRO', + observacao: 'Pagamento integral', + }, + }), + prisma.comanda.update({ + where: { id: comandaId }, + data: { + status: 'PAGA', + fechadoEm: new Date(), + formaPagamento: data?.formaPagamento, + valorPago: totalFinal, + }, + }), + ]); + + return comandaAtualizada; +} + +function calcularTotalFinal(comanda: { total: number; desconto: number; acrescimo: number; taxaServico: boolean }): number { + let total = comanda.total || 0; + total -= (comanda.desconto || 0); + total += (comanda.acrescimo || 0); + if (comanda.taxaServico) total += total * 0.10; + return Math.max(total, 0); +} + +export async function registerPayment(comandaId: string, data: PartialPaymentInput) { + const comanda = await prisma.comanda.findUnique({ where: { id: comandaId }, - data: { - status: 'PAGA', - fechadoEm: new Date(), - formaPagamento: data?.formaPagamento, - }, + include: { itens: true }, }); + + if (!comanda) { + throw new Error('Comanda não encontrada'); + } + + if (comanda.status === 'PAGA') { + throw new Error('Comanda já foi paga integralmente'); + } + + if (comanda.status === 'CANCELADA') { + throw new Error('Não é possível pagar uma comanda cancelada'); + } + + const totalFinal = calcularTotalFinal(comanda); + const restante = Math.max(totalFinal - comanda.valorPago, 0); + + if (restante <= 0) { + throw new Error('Comanda já foi paga integralmente'); + } + + let valorPagamento = data.valor ?? 0; + + if (data.itemIds && data.itemIds.length > 0) { + const itensSelecionados = comanda.itens.filter(i => data.itemIds!.includes(i.id)); + if (itensSelecionados.length === 0) { + throw new Error('Nenhum item válido selecionado'); + } + valorPagamento = itensSelecionados.reduce((acc, i) => acc + i.precoUnitario * i.quantidade, 0); + } + + if (!valorPagamento || valorPagamento <= 0) { + throw new Error('Valor do pagamento deve ser maior que zero'); + } + + if (valorPagamento > restante) { + throw new Error(`Valor excede o restante. Disponível: R$ ${restante.toFixed(2)}`); + } + + const novoValorPago = comanda.valorPago + valorPagamento; + const totalmentePago = novoValorPago >= totalFinal; + + const pagamento = await prisma.$transaction([ + prisma.pagamento.create({ + data: { + comandaId, + valor: valorPagamento, + formaPagamento: data.formaPagamento, + observacao: data.observacao ?? null, + }, + }), + prisma.comanda.update({ + where: { id: comandaId }, + data: { + valorPago: novoValorPago, + ...(totalmentePago ? { status: 'PAGA', fechadoEm: new Date() } : {}), + }, + }), + ]); + + return pagamento[0]; } export async function updateOrder(comandaId: string, data: UpdateOrderInput) { @@ -168,7 +322,8 @@ export async function updateOrder(comandaId: string, data: UpdateOrderInput) { include: { itens: { include: { produto: true } - } + }, + pagamentos: true, } }); } diff --git a/web/src/main.ts b/web/src/main.ts index 30622e7..d4ef636 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -9,9 +9,9 @@ import { renderDashboard } from './views/dashboard'; const app = document.getElementById('app')!; -export function navigateTo(path: string) { +export async function navigateTo(path: string) { window.history.pushState({}, '', path); - router(); + await router(); } (window as any).navigateTo = navigateTo; diff --git a/web/src/style.css b/web/src/style.css index 7269533..6f28727 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -803,6 +803,24 @@ tbody td { white-space: nowrap; text-align: right; } + #detalhes-itens-tbody td.item-obs .item-obs-editable { + cursor: pointer; + display: inline-block; + padding: 0.15rem 0.3rem; + border-radius: 4px; + border: 1px dashed transparent; + transition: border-color 0.15s, background 0.15s; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + #detalhes-itens-tbody td.item-obs .item-obs-editable:hover { + border-color: var(--accent-primary); + background: var(--bg-secondary); + } + #detalhes-itens-tbody td.item-obs .obs-inline-input { + width: 100%; + } /* Line 3 — status + actions */ #detalhes-itens-tbody td.item-status { text-align: left; } @@ -1008,6 +1026,16 @@ tbody td { margin-top: 0.25rem; } +#pag-itens-list label:hover { + background: var(--bg-secondary); + border-radius: 4px; +} +#pag-itens-list input[type="checkbox"] { + accent-color: var(--accent-primary); + width: 16px; + height: 16px; +} + @media (max-width: 480px) { h1 { font-size: 1.5rem; } diff --git a/web/src/views/checkout.ts b/web/src/views/checkout.ts index 75c6ff3..98aece8 100644 --- a/web/src/views/checkout.ts +++ b/web/src/views/checkout.ts @@ -1,14 +1,30 @@ import { api } from '../api'; -import { renderNavbar, toast, formatBRL } from '../utils'; +import { renderNavbar, formatBRL } from '../utils'; -const FORMAS_PAGAMENTO = [ - { value: 'DINHEIRO', label: '💵 Dinheiro' }, - { value: 'CARTAO_CREDITO', label: '💳 Cartão Crédito' }, - { value: 'CARTAO_DEBITO', label: '💳 Cartão Débito' }, - { value: 'PIX', label: '📱 PIX' }, -]; +const FORMAS_PAGAMENTO_LABEL: Record = { + DINHEIRO: '💵 Dinheiro', + CARTAO_CREDITO: '💳 Cartão Crédito', + CARTAO_DEBITO: '💳 Cartão Débito', + PIX: '📱 PIX', +}; + +function toLocalDateStr(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +function getDefaultDateRange(): { inicio: string; fim: string } { + const now = new Date(); + const fim = toLocalDateStr(now); + const inicio = toLocalDateStr(new Date(now.getTime() - 12 * 60 * 60 * 1000)); + return { inicio, fim }; +} export async function renderCheckout(container: HTMLElement) { + const { inicio, fim } = getDefaultDateRange(); + container.innerHTML = ` ${renderNavbar('/checkout')}
@@ -16,160 +32,111 @@ export async function renderCheckout(container: HTMLElement) {

Caixa / Checkout 💳

-
-

Buscar Comanda

-
- - +
+
+ +
+ + +
+
+ + +
+ +
-
+
+
+

Carregando pagamentos...

+
`; - document.getElementById('btn-buscar')?.addEventListener('click', searchComanda); - document.getElementById('search-input')?.addEventListener('keydown', (e) => { - if ((e as KeyboardEvent).key === 'Enter') searchComanda(); + document.getElementById('checkout-btn-filtrar')?.addEventListener('click', loadPagamentos); + document.getElementById('checkout-btn-limpar')?.addEventListener('click', () => { + const range = getDefaultDateRange(); + (document.getElementById('checkout-data-inicio') as HTMLInputElement).value = range.inicio; + (document.getElementById('checkout-data-fim') as HTMLInputElement).value = range.fim; + loadPagamentos(); }); + + await loadPagamentos(); } -function calcularTotalFinal(c: any): number { - let total = c.total || 0; - total -= (c.desconto || 0); - total += (c.acrescimo || 0); - if (c.taxaServico) total += total * 0.10; - return Math.max(total, 0); -} +async function loadPagamentos() { + const dataInicio = (document.getElementById('checkout-data-inicio') as HTMLInputElement).value; + const dataFim = (document.getElementById('checkout-data-fim') as HTMLInputElement).value; + const lista = document.getElementById('checkout-pagamentos-lista')!; + const totalEl = document.getElementById('checkout-pagamentos-total')!; -async function searchComanda() { - const query = (document.getElementById('search-input') as HTMLInputElement).value.trim(); - const result = document.getElementById('checkout-result')!; + lista.innerHTML = `

Carregando...

`; - if (!query) { toast('Digite um identificador para buscar', 'error'); return; } - - result.innerHTML = `

Buscando...

`; try { - const all = await api.get('/comandas'); - const found: any[] = all.filter((c: any) => - c.identificador.toLowerCase().includes(query.toLowerCase()) && c.status === 'ABERTA' - ); + const qs = new URLSearchParams(); + if (dataInicio) qs.set('dataInicio', dataInicio); + if (dataFim) qs.set('dataFim', dataFim); + const query = qs.toString(); + const pagamentos = await api.get(`/pagamentos${query ? '?' + query : ''}`); - if (!found.length) { - result.innerHTML = `

Nenhuma comanda ABERTA encontrada para "${query}".

`; + if (!pagamentos.length) { + lista.innerHTML = `

Nenhum pagamento registrado no período.

`; + totalEl.innerHTML = ''; return; } - result.innerHTML = found.map((c: any) => buildComandaCard(c)).join(''); - - document.querySelectorAll('.btn-pay').forEach(btn => { - btn.addEventListener('click', async () => { - const id = (btn as HTMLElement).dataset.id!; - const ident = (btn as HTMLElement).dataset.ident!; - const total = parseFloat((btn as HTMLElement).dataset.total!); - const forma = (document.getElementById(`pagamento-${id}`) as HTMLSelectElement)?.value; - - if (!forma) { - toast('Selecione a forma de pagamento', 'error'); - return; - } - - if (!confirm(`Confirmar pagamento de ${formatBRL(total)} para "${ident}" via ${forma.replace('_', ' ')}?`)) return; - try { - await api.post(`/comandas/${id}/pagar`, { formaPagamento: forma }); - toast(`Comanda "${ident}" fechada com sucesso!`, 'success'); - result.innerHTML = ` -
-

-

Pagamento Realizado!

-

Comanda "${ident}" foi fechada.

-
`; - } catch (err: any) { - toast(err.message, 'error'); - } - }); - }); - } catch (err: any) { - result.innerHTML = `

Erro: ${err.message}

`; - } -} - -function buildComandaCard(c: any): string { - const itens = c.itens ?? []; - const itensHtml = itens.length - ? itens.map((i: any) => ` - - ${i.produto?.nome ?? '—'} - ${i.quantidade} - ${formatBRL(i.precoUnitario)} - ${formatBRL(i.precoUnitario * i.quantidade)} - ${i.status} - - `).join('') - : `Nenhum item ainda`; - - const totalFinal = calcularTotalFinal(c); - const desconto = c.desconto || 0; - const acrescimo = c.acrescimo || 0; - - return ` -
-
+ + + `; await loadComandas({ status: 'ABERTA,FECHADA' }); bindOrderEvents(); + + const hash = window.location.hash.slice(1); + if (hash) { + await loadComandas({}); + setTimeout(() => { + (window as any).openDetails(hash); + window.history.replaceState({}, '', '/orders'); + }, 50); + } } function calcularTotalFinal(c: any): number { @@ -434,6 +496,71 @@ function bindOrderEvents() { }, 300); }); + // Modal Pagamento Parcial - close + document.getElementById('modal-pagamento-close')?.addEventListener('click', () => { + (document.getElementById('modal-pagamento-parcial') as HTMLElement).style.display = 'none'; + }); + + // Modal Pagamento Parcial - tab switching + document.querySelectorAll('[data-pagtab]').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('[data-pagtab]').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const tab = (btn as HTMLElement).dataset.pagtab; + (document.getElementById('pagtab-itens') as HTMLElement).style.display = tab === 'itens' ? 'block' : 'none'; + (document.getElementById('pagtab-fixo') as HTMLElement).style.display = tab === 'fixo' ? 'block' : 'none'; + }); + }); + + // Modal Pagamento Parcial - select all + document.getElementById('pag-select-all')?.addEventListener('change', (e) => { + const checked = (e.target as HTMLInputElement).checked; + document.querySelectorAll('.pag-item-check').forEach(cb => { + (cb as HTMLInputElement).checked = checked; + }); + updatePagamentoItensValor(); + }); + + // Modal Pagamento Parcial - confirm + document.getElementById('btn-confirmar-pagamento')?.addEventListener('click', async () => { + const comandaId = (document.getElementById('pagamento-comanda-id') as HTMLInputElement).value; + const forma = (document.getElementById('pag-forma') as HTMLSelectElement).value; + const obs = (document.getElementById('pag-obs') as HTMLInputElement).value; + const activeTab = document.querySelector('[data-pagtab].active') as HTMLElement; + const isItensTab = activeTab?.dataset.pagtab === 'itens'; + + let payload: any = { formaPagamento: forma }; + if (obs.trim()) payload.observacao = obs.trim(); + + if (isItensTab) { + const checked = document.querySelectorAll('.pag-item-check:checked'); + const itemIds = Array.from(checked).map(cb => (cb as HTMLInputElement).dataset.itemId); + if (itemIds.length === 0) { + toast('Selecione pelo menos um item!', 'error'); + return; + } + payload.itemIds = itemIds; + payload.valor = 0; + } else { + const valor = parseFloat((document.getElementById('pag-valor-fixo') as HTMLInputElement).value); + if (!valor || valor <= 0) { + toast('Informe um valor válido!', 'error'); + return; + } + payload.valor = valor; + } + + try { + await api.post(`/comandas/${comandaId}/pagamentos`, payload); + toast('Pagamento registrado!', 'success'); + (document.getElementById('modal-pagamento-parcial') as HTMLElement).style.display = 'none'; + await loadComandas({ status: 'ABERTA,FECHADA' }); + (window as any).openDetails(comandaId); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + (window as any).openAddItem = async (comandaId: string) => { (document.getElementById('current-comanda-id') as HTMLInputElement).value = comandaId; (document.getElementById('selected-produto-id') as HTMLInputElement).value = ''; @@ -494,7 +621,12 @@ function bindOrderEvents() { ${i.quantidade}x ${i.produto?.nome ?? '—'} ${i.quantidade} ${formatBRL(i.precoUnitario * i.quantidade)} - ${i.observacao ?? ''} + + ${canDelete + ? `${i.observacao ?? 'adicionar obs'}` + : (i.observacao ?? '') + } + ${i.status} ${canDelete ? ` @@ -522,6 +654,49 @@ function bindOrderEvents() { } }); }); + + // Inline edit observacao + tbody.querySelectorAll('.item-obs-editable').forEach(span => { + span.addEventListener('click', () => { + const itemId = (span as HTMLElement).dataset.itemId!; + const currentText = span.textContent ?? ''; + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'obs-inline-input'; + input.value = currentText; + input.style.cssText = 'width:100%;padding:0.25rem 0.4rem;border:1px solid var(--accent-primary);border-radius:4px;font-size:0.8rem;background:var(--bg-primary);color:var(--text-primary);'; + + const save = async () => { + const novaObs = input.value.trim(); + if (novaObs === currentText) { + span.textContent = currentText || ''; + if (!currentText) span.innerHTML = 'adicionar obs'; + return; + } + try { + await api.patch(`/comandas/itens/${itemId}/observacao`, { observacao: novaObs || undefined }); + span.textContent = novaObs; + if (!novaObs) span.innerHTML = 'adicionar obs'; + toast('Observação atualizada!', 'success'); + await loadComandas({ status: 'ABERTA,FECHADA' }); + } catch (err: any) { + toast(err.message, 'error'); + span.textContent = currentText; + } + }; + + input.addEventListener('blur', save); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); input.blur(); } + if (e.key === 'Escape') { span.textContent = currentText; if (!currentText) span.innerHTML = 'adicionar obs'; } + }); + + span.textContent = ''; + span.appendChild(input); + input.focus(); + input.select(); + }); + }); } // Resumo financeiro @@ -531,6 +706,9 @@ function bindOrderEvents() { const acrescimo = c.acrescimo || 0; const taxaServico = c.taxaServico ? (subtotal - desconto + acrescimo) * 0.10 : 0; const totalFinal = Math.max(subtotal - desconto + acrescimo + taxaServico, 0); + const valorPago = c.valorPago || 0; + const restante = Math.max(totalFinal - valorPago, 0); + const pagamentos = c.pagamentos ?? []; resumoEl.innerHTML = `
@@ -547,39 +725,57 @@ function bindOrderEvents() { ${c.taxaServico ? `
Taxa Serviço (10%):+${formatBRL(taxaServico)}
` : ''} - ${c.formaPagamento ? `
- Pagamento: - ${c.formaPagamento.replace('_', ' ')} + ${valorPago > 0 ? `
+ Já pago:-${formatBRL(valorPago)} +
` : ''} + ${restante > 0 && valorPago > 0 ? `
+ Restante:${formatBRL(restante)}
` : ''}
+ ${pagamentos.length > 0 ? ` +
+
Histórico de Pagamentos
+ ${pagamentos.map((p: any) => ` +
+
+ ${new Date(p.criadoEm).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} + ${p.formaPagamento.replace('_', ' ')} + ${p.observacao ? `— ${p.observacao}` : ''} +
+ ${formatBRL(p.valor)} +
+ `).join('')} +
+ ` : ''} `; - document.getElementById('detalhes-total')!.textContent = formatBRL(totalFinal); + document.getElementById('detalhes-total')!.textContent = formatBRL(restante > 0 ? restante : totalFinal); const acoesContainer = document.getElementById('detalhes-acoes-pagamento')!; acoesContainer.innerHTML = ''; const canPay = user && ['ADMIN', 'CAIXA'].includes(user.role); - if (canPay && c.status !== 'PAGA' && c.status !== 'CANCELADA') { + if (canPay && c.status !== 'PAGA' && c.status !== 'CANCELADA' && restante > 0) { acoesContainer.innerHTML = ` -
-
- - -
- -
+ + `; - document.getElementById('btn-detalhes-receber')?.addEventListener('click', async () => { - const forma = (document.getElementById('pagamento-forma') as HTMLSelectElement).value; - if (!confirm(`Confirmar recebimento de ${formatBRL(totalFinal)} para "${c.identificador}"?`)) return; + document.getElementById('btn-pagamento-parcial')?.addEventListener('click', () => { + openPagamentoModal(c); + }); + + document.getElementById('btn-pagamento-total')?.addEventListener('click', async () => { + const forma = prompt('Forma de pagamento:\n1 - Dinheiro\n2 - Cartão Crédito\n3 - Cartão Débito\n4 - PIX\n\nDigite o número:'); + const formaMap: Record = { '1': 'DINHEIRO', '2': 'CARTAO_CREDITO', '3': 'CARTAO_DEBITO', '4': 'PIX' }; + const formaPagamento = formaMap[forma ?? ''] ?? 'DINHEIRO'; + if (!confirm(`Confirmar pagamento de ${formatBRL(restante)} (restante) para "${c.identificador}"?`)) return; try { - await api.post(`/comandas/${c.id}/pagar`, { formaPagamento: forma }); + await api.post(`/comandas/${c.id}/pagar`, { formaPagamento }); toast('Pagamento registrado!', 'success'); (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'none'; await loadComandas({ status: 'ABERTA,FECHADA' }); @@ -587,9 +783,66 @@ function bindOrderEvents() { toast(err.message, 'error'); } }); + } else if (canPay && c.status === 'PAGA') { + acoesContainer.innerHTML = `

✅ Comanda Paga

`; } (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'flex'; document.querySelector('.detalhes-edit-section')?.classList.remove('expanded'); }; + + function openPagamentoModal(c: any) { + const totalFinal = calcularTotalFinal(c); + const restante = Math.max(totalFinal - (c.valorPago || 0), 0); + + (document.getElementById('pagamento-comanda-id') as HTMLInputElement).value = c.id; + (document.getElementById('pagamento-restante-info') as HTMLElement).innerHTML = + `
Restante a pagar:${formatBRL(restante)}
`; + + // Reset tabs + document.querySelectorAll('[data-pagtab]').forEach(b => b.classList.remove('active')); + document.querySelector('[data-pagtab="itens"]')?.classList.add('active'); + (document.getElementById('pagtab-itens') as HTMLElement).style.display = 'block'; + (document.getElementById('pagtab-fixo') as HTMLElement).style.display = 'none'; + (document.getElementById('pag-valor-fixo') as HTMLInputElement).value = ''; + (document.getElementById('pag-obs') as HTMLInputElement).value = ''; + (document.getElementById('pag-select-all') as HTMLInputElement).checked = false; + + // Render items + const itensList = document.getElementById('pag-itens-list')!; + const itens = c.itens ?? []; + itensList.innerHTML = itens.map((i: any) => { + const subtotal = i.precoUnitario * i.quantidade; + return ` + + `; + }).join(''); + + // Bind item checkboxes + itensList.querySelectorAll('.pag-item-check').forEach(cb => { + cb.addEventListener('change', () => { + updatePagamentoItensValor(); + const totalChecks = itensList.querySelectorAll('.pag-item-check').length; + const checkedChecks = itensList.querySelectorAll('.pag-item-check:checked').length; + (document.getElementById('pag-select-all') as HTMLInputElement).checked = totalChecks === checkedChecks; + }); + }); + + updatePagamentoItensValor(); + + (document.getElementById('modal-pagamento-parcial') as HTMLElement).style.display = 'flex'; + } + + function updatePagamentoItensValor() { + const checked = document.querySelectorAll('.pag-item-check:checked'); + let total = 0; + checked.forEach(cb => { + total += parseFloat((cb as HTMLElement).dataset.itemValor || '0'); + }); + (document.getElementById('pag-itens-valor')!.textContent) = formatBRL(total); + } }