feat: estoque negativo, UTC-3, dd/mm/yyyy, estorno de pagamento

This commit is contained in:
2026-07-27 14:13:56 -03:00
parent 14e92c91fd
commit b567a45803
14 changed files with 239 additions and 75 deletions

View File

@@ -21,6 +21,28 @@ export function getUser(): { id: string; nome: string; email: string; role: stri
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$ */
export function formatBRL(value: number): string {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);

View File

@@ -1,5 +1,5 @@
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> = {
DINHEIRO: '💵 Dinheiro',
@@ -8,18 +8,12 @@ const FORMAS_PAGAMENTO_LABEL: Record<string, string> = {
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 };
const hoje = todayStr();
const ontemMs = new Date(hoje + 'T12:00:00Z').getTime() - 12 * 60 * 60 * 1000;
const ontem = new Date(ontemMs);
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) {
@@ -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>
<div style="display:flex;align-items:center;gap:0.4rem;">
<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 style="display:flex;align-items:center;gap:0.4rem;">
<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>
<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>
@@ -67,8 +61,8 @@ export async function renderCheckout(container: HTMLElement) {
}
async function loadPagamentos() {
const dataInicio = (document.getElementById('checkout-data-inicio') as HTMLInputElement).value;
const dataFim = (document.getElementById('checkout-data-fim') as HTMLInputElement).value;
const dataInicio = parseDisplayDate((document.getElementById('checkout-data-inicio') as HTMLInputElement).value);
const dataFim = parseDisplayDate((document.getElementById('checkout-data-fim') as HTMLInputElement).value);
const lista = document.getElementById('checkout-pagamentos-lista')!;
const totalEl = document.getElementById('checkout-pagamentos-total')!;
@@ -95,6 +89,9 @@ async function loadPagamentos() {
</div>
`;
const user = getUser();
const canReverse = user?.role === 'ADMIN' || user?.role === 'CAIXA';
lista.innerHTML = `
<div class="table-wrap">
<table>
@@ -106,11 +103,18 @@ async function loadPagamentos() {
<th>Forma de Pagamento</th>
<th style="text-align:right;">Valor</th>
<th>Obs</th>
${canReverse ? '<th style="text-align:center;">Ação</th>' : ''}
</tr>
</thead>
<tbody>
${pagamentos.map((p: any) => `
<tr>
${pagamentos.map((p: any) => {
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="Comanda">
<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 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="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>
</tr>
`).join('')}
${canReverse ? `<td data-label="Ação" style="text-align:center;">${estornado
? '<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>
</table>
</div>
@@ -136,6 +144,21 @@ async function loadPagamentos() {
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) {
lista.innerHTML = `<p style="color:var(--accent-primary)">Erro: ${err.message}</p>`;
}

View File

@@ -1,5 +1,5 @@
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) {
container.innerHTML = `
@@ -39,9 +39,11 @@ export async function renderDashboard(container: HTMLElement) {
<h3 style="margin:0;">Relatório de Vendas</h3>
<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>
<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>
<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>
</div>
</div>
@@ -59,10 +61,10 @@ export async function renderDashboard(container: HTMLElement) {
`;
// Set default dates
const hoje = new Date().toISOString().split('T')[0];
const inicioMes = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
(document.getElementById('rel-inicio') as HTMLInputElement).value = inicioMes;
(document.getElementById('rel-fim') as HTMLInputElement).value = hoje;
const hoje = todayStr();
const inicioMes = hoje.substring(0, 7) + '-01';
(document.getElementById('rel-inicio') as HTMLInputElement).value = toDisplayDate(inicioMes);
(document.getElementById('rel-fim') as HTMLInputElement).value = toDisplayDate(hoje);
const loadDashboard = async () => {
try {
@@ -80,12 +82,16 @@ export async function renderDashboard(container: HTMLElement) {
};
const loadRelatorio = async () => {
const inicio = (document.getElementById('rel-inicio') as HTMLInputElement).value;
const fim = (document.getElementById('rel-fim') as HTMLInputElement).value;
const inicio = parseDisplayDate((document.getElementById('rel-inicio') 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 {
const params = new URLSearchParams();
if (inicio) params.set('inicio', inicio);
if (fim) params.set('fim', fim);
params.set('horaInicio', horaInicio);
params.set('horaFim', horaFim);
const data = await api.get(`/relatorios/vendas?${params.toString()}`);
// Resumo

View File

@@ -43,11 +43,11 @@ export async function renderMenu(container: HTMLElement) {
</div>
<div class="form-group" style="margin:0;">
<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 class="form-group" style="margin:0;">
<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 class="form-group" style="margin-top:1rem;">

View File

@@ -1,5 +1,5 @@
import { api } from '../api';
import { renderNavbar, toast, formatBRL, getUser, statusBadge } from '../utils';
import { renderNavbar, toast, formatBRL, getUser, statusBadge, parseDisplayDate } from '../utils';
let localComandas: any[] = [];
@@ -22,11 +22,11 @@ export async function renderOrders(container: HTMLElement) {
<form id="form-busca-comandas" class="comandas-search-bar">
<div class="comandas-search-field">
<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 class="comandas-search-field">
<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 class="comandas-search-actions">
<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) => {
e.preventDefault();
const dataInicio = (document.getElementById('busca-data-inicio') as HTMLInputElement).value;
const dataFim = (document.getElementById('busca-data-fim') as HTMLInputElement).value;
const dataInicio = parseDisplayDate((document.getElementById('busca-data-inicio') as HTMLInputElement).value);
const dataFim = parseDisplayDate((document.getElementById('busca-data-fim') as HTMLInputElement).value);
if (!dataInicio && !dataFim) return loadComandas({ status: 'ABERTA,FECHADA' });
loadComandas({ dataInicio, dataFim });
});
@@ -747,13 +747,14 @@ function bindOrderEvents() {
<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>
${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>
<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>
${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>
<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>
`).join('')}
</div>