feat: estoque negativo, UTC-3, dd/mm/yyyy, estorno de pagamento
This commit is contained in:
15
AGENTS.md
15
AGENTS.md
@@ -34,7 +34,8 @@ app_ces/
|
|||||||
├── src/ # Backend API
|
├── src/ # Backend API
|
||||||
│ ├── server.ts # Entrypoint Fastify, porta 3333
|
│ ├── server.ts # Entrypoint Fastify, porta 3333
|
||||||
│ ├── lib/
|
│ ├── lib/
|
||||||
│ │ └── prisma.ts # Instância PrismaClient
|
│ │ ├── prisma.ts # Instância PrismaClient
|
||||||
|
│ │ └── timezone.ts # Helpers UTC-3 (dateStart, dateEnd, dateTime, nowUTC3)
|
||||||
│ ├── middlewares/
|
│ ├── middlewares/
|
||||||
│ │ └── auth.middleware.ts # authenticate + requireRoles
|
│ │ └── auth.middleware.ts # authenticate + requireRoles
|
||||||
│ ├── controllers/
|
│ ├── controllers/
|
||||||
@@ -70,7 +71,7 @@ app_ces/
|
|||||||
└── src/
|
└── src/
|
||||||
├── main.ts # Router SPA + navegação
|
├── main.ts # Router SPA + navegação
|
||||||
├── api.ts # Cliente HTTP centralizado
|
├── api.ts # Cliente HTTP centralizado
|
||||||
├── utils.ts # renderNavbar, toast, helpers
|
├── utils.ts # renderNavbar, toast, helpers, toDisplayDate, parseDisplayDate
|
||||||
├── style.css # Dark theme + glassmorphism
|
├── style.css # Dark theme + glassmorphism
|
||||||
└── views/
|
└── views/
|
||||||
├── login.ts
|
├── login.ts
|
||||||
@@ -107,7 +108,7 @@ app_ces/
|
|||||||
- Relations: `produto` (N:1 Produto), `comanda` (N:1 Comanda), `usuario` (N:1 Usuario)
|
- Relations: `produto` (N:1 Produto), `comanda` (N:1 Comanda), `usuario` (N:1 Usuario)
|
||||||
|
|
||||||
**Pagamento**
|
**Pagamento**
|
||||||
- `id` (UUID), `comandaId` (FK), `valor`, `formaPagamento` (DINHEIRO|CARTAO_CREDITO|CARTAO_DEBITO|PIX), `observacao?`, `criadoEm`
|
- `id` (UUID), `comandaId` (FK), `valor`, `formaPagamento` (DINHEIRO|CARTAO_CREDITO|CARTAO_DEBITO|PIX), `observacao?`, `estornado` (boolean, default false), `criadoEm`
|
||||||
|
|
||||||
## Rotas da API
|
## Rotas da API
|
||||||
|
|
||||||
@@ -148,6 +149,7 @@ app_ces/
|
|||||||
| PATCH | `/comandas/:id` | GARCOM/CAIXA/ADMIN |
|
| PATCH | `/comandas/:id` | GARCOM/CAIXA/ADMIN |
|
||||||
| POST | `/comandas/:id/pagar` | CAIXA/ADMIN |
|
| POST | `/comandas/:id/pagar` | CAIXA/ADMIN |
|
||||||
| POST | `/comandas/:id/pagamentos` | CAIXA/ADMIN |
|
| POST | `/comandas/:id/pagamentos` | CAIXA/ADMIN |
|
||||||
|
| PATCH | `/pagamentos/:id/estornar` | CAIXA/ADMIN |
|
||||||
| PATCH | `/comandas/itens/:id/observacao` | GARCOM/CAIXA/ADMIN |
|
| PATCH | `/comandas/itens/:id/observacao` | GARCOM/CAIXA/ADMIN |
|
||||||
| DELETE | `/comandas/itens/:id` | CAIXA/ADMIN |
|
| DELETE | `/comandas/itens/:id` | CAIXA/ADMIN |
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ app_ces/
|
|||||||
### Fluxo de Comanda
|
### Fluxo de Comanda
|
||||||
1. Criar comanda (identificador = mesa/código) - **registra usuário logado**
|
1. Criar comanda (identificador = mesa/código) - **registra usuário logado**
|
||||||
2. Adicionar itens (baixa estoque automaticamente, atualiza total) - **registra usuário logado**
|
2. Adicionar itens (baixa estoque automaticamente, atualiza total) - **registra usuário logado**
|
||||||
3. Itens aparecem no painel de cozinha/bar com status `PENDENTE`
|
3. Itens aparecem no painel de cozinha/bar com status `PENDENTE` (apenas últimos 12h)
|
||||||
4. Cozinha/bar avança: `PENDENTE → PREPARANDO → PRONTO → ENTREGUE`
|
4. Cozinha/bar avança: `PENDENTE → PREPARANDO → PRONTO → ENTREGUE`
|
||||||
5. Caixa fecha com pagamento (integral ou parcial)
|
5. Caixa fecha com pagamento (integral ou parcial)
|
||||||
6. Status da comanda: `ABERTA → PAGA`
|
6. Status da comanda: `ABERTA → PAGA`
|
||||||
@@ -197,17 +199,19 @@ app_ces/
|
|||||||
- **Parcial**: `POST /comandas/:id/pagamentos` - registra pagamento parcial, pode selecionar itens específicos
|
- **Parcial**: `POST /comandas/:id/pagamentos` - registra pagamento parcial, pode selecionar itens específicos
|
||||||
- **Cálculo do total final**: `total - desconto + acrescimo + (taxaServico ? 10% : 0)`
|
- **Cálculo do total final**: `total - desconto + acrescimo + (taxaServico ? 10% : 0)`
|
||||||
- Formas: `DINHEIRO`, `CARTAO_CREDITO`, `CARTAO_DEBITO`, `PIX`
|
- Formas: `DINHEIRO`, `CARTAO_CREDITO`, `CARTAO_DEBITO`, `PIX`
|
||||||
|
- **Estorno**: `PATCH /pagamentos/:id/estornar` - marca pagamento como estornado, subtrai valor de `comanda.valorPago`, se comanda era `PAGA` volta para `FECHADA`. Pagamento continua visível com indicador visual (opacity + line-through + badge "ESTORNADO")
|
||||||
|
|
||||||
### Estoque
|
### Estoque
|
||||||
- Decrementa ao adicionar item na comanda
|
- Decrementa ao adicionar item na comanda
|
||||||
- Incrementa ao remover item da comanda
|
- Incrementa ao remover item da comanda
|
||||||
- Validação de estoque insuficiente no backend
|
- **Permite estoque negativo** (sem validação de insuficiência)
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- Rotas SPA manuais via `window.history.pushState`
|
- Rotas SPA manuais via `window.history.pushState`
|
||||||
- Auto-refresh no painel de cozinha a cada 10 segundos
|
- Auto-refresh no painel de cozinha a cada 10 segundos
|
||||||
- Notificações sonoras via Web Audio API para novos pedidos
|
- Notificações sonoras via Web Audio API para novos pedidos
|
||||||
- Navbar dinâmica baseada no role do usuário logado
|
- Navbar dinâmica baseada no role do usuário logado
|
||||||
|
- Filtros de data em formato `dd/mm/yyyy` (inputs `type="text"`, conversão via `parseDisplayDate`)
|
||||||
|
|
||||||
## Comandos Úteis
|
## Comandos Úteis
|
||||||
|
|
||||||
@@ -232,6 +236,7 @@ cd web && npm run build # Build de produção
|
|||||||
- CORS habilitado para qualquer origem (`origin: '*'`)
|
- CORS habilitado para qualquer origem (`origin: '*'`)
|
||||||
- JWT secret definido em `.env` (fallback: `fallback_secret_change_me`)
|
- JWT secret definido em `.env` (fallback: `fallback_secret_change_me`)
|
||||||
- Error handler global trata `ZodError` (400) e erros HTTP
|
- Error handler global trata `ZodError` (400) e erros HTTP
|
||||||
|
- **Fuso horário UTC-3** configurado em `src/lib/timezone.ts` - todas as datas no backend e frontend usam offset UTC-3
|
||||||
- Frontend não usa framework UI - renderiza HTML via template literals
|
- Frontend não usa framework UI - renderiza HTML via template literals
|
||||||
- Utiliza `event delegation` para botões dinâmicos no painel
|
- Utiliza `event delegation` para botões dinâmicos no painel
|
||||||
- Cleanup de listeners ao navegar entre views
|
- Cleanup de listeners ao navegar entre views
|
||||||
|
|||||||
@@ -83,5 +83,6 @@ model Pagamento {
|
|||||||
valor Float
|
valor Float
|
||||||
formaPagamento String // DINHEIRO, CARTAO_CREDITO, CARTAO_DEBITO, PIX
|
formaPagamento String // DINHEIRO, CARTAO_CREDITO, CARTAO_DEBITO, PIX
|
||||||
observacao String?
|
observacao String?
|
||||||
|
estornado Boolean @default(false)
|
||||||
criadoEm DateTime @default(now())
|
criadoEm DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,3 +133,13 @@ export async function deleteItemFromOrderController(request: FastifyRequest, rep
|
|||||||
return reply.status(400).send({ error: error.message });
|
return reply.status(400).send({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function reversePaymentController(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||||
|
try {
|
||||||
|
const pagamento = await orderService.reversePayment(id);
|
||||||
|
return reply.send(pagamento);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(400).send({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import * as reportService from '../services/report.service';
|
import * as reportService from '../services/report.service';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { dateTime, nowUTC3, todayStrUTC3 } from '../lib/timezone';
|
||||||
|
|
||||||
export async function salesReportController(request: FastifyRequest, reply: FastifyReply) {
|
export async function salesReportController(request: FastifyRequest, reply: FastifyReply) {
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
inicio: z.string().optional(),
|
inicio: z.string().optional(),
|
||||||
fim: z.string().optional(),
|
fim: z.string().optional(),
|
||||||
|
horaInicio: z.string().optional(),
|
||||||
|
horaFim: z.string().optional(),
|
||||||
});
|
});
|
||||||
const query = querySchema.parse(request.query);
|
const query = querySchema.parse(request.query);
|
||||||
|
|
||||||
const inicio = query.inicio ? new Date(query.inicio) : new Date(new Date().setDate(new Date().getDate() - 30));
|
const today = todayStrUTC3();
|
||||||
const fim = query.fim ? new Date(query.fim + 'T23:59:59') : new Date();
|
const inicioStr = query.inicio || today;
|
||||||
|
const horaInicio = query.horaInicio || '00:00';
|
||||||
|
const inicio = dateTime(inicioStr, horaInicio);
|
||||||
|
|
||||||
|
const fimStr = query.fim || today;
|
||||||
|
const horaFim = query.horaFim || '23:59';
|
||||||
|
const fim = dateTime(fimStr, horaFim, 59);
|
||||||
|
|
||||||
const report = await reportService.getSalesReport(inicio, fim);
|
const report = await reportService.getSalesReport(inicio, fim);
|
||||||
return reply.send(report);
|
return reply.send(report);
|
||||||
|
|||||||
62
src/lib/timezone.ts
Normal file
62
src/lib/timezone.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const OFFSET_HOURS = -3;
|
||||||
|
|
||||||
|
/** Componentes de data/hora em UTC-3 */
|
||||||
|
export interface UTC3 {
|
||||||
|
year: number;
|
||||||
|
month: number; // 0-indexed
|
||||||
|
date: number;
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retorna componentes de agora em UTC-3 */
|
||||||
|
export function nowUTC3(): UTC3 {
|
||||||
|
const d = new Date(Date.now() + OFFSET_HOURS * 60 * 60 * 1000);
|
||||||
|
return {
|
||||||
|
year: d.getUTCFullYear(),
|
||||||
|
month: d.getUTCMonth(),
|
||||||
|
date: d.getUTCDate(),
|
||||||
|
hours: d.getUTCHours(),
|
||||||
|
minutes: d.getUTCMinutes(),
|
||||||
|
seconds: d.getUTCSeconds(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cria Date cuja representação UTC = meia-noite UTC-3 do dateStr (YYYY-MM-DD) */
|
||||||
|
export function dateStart(dateStr: string): Date {
|
||||||
|
const [y, m, d] = dateStr.split('-').map(Number);
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, 0 - OFFSET_HOURS, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cria Date cuja representação UTC = 23:59:59.999 UTC-3 do dateStr */
|
||||||
|
export function dateEnd(dateStr: string): Date {
|
||||||
|
const [y, m, d] = dateStr.split('-').map(Number);
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, 23 - OFFSET_HOURS, 59, 59, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Date + hora em UTC-3. dateStr = YYYY-MM-DD, timeStr = HH:mm */
|
||||||
|
export function dateTime(dateStr: string, timeStr: string, seconds = 0): Date {
|
||||||
|
const [y, m, d] = dateStr.split('-').map(Number);
|
||||||
|
const [hh, mm] = timeStr.split(':').map(Number);
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, hh - OFFSET_HOURS, mm, seconds, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converte Date (UTC no banco) para UTC-3 para exibição */
|
||||||
|
export function toUTC3(d: Date): Date {
|
||||||
|
return new Date(d.getTime() + OFFSET_HOURS * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYY-MM-DD em UTC-3 */
|
||||||
|
export function todayStrUTC3(): string {
|
||||||
|
const c = nowUTC3();
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${c.year}-${pad(c.month + 1)}-${pad(c.date)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYY-MM-DD do primeiro dia do mês atual em UTC-3 */
|
||||||
|
export function firstDayOfMonthUTC3(): string {
|
||||||
|
const c = nowUTC3();
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${c.year}-${pad(c.month + 1)}-01`;
|
||||||
|
}
|
||||||
@@ -27,4 +27,7 @@ export async function orderRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// Excluir item de comanda (apenas CAIXA e ADMIN)
|
// Excluir item de comanda (apenas CAIXA e ADMIN)
|
||||||
app.delete('/comandas/itens/:id', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.deleteItemFromOrderController);
|
app.delete('/comandas/itens/:id', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.deleteItemFromOrderController);
|
||||||
|
|
||||||
|
// Estornar pagamento (Caixa, Admin)
|
||||||
|
app.patch('/pagamentos/:id/estornar', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.reversePaymentController);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export const productSchema = z.object({
|
|||||||
fotoUrl: z.string().url().optional(),
|
fotoUrl: z.string().url().optional(),
|
||||||
preco: z.number().positive(),
|
preco: z.number().positive(),
|
||||||
custo: z.number().positive().optional(),
|
custo: z.number().positive().optional(),
|
||||||
estoqueAtual: z.number().int().nonnegative().default(0),
|
estoqueAtual: z.number().int().default(0),
|
||||||
estoqueMinimo: z.number().int().nonnegative().default(0),
|
estoqueMinimo: z.number().int().default(0),
|
||||||
itemCozinha: z.boolean(),
|
itemCozinha: z.boolean(),
|
||||||
ativo: z.boolean().default(true),
|
ativo: z.boolean().default(true),
|
||||||
categoriaId: z.string().uuid(),
|
categoriaId: z.string().uuid(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import { prisma } from '../lib/prisma';
|
||||||
import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateItemObservacaoInput, UpdateOrderInput, PayOrderInput, PartialPaymentInput } from '../schemas/order.schema';
|
import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateItemObservacaoInput, UpdateOrderInput, PayOrderInput, PartialPaymentInput } from '../schemas/order.schema';
|
||||||
|
import { dateStart, dateEnd, nowUTC3 } from '../lib/timezone';
|
||||||
|
|
||||||
export interface ListOrdersFilter {
|
export interface ListOrdersFilter {
|
||||||
statuses?: string[];
|
statuses?: string[];
|
||||||
@@ -17,14 +18,8 @@ export async function listPayments(filter?: ListPaymentsFilter) {
|
|||||||
|
|
||||||
if (filter?.dataInicio || filter?.dataFim) {
|
if (filter?.dataInicio || filter?.dataFim) {
|
||||||
where.criadoEm = {};
|
where.criadoEm = {};
|
||||||
if (filter.dataInicio) {
|
if (filter.dataInicio) where.criadoEm.gte = dateStart(filter.dataInicio);
|
||||||
const [y, m, d] = filter.dataInicio.split('-').map(Number);
|
if (filter.dataFim) where.criadoEm.lte = dateEnd(filter.dataFim);
|
||||||
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({
|
return prisma.pagamento.findMany({
|
||||||
@@ -52,12 +47,8 @@ export async function listOrders(filter?: ListOrdersFilter) {
|
|||||||
|
|
||||||
if (filter?.dataInicio || filter?.dataFim) {
|
if (filter?.dataInicio || filter?.dataFim) {
|
||||||
where.criadoEm = {};
|
where.criadoEm = {};
|
||||||
if (filter.dataInicio) where.criadoEm.gte = new Date(filter.dataInicio);
|
if (filter.dataInicio) where.criadoEm.gte = dateStart(filter.dataInicio);
|
||||||
if (filter.dataFim) {
|
if (filter.dataFim) where.criadoEm.lte = dateEnd(filter.dataFim);
|
||||||
const end = new Date(filter.dataFim);
|
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
where.criadoEm.lte = end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return prisma.comanda.findMany({
|
return prisma.comanda.findMany({
|
||||||
@@ -101,10 +92,6 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput,
|
|||||||
throw new Error('Produto não encontrado');
|
throw new Error('Produto não encontrado');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (produto.estoqueAtual < data.quantidade) {
|
|
||||||
throw new Error(`Estoque insuficiente. Disponível: ${produto.estoqueAtual}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const precoTotalItem = produto.preco * data.quantidade;
|
const precoTotalItem = produto.preco * data.quantidade;
|
||||||
|
|
||||||
const [, itemPedido] = await prisma.$transaction([
|
const [, itemPedido] = await prisma.$transaction([
|
||||||
@@ -134,11 +121,15 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput,
|
|||||||
|
|
||||||
export async function listKitchenBarItems(itemCozinha?: boolean, status?: string) {
|
export async function listKitchenBarItems(itemCozinha?: boolean, status?: string) {
|
||||||
const filter: any = {};
|
const filter: any = {};
|
||||||
|
|
||||||
|
const now = nowUTC3();
|
||||||
|
const dozeHorasAtrasMs = Date.UTC(now.year, now.month, now.date, now.hours - 12, now.minutes, now.seconds, 0);
|
||||||
|
filter.criadoEm = { gte: new Date(dozeHorasAtrasMs) };
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
filter.status = status;
|
filter.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof itemCozinha === 'boolean') {
|
if (typeof itemCozinha === 'boolean') {
|
||||||
filter.produto = {
|
filter.produto = {
|
||||||
itemCozinha,
|
itemCozinha,
|
||||||
@@ -396,3 +387,34 @@ export async function deleteItemFromOrder(itemId: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function reversePayment(pagamentoId: string) {
|
||||||
|
const pagamento = await prisma.pagamento.findUnique({
|
||||||
|
where: { id: pagamentoId },
|
||||||
|
include: { comanda: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pagamento) throw new Error('Pagamento não encontrado');
|
||||||
|
if (pagamento.estornado) throw new Error('Pagamento já foi estornado');
|
||||||
|
|
||||||
|
const novoValorPago = Math.max(pagamento.comanda.valorPago - pagamento.valor, 0);
|
||||||
|
|
||||||
|
const statusComanda = pagamento.comanda.status === 'PAGA' ? 'FECHADA' : pagamento.comanda.status;
|
||||||
|
|
||||||
|
const [, pagamentoEstornado] = await prisma.$transaction([
|
||||||
|
prisma.pagamento.update({
|
||||||
|
where: { id: pagamentoId },
|
||||||
|
data: { estornado: true },
|
||||||
|
}),
|
||||||
|
prisma.comanda.update({
|
||||||
|
where: { id: pagamento.comandaId },
|
||||||
|
data: {
|
||||||
|
valorPago: novoValorPago,
|
||||||
|
status: statusComanda,
|
||||||
|
...(pagamento.comanda.status === 'PAGA' ? { fechadoEm: null, formaPagamento: null } : {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return pagamentoEstornado;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import { prisma } from '../lib/prisma';
|
||||||
|
import { nowUTC3, toUTC3 } from '../lib/timezone';
|
||||||
|
|
||||||
export async function getSalesReport(inicio: Date, fim: Date) {
|
export async function getSalesReport(inicio: Date, fim: Date) {
|
||||||
const comandas = await prisma.comanda.findMany({
|
const comandas = await prisma.comanda.findMany({
|
||||||
@@ -39,7 +40,7 @@ export async function getSalesReport(inicio: Date, fim: Date) {
|
|||||||
// Vendas por dia
|
// Vendas por dia
|
||||||
const vendasPorDia: Record<string, { vendas: number; comandas: number }> = {};
|
const vendasPorDia: Record<string, { vendas: number; comandas: number }> = {};
|
||||||
for (const c of comandas) {
|
for (const c of comandas) {
|
||||||
const dia = c.fechadoEm!.toISOString().split('T')[0];
|
const dia = toUTC3(c.fechadoEm!).toISOString().split('T')[0];
|
||||||
if (!vendasPorDia[dia]) vendasPorDia[dia] = { vendas: 0, comandas: 0 };
|
if (!vendasPorDia[dia]) vendasPorDia[dia] = { vendas: 0, comandas: 0 };
|
||||||
vendasPorDia[dia].vendas += c.total;
|
vendasPorDia[dia].vendas += c.total;
|
||||||
vendasPorDia[dia].comandas += 1;
|
vendasPorDia[dia].comandas += 1;
|
||||||
@@ -80,12 +81,11 @@ export async function getSalesReport(inicio: Date, fim: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDashboardData() {
|
export async function getDashboardData() {
|
||||||
const now = new Date();
|
const now = nowUTC3();
|
||||||
const inicioDia = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
const inicioDia = new Date(Date.UTC(now.year, now.month, now.date, 0, 0, 0, 0));
|
||||||
const inicioSemana = new Date(now);
|
const diaSemana = new Date(Date.UTC(now.year, now.month, now.date)).getUTCDay();
|
||||||
inicioSemana.setDate(now.getDate() - now.getDay());
|
const inicioSemana = new Date(Date.UTC(now.year, now.month, now.date - diaSemana, 0, 0, 0, 0));
|
||||||
inicioSemana.setHours(0, 0, 0, 0);
|
const inicioMes = new Date(Date.UTC(now.year, now.month, 1, 0, 0, 0, 0));
|
||||||
const inicioMes = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
||||||
|
|
||||||
const [comandasHoje, comandasSemana, comandasMes, totalProdutos, produtosEstoqueBaixo, comandasAbertas] = await Promise.all([
|
const [comandasHoje, comandasSemana, comandasMes, totalProdutos, produtosEstoqueBaixo, comandasAbertas] = await Promise.all([
|
||||||
prisma.comanda.findMany({
|
prisma.comanda.findMany({
|
||||||
|
|||||||
@@ -21,6 +21,28 @@ export function getUser(): { id: string; nome: string; email: string; role: stri
|
|||||||
return raw ? JSON.parse(raw) : null;
|
return raw ? JSON.parse(raw) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retorna YYYY-MM-DD de hoje em UTC-3 */
|
||||||
|
export function todayStr(): string {
|
||||||
|
const d = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
||||||
|
const y = d.getUTCFullYear();
|
||||||
|
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formata Date → dd/mm/yyyy para exibição */
|
||||||
|
export function toDisplayDate(iso: string): string {
|
||||||
|
const [y, m, d] = iso.split('-');
|
||||||
|
return `${d}/${m}/${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converte dd/mm/yyyy → YYYY-MM-DD para a API */
|
||||||
|
export function parseDisplayDate(display: string): string {
|
||||||
|
const [d, m, y] = display.split('/');
|
||||||
|
if (!d || !m || !y) return '';
|
||||||
|
return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Formata valor em R$ */
|
/** Formata valor em R$ */
|
||||||
export function formatBRL(value: number): string {
|
export function formatBRL(value: number): string {
|
||||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { renderNavbar, formatBRL } from '../utils';
|
import { renderNavbar, formatBRL, toDisplayDate, parseDisplayDate, todayStr, getUser, toast } from '../utils';
|
||||||
|
|
||||||
const FORMAS_PAGAMENTO_LABEL: Record<string, string> = {
|
const FORMAS_PAGAMENTO_LABEL: Record<string, string> = {
|
||||||
DINHEIRO: '💵 Dinheiro',
|
DINHEIRO: '💵 Dinheiro',
|
||||||
@@ -8,18 +8,12 @@ const FORMAS_PAGAMENTO_LABEL: Record<string, string> = {
|
|||||||
PIX: '📱 PIX',
|
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 } {
|
function getDefaultDateRange(): { inicio: string; fim: string } {
|
||||||
const now = new Date();
|
const hoje = todayStr();
|
||||||
const fim = toLocalDateStr(now);
|
const ontemMs = new Date(hoje + 'T12:00:00Z').getTime() - 12 * 60 * 60 * 1000;
|
||||||
const inicio = toLocalDateStr(new Date(now.getTime() - 12 * 60 * 60 * 1000));
|
const ontem = new Date(ontemMs);
|
||||||
return { inicio, fim };
|
const ontemStr = `${ontem.getUTCFullYear()}-${String(ontem.getUTCMonth() + 1).padStart(2, '0')}-${String(ontem.getUTCDate()).padStart(2, '0')}`;
|
||||||
|
return { inicio: toDisplayDate(ontemStr), fim: toDisplayDate(hoje) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderCheckout(container: HTMLElement) {
|
export async function renderCheckout(container: HTMLElement) {
|
||||||
@@ -37,11 +31,11 @@ export async function renderCheckout(container: HTMLElement) {
|
|||||||
<label style="color:var(--text-secondary);font-size:0.875rem;font-weight:600;">Período:</label>
|
<label style="color:var(--text-secondary);font-size:0.875rem;font-weight:600;">Período:</label>
|
||||||
<div style="display:flex;align-items:center;gap:0.4rem;">
|
<div style="display:flex;align-items:center;gap:0.4rem;">
|
||||||
<label style="font-size:0.8rem;color:var(--text-secondary);">De</label>
|
<label style="font-size:0.8rem;color:var(--text-secondary);">De</label>
|
||||||
<input type="date" id="checkout-data-inicio" class="form-control" value="${inicio}" style="font-size:0.85rem;">
|
<input type="text" id="checkout-data-inicio" class="form-control" value="${inicio}" style="font-size:0.85rem;" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:0.4rem;">
|
<div style="display:flex;align-items:center;gap:0.4rem;">
|
||||||
<label style="font-size:0.8rem;color:var(--text-secondary);">Até</label>
|
<label style="font-size:0.8rem;color:var(--text-secondary);">Até</label>
|
||||||
<input type="date" id="checkout-data-fim" class="form-control" value="${fim}" style="font-size:0.85rem;">
|
<input type="text" id="checkout-data-fim" class="form-control" value="${fim}" style="font-size:0.85rem;" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm" id="checkout-btn-filtrar">Filtrar</button>
|
<button class="btn btn-sm" id="checkout-btn-filtrar">Filtrar</button>
|
||||||
<button class="btn btn-sm btn-secondary" id="checkout-btn-limpar" style="color:var(--text-secondary);">Últimas 12h</button>
|
<button class="btn btn-sm btn-secondary" id="checkout-btn-limpar" style="color:var(--text-secondary);">Últimas 12h</button>
|
||||||
@@ -67,8 +61,8 @@ export async function renderCheckout(container: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPagamentos() {
|
async function loadPagamentos() {
|
||||||
const dataInicio = (document.getElementById('checkout-data-inicio') as HTMLInputElement).value;
|
const dataInicio = parseDisplayDate((document.getElementById('checkout-data-inicio') as HTMLInputElement).value);
|
||||||
const dataFim = (document.getElementById('checkout-data-fim') as HTMLInputElement).value;
|
const dataFim = parseDisplayDate((document.getElementById('checkout-data-fim') as HTMLInputElement).value);
|
||||||
const lista = document.getElementById('checkout-pagamentos-lista')!;
|
const lista = document.getElementById('checkout-pagamentos-lista')!;
|
||||||
const totalEl = document.getElementById('checkout-pagamentos-total')!;
|
const totalEl = document.getElementById('checkout-pagamentos-total')!;
|
||||||
|
|
||||||
@@ -95,6 +89,9 @@ async function loadPagamentos() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const user = getUser();
|
||||||
|
const canReverse = user?.role === 'ADMIN' || user?.role === 'CAIXA';
|
||||||
|
|
||||||
lista.innerHTML = `
|
lista.innerHTML = `
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
@@ -106,11 +103,18 @@ async function loadPagamentos() {
|
|||||||
<th>Forma de Pagamento</th>
|
<th>Forma de Pagamento</th>
|
||||||
<th style="text-align:right;">Valor</th>
|
<th style="text-align:right;">Valor</th>
|
||||||
<th>Obs</th>
|
<th>Obs</th>
|
||||||
|
${canReverse ? '<th style="text-align:center;">Ação</th>' : ''}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${pagamentos.map((p: any) => `
|
${pagamentos.map((p: any) => {
|
||||||
<tr>
|
const estornado = p.estornado;
|
||||||
|
const rowStyle = estornado ? 'opacity:0.5;text-decoration:line-through;' : '';
|
||||||
|
const valorStyle = estornado
|
||||||
|
? 'text-align:right;font-weight:700;color:var(--accent-primary);'
|
||||||
|
: 'text-align:right;font-weight:700;color:var(--success);';
|
||||||
|
return `
|
||||||
|
<tr style="${rowStyle}">
|
||||||
<td data-label="Data" style="white-space:nowrap;">${new Date(p.criadoEm).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })}</td>
|
<td data-label="Data" style="white-space:nowrap;">${new Date(p.criadoEm).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })}</td>
|
||||||
<td data-label="Comanda">
|
<td data-label="Comanda">
|
||||||
<a href="#" class="checkout-link-comanda" data-comanda-id="${p.comanda.id}" style="color:var(--accent-primary);text-decoration:none;font-weight:600;">
|
<a href="#" class="checkout-link-comanda" data-comanda-id="${p.comanda.id}" style="color:var(--accent-primary);text-decoration:none;font-weight:600;">
|
||||||
@@ -120,10 +124,14 @@ async function loadPagamentos() {
|
|||||||
</td>
|
</td>
|
||||||
<td data-label="Cliente" style="color:var(--text-secondary);">${p.comanda.nomeCliente ?? '—'}</td>
|
<td data-label="Cliente" style="color:var(--text-secondary);">${p.comanda.nomeCliente ?? '—'}</td>
|
||||||
<td data-label="Pagamento">${FORMAS_PAGAMENTO_LABEL[p.formaPagamento] ?? p.formaPagamento}</td>
|
<td data-label="Pagamento">${FORMAS_PAGAMENTO_LABEL[p.formaPagamento] ?? p.formaPagamento}</td>
|
||||||
<td data-label="Valor" style="text-align:right;font-weight:700;color:var(--success);">${formatBRL(p.valor)}</td>
|
<td data-label="Valor" style="${valorStyle}">${formatBRL(p.valor)}</td>
|
||||||
<td data-label="Obs" style="color:var(--text-secondary);font-style:italic;font-size:0.8rem;">${p.observacao ?? '—'}</td>
|
<td data-label="Obs" style="color:var(--text-secondary);font-style:italic;font-size:0.8rem;">${p.observacao ?? '—'}</td>
|
||||||
</tr>
|
${canReverse ? `<td data-label="Ação" style="text-align:center;">${estornado
|
||||||
`).join('')}
|
? '<span class="badge status-cancelada" style="font-size:0.65rem;">ESTORNADO</span>'
|
||||||
|
: `<button class="btn btn-sm btn-danger checkout-btn-estornar" data-pagamento-id="${p.id}" data-comanda-id="${p.comanda.id}">Estornar</button>`
|
||||||
|
}</td>` : ''}
|
||||||
|
</tr>`;
|
||||||
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +144,21 @@ async function loadPagamentos() {
|
|||||||
await (window as any).navigateTo('/orders#' + comandaId);
|
await (window as any).navigateTo('/orders#' + comandaId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lista.querySelectorAll('.checkout-btn-estornar').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pagamentoId = (btn as HTMLElement).dataset.pagamentoId!;
|
||||||
|
if (!confirm('Deseja estornar este pagamento? A comanda será reaberta.')) return;
|
||||||
|
try {
|
||||||
|
await api.patch(`/pagamentos/${pagamentoId}/estornar`, {});
|
||||||
|
toast('Pagamento estornado com sucesso', 'success');
|
||||||
|
await loadPagamentos();
|
||||||
|
} catch (err: any) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
lista.innerHTML = `<p style="color:var(--accent-primary)">Erro: ${err.message}</p>`;
|
lista.innerHTML = `<p style="color:var(--accent-primary)">Erro: ${err.message}</p>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { renderNavbar, toast, formatBRL } from '../utils';
|
import { renderNavbar, toast, formatBRL, toDisplayDate, parseDisplayDate, todayStr } from '../utils';
|
||||||
|
|
||||||
export async function renderDashboard(container: HTMLElement) {
|
export async function renderDashboard(container: HTMLElement) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -39,9 +39,11 @@ export async function renderDashboard(container: HTMLElement) {
|
|||||||
<h3 style="margin:0;">Relatório de Vendas</h3>
|
<h3 style="margin:0;">Relatório de Vendas</h3>
|
||||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||||||
<label style="color:var(--text-secondary);font-size:0.85rem;">Período:</label>
|
<label style="color:var(--text-secondary);font-size:0.85rem;">Período:</label>
|
||||||
<input type="date" id="rel-inicio" class="form-control" style="width:auto;">
|
<input type="text" id="rel-inicio" class="form-control" style="width:auto;" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
|
<input type="time" id="rel-hora-inicio" class="form-control" style="width:auto;" value="15:00">
|
||||||
<span style="color:var(--text-secondary);">até</span>
|
<span style="color:var(--text-secondary);">até</span>
|
||||||
<input type="date" id="rel-fim" class="form-control" style="width:auto;">
|
<input type="text" id="rel-fim" class="form-control" style="width:auto;" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
|
<input type="time" id="rel-hora-fim" class="form-control" style="width:auto;" value="04:00">
|
||||||
<button class="btn btn-sm" id="btn-gerar-relatorio">Gerar</button>
|
<button class="btn btn-sm" id="btn-gerar-relatorio">Gerar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,10 +61,10 @@ export async function renderDashboard(container: HTMLElement) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Set default dates
|
// Set default dates
|
||||||
const hoje = new Date().toISOString().split('T')[0];
|
const hoje = todayStr();
|
||||||
const inicioMes = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
|
const inicioMes = hoje.substring(0, 7) + '-01';
|
||||||
(document.getElementById('rel-inicio') as HTMLInputElement).value = inicioMes;
|
(document.getElementById('rel-inicio') as HTMLInputElement).value = toDisplayDate(inicioMes);
|
||||||
(document.getElementById('rel-fim') as HTMLInputElement).value = hoje;
|
(document.getElementById('rel-fim') as HTMLInputElement).value = toDisplayDate(hoje);
|
||||||
|
|
||||||
const loadDashboard = async () => {
|
const loadDashboard = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -80,12 +82,16 @@ export async function renderDashboard(container: HTMLElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadRelatorio = async () => {
|
const loadRelatorio = async () => {
|
||||||
const inicio = (document.getElementById('rel-inicio') as HTMLInputElement).value;
|
const inicio = parseDisplayDate((document.getElementById('rel-inicio') as HTMLInputElement).value);
|
||||||
const fim = (document.getElementById('rel-fim') as HTMLInputElement).value;
|
const horaInicio = (document.getElementById('rel-hora-inicio') as HTMLInputElement).value || '15:00';
|
||||||
|
const fim = parseDisplayDate((document.getElementById('rel-fim') as HTMLInputElement).value);
|
||||||
|
const horaFim = (document.getElementById('rel-hora-fim') as HTMLInputElement).value || '04:00';
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (inicio) params.set('inicio', inicio);
|
if (inicio) params.set('inicio', inicio);
|
||||||
if (fim) params.set('fim', fim);
|
if (fim) params.set('fim', fim);
|
||||||
|
params.set('horaInicio', horaInicio);
|
||||||
|
params.set('horaFim', horaFim);
|
||||||
const data = await api.get(`/relatorios/vendas?${params.toString()}`);
|
const data = await api.get(`/relatorios/vendas?${params.toString()}`);
|
||||||
|
|
||||||
// Resumo
|
// Resumo
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export async function renderMenu(container: HTMLElement) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin:0;">
|
<div class="form-group" style="margin:0;">
|
||||||
<label>Estoque Atual</label>
|
<label>Estoque Atual</label>
|
||||||
<input type="number" id="p-estoque" class="form-control" value="0" min="0">
|
<input type="number" id="p-estoque" class="form-control" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin:0;">
|
<div class="form-group" style="margin:0;">
|
||||||
<label>Estoque Mínimo</label>
|
<label>Estoque Mínimo</label>
|
||||||
<input type="number" id="p-estoque-min" class="form-control" value="0" min="0">
|
<input type="number" id="p-estoque-min" class="form-control" value="0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-top:1rem;">
|
<div class="form-group" style="margin-top:1rem;">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { renderNavbar, toast, formatBRL, getUser, statusBadge } from '../utils';
|
import { renderNavbar, toast, formatBRL, getUser, statusBadge, parseDisplayDate } from '../utils';
|
||||||
|
|
||||||
let localComandas: any[] = [];
|
let localComandas: any[] = [];
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@ export async function renderOrders(container: HTMLElement) {
|
|||||||
<form id="form-busca-comandas" class="comandas-search-bar">
|
<form id="form-busca-comandas" class="comandas-search-bar">
|
||||||
<div class="comandas-search-field">
|
<div class="comandas-search-field">
|
||||||
<label>De</label>
|
<label>De</label>
|
||||||
<input type="date" id="busca-data-inicio" class="form-control">
|
<input type="text" id="busca-data-inicio" class="form-control" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
<div class="comandas-search-field">
|
<div class="comandas-search-field">
|
||||||
<label>Até</label>
|
<label>Até</label>
|
||||||
<input type="date" id="busca-data-fim" class="form-control">
|
<input type="text" id="busca-data-fim" class="form-control" placeholder="dd/mm/yyyy" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
<div class="comandas-search-actions">
|
<div class="comandas-search-actions">
|
||||||
<button type="submit" class="btn btn-sm btn-secondary">Buscar</button>
|
<button type="submit" class="btn btn-sm btn-secondary">Buscar</button>
|
||||||
@@ -384,8 +384,8 @@ function bindOrderEvents() {
|
|||||||
|
|
||||||
document.getElementById('form-busca-comandas')?.addEventListener('submit', (e) => {
|
document.getElementById('form-busca-comandas')?.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const dataInicio = (document.getElementById('busca-data-inicio') as HTMLInputElement).value;
|
const dataInicio = parseDisplayDate((document.getElementById('busca-data-inicio') as HTMLInputElement).value);
|
||||||
const dataFim = (document.getElementById('busca-data-fim') as HTMLInputElement).value;
|
const dataFim = parseDisplayDate((document.getElementById('busca-data-fim') as HTMLInputElement).value);
|
||||||
if (!dataInicio && !dataFim) return loadComandas({ status: 'ABERTA,FECHADA' });
|
if (!dataInicio && !dataFim) return loadComandas({ status: 'ABERTA,FECHADA' });
|
||||||
loadComandas({ dataInicio, dataFim });
|
loadComandas({ dataInicio, dataFim });
|
||||||
});
|
});
|
||||||
@@ -747,13 +747,14 @@ function bindOrderEvents() {
|
|||||||
<div style="margin-top:0.75rem;">
|
<div style="margin-top:0.75rem;">
|
||||||
<div style="font-size:0.8rem;color:var(--text-secondary);margin-bottom:0.35rem;font-weight:600;">Histórico de Pagamentos</div>
|
<div style="font-size:0.8rem;color:var(--text-secondary);margin-bottom:0.35rem;font-weight:600;">Histórico de Pagamentos</div>
|
||||||
${pagamentos.map((p: any) => `
|
${pagamentos.map((p: any) => `
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:0.8rem;padding:0.3rem 0;border-bottom:1px solid var(--border-color);">
|
<div style="display:flex;justify-content:space-between;align-items:center;font-size:0.8rem;padding:0.3rem 0;border-bottom:1px solid var(--border-color);${p.estornado ? 'opacity:0.5;text-decoration:line-through;' : ''}">
|
||||||
<div>
|
<div>
|
||||||
<span style="color:var(--text-secondary);">${new Date(p.criadoEm).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
|
<span style="color:var(--text-secondary);">${new Date(p.criadoEm).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
|
||||||
<span style="margin-left:0.4rem;">${p.formaPagamento.replace('_', ' ')}</span>
|
<span style="margin-left:0.4rem;">${p.formaPagamento.replace('_', ' ')}</span>
|
||||||
${p.observacao ? `<span style="margin-left:0.4rem;color:var(--text-secondary);font-style:italic;">— ${p.observacao}</span>` : ''}
|
${p.observacao ? `<span style="margin-left:0.4rem;color:var(--text-secondary);font-style:italic;">— ${p.observacao}</span>` : ''}
|
||||||
|
${p.estornado ? '<span class="badge status-cancelada" style="font-size:0.6rem;margin-left:0.4rem;">ESTORNADO</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<span style="font-weight:600;color:var(--success);">${formatBRL(p.valor)}</span>
|
<span style="font-weight:600;color:${p.estornado ? 'var(--accent-primary)' : 'var(--success)'};">${formatBRL(p.valor)}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user