fix(reporting): resize trend chart and repair closures detail

This commit is contained in:
Deploy
2026-08-25 22:24:11 +02:00
parent ae42bae61e
commit f76591518e
15 changed files with 68 additions and 60 deletions

View File

@@ -1 +1 @@
0.2.1 0.2.2

View File

@@ -1,12 +1,12 @@
{ {
"name": "@mercadodevida/admin", "name": "@mercadodevida/admin",
"version": "0.2.1", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@mercadodevida/admin", "name": "@mercadodevida/admin",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"@lexical/history": "^0.49.0", "@lexical/history": "^0.49.0",
"@lexical/html": "^0.49.0", "@lexical/html": "^0.49.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mercadodevida/admin", "name": "@mercadodevida/admin",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --port 3001", "dev": "next dev --port 3001",

View File

@@ -13,18 +13,25 @@ interface SalesSummary {
byState: Record<string, { count: number; totalCents: number }>; byState: Record<string, { count: number; totalCents: number }>;
} }
interface ClosedSession { interface CashCloseReport {
id: string; session: {
openedAt: string; id: string;
closedAt: string; openedAt: string;
userId: string; closedAt: string | null;
status: string; userId: string;
openingCashCents: number; status: string;
closingCashCents: number | null; };
actualCashCents: number | null; storeId: string;
differenceCents: number | null; terminalId: string;
financial: {
openingCashCents: number;
closingCashCents: number;
actualCashCents: number;
expectedCashCents: number;
differenceCents: number;
};
sales: SalesSummary; sales: SalesSummary;
paymentsByMethod: Array<{ methodCode: string; methodName: string; totalCents: number; count: number }>; payments: Array<{ methodCode: string; methodName: string; totalCents: number; transactionCount: number }>;
items: { soldCount: number; uniqueProducts: number }; items: { soldCount: number; uniqueProducts: number };
} }
@@ -53,10 +60,11 @@ interface SessionRow {
export default function ReportingClosuresPage() { export default function ReportingClosuresPage() {
const [sessions, setSessions] = useState<SessionRow[]>([]); const [sessions, setSessions] = useState<SessionRow[]>([]);
const [report, setReport] = useState<ClosedSession | null>(null); const [report, setReport] = useState<CashCloseReport | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [loadingReport, setLoadingReport] = useState(false); const [loadingReport, setLoadingReport] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [reportError, setReportError] = useState('');
const [selectedStore, setSelectedStore] = useState(''); const [selectedStore, setSelectedStore] = useState('');
const [stores, setStores] = useState<Array<{ id: string; name: string }>>([]); const [stores, setStores] = useState<Array<{ id: string; name: string }>>([]);
const [filterDays, setFilterDays] = useState(30); const [filterDays, setFilterDays] = useState(30);
@@ -85,11 +93,14 @@ export default function ReportingClosuresPage() {
const loadReport = async (sessionId: string) => { const loadReport = async (sessionId: string) => {
setLoadingReport(true); setLoadingReport(true);
setReport(null);
setReportError('');
try { try {
const data = await api.get<ClosedSession>(`/api/pos/reports/cash-close/${sessionId}`); const data = await api.get<CashCloseReport>(`/api/pos/reports/cash-close/${sessionId}`);
setReport(data); setReport(data);
} catch { } catch (err) {
setReport(null); setReport(null);
setReportError(err instanceof Error ? err.message : 'Error al cargar reporte de cierre');
} finally { } finally {
setLoadingReport(false); setLoadingReport(false);
} }
@@ -108,7 +119,7 @@ export default function ReportingClosuresPage() {
if (selectedStore) void loadSessions(); if (selectedStore) void loadSessions();
}, [selectedStore, filterDays, loadSessions]); }, [selectedStore, filterDays, loadSessions]);
const diff = report?.differenceCents ?? 0; const diff = report?.financial.differenceCents ?? 0;
const diffClass = diff > 0 ? 'text-green-600' : diff < 0 ? 'text-red-600' : 'text-gray-600'; const diffClass = diff > 0 ? 'text-green-600' : diff < 0 ? 'text-red-600' : 'text-gray-600';
return ( return (
@@ -163,7 +174,7 @@ export default function ReportingClosuresPage() {
type="button" type="button"
onClick={() => void loadReport(s.id)} onClick={() => void loadReport(s.id)}
className={`w-full text-left rounded-xl border p-3 text-sm transition-colors ${ className={`w-full text-left rounded-xl border p-3 text-sm transition-colors ${
report?.id === s.id report?.session.id === s.id
? 'border-[#2D6A4F] bg-[#2D6A4F]/5' ? 'border-[#2D6A4F] bg-[#2D6A4F]/5'
: 'border-gray-200 hover:border-[#2D6A4F] hover:bg-gray-50' : 'border-gray-200 hover:border-[#2D6A4F] hover:bg-gray-50'
}`} }`}
@@ -193,14 +204,14 @@ export default function ReportingClosuresPage() {
{/* Report detail */} {/* Report detail */}
<div className="lg:col-span-3"> <div className="lg:col-span-3">
{!report ? ( {loadingReport ? (
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-dashed border-gray-300 text-gray-400 text-sm">
Selecciona una sesión para ver el reporte
</div>
) : loadingReport ? (
<div className="flex h-64 items-center justify-center text-gray-400 text-sm"> <div className="flex h-64 items-center justify-center text-gray-400 text-sm">
Cargando reporte Cargando reporte
</div> </div>
) : !report ? (
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-dashed border-gray-300 px-6 text-center text-gray-400 text-sm">
{reportError || 'Selecciona una sesión para ver el reporte'}
</div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{/* Financial summary */} {/* Financial summary */}
@@ -208,9 +219,9 @@ export default function ReportingClosuresPage() {
<h3 className="mb-4 text-base font-bold text-gray-900">📊 Resumen financiero</h3> <h3 className="mb-4 text-base font-bold text-gray-900">📊 Resumen financiero</h3>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{[ {[
['Saldo inicial', fmt(report.openingCashCents)], ['Saldo inicial', fmt(report.financial.openingCashCents)],
['Ventas', fmt(report.sales.completedTotalCents)], ['Ventas', fmt(report.sales.completedTotalCents)],
['Saldo esperado', fmt(report.openingCashCents + report.sales.completedTotalCents)], ['Saldo esperado', fmt(report.financial.expectedCashCents)],
].map(([label, value]) => ( ].map(([label, value]) => (
<div key={label}> <div key={label}>
<p className="text-xs text-gray-500">{label}</p> <p className="text-xs text-gray-500">{label}</p>
@@ -219,7 +230,7 @@ export default function ReportingClosuresPage() {
))} ))}
<div> <div>
<p className="text-xs text-gray-500">Efectivo real</p> <p className="text-xs text-gray-500">Efectivo real</p>
<p className="text-lg font-bold text-gray-900">{fmt(report.actualCashCents ?? 0)}</p> <p className="text-lg font-bold text-gray-900">{fmt(report.financial.actualCashCents)}</p>
</div> </div>
<div> <div>
<p className="text-xs text-gray-500">Diferencia</p> <p className="text-xs text-gray-500">Diferencia</p>
@@ -257,7 +268,7 @@ export default function ReportingClosuresPage() {
</div> </div>
{/* By payment method */} {/* By payment method */}
{report.paymentsByMethod.length > 0 && ( {report.payments.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-5"> <div className="rounded-xl border border-gray-200 bg-white p-5">
<h3 className="mb-3 text-base font-bold text-gray-900">💳 Formas de pago</h3> <h3 className="mb-3 text-base font-bold text-gray-900">💳 Formas de pago</h3>
<table className="w-full text-sm"> <table className="w-full text-sm">
@@ -269,10 +280,10 @@ export default function ReportingClosuresPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{report.paymentsByMethod.map(p => ( {report.payments.map(p => (
<tr key={p.methodCode} className="border-b border-gray-50 last:border-0"> <tr key={p.methodCode} className="border-b border-gray-50 last:border-0">
<td className="py-2 font-medium text-gray-800">{p.methodName}</td> <td className="py-2 font-medium text-gray-800">{p.methodName}</td>
<td className="py-2 text-right text-gray-600">{p.count}</td> <td className="py-2 text-right text-gray-600">{p.transactionCount}</td>
<td className="py-2 text-right font-bold text-gray-900">{fmt(p.totalCents)}</td> <td className="py-2 text-right font-bold text-gray-900">{fmt(p.totalCents)}</td>
</tr> </tr>
))} ))}

View File

@@ -79,7 +79,7 @@ function TrendDashboard({ filters }: { filters: FilterState }) {
const maxValue = Math.max(...points.map((p) => p.value), 1); const maxValue = Math.max(...points.map((p) => p.value), 1);
return <TrendChart data={points} maxValue={maxValue} height={100} />; return <TrendChart data={points} maxValue={maxValue} height={180} />;
} }
function ChannelDashboard({ filters }: { filters: FilterState }) { function ChannelDashboard({ filters }: { filters: FilterState }) {

View File

@@ -19,14 +19,15 @@ export function TrendChart({ data, maxValue, height = 120 }: TrendChartProps) {
const width = 100; // percentage-based SVG const width = 100; // percentage-based SVG
const barWidth = Math.min(3, (width * 0.9) / data.length); const barWidth = Math.min(3, (width * 0.9) / data.length);
const gap = Math.max(0.2, (width - barWidth * data.length) / (data.length + 1)); const gap = Math.max(0.2, (width - barWidth * data.length) / (data.length + 1));
const chartHeight = height - 40; // leave room for labels const chartHeight = height - 34; // leave room for labels
return ( return (
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto rounded-xl bg-white">
<svg <svg
viewBox={`0 0 ${width} ${height}`} viewBox={`0 0 ${width} ${height}`}
className="w-full" className="block w-full"
style={{ minWidth: `${Math.max(300, data.length * 8)}px` }} style={{ minWidth: `${Math.max(360, data.length * 10)}px`, height: `${height}px` }}
preserveAspectRatio="none"
aria-label="Tendencia de ventas" aria-label="Tendencia de ventas"
role="img" role="img"
> >

View File

@@ -1,12 +1,12 @@
{ {
"name": "mercadodevida-pos", "name": "mercadodevida-pos",
"version": "0.2.1", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mercadodevida-pos", "name": "mercadodevida-pos",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"next": "^16.3.1", "next": "^16.3.1",
"react": "^19.2.8", "react": "^19.2.8",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mercadodevida-pos", "name": "mercadodevida-pos",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --port 3002", "dev": "next dev --port 3002",

View File

@@ -1,12 +1,12 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.2.1", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend", "name": "frontend",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"next": "16.3.1", "next": "16.3.1",
"react": "19.2.8", "react": "19.2.8",

View File

@@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -1,12 +1,12 @@
{ {
"name": "mercadodevida-backend", "name": "mercadodevida-backend",
"version": "0.2.1", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mercadodevida-backend", "name": "mercadodevida-backend",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"@fastify/cookie": "^11.1.2", "@fastify/cookie": "^11.1.2",
"@fastify/cors": "^11.3.0", "@fastify/cors": "^11.3.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mercadodevida-backend", "name": "mercadodevida-backend",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "mercadodevida vNext backend - modular monolith skeleton", "description": "mercadodevida vNext backend - modular monolith skeleton",

View File

@@ -699,13 +699,11 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
), ),
pool.query<{ method_code: string; method_name: string; total: string; count: string }>( pool.query<{ method_code: string; method_name: string; total: string; count: string }>(
`SELECT pm.code AS method_code, pm.label AS method_name, `SELECT pm.code AS method_code, pm.label AS method_name,
COALESCE(SUM(pt.amount_cents), 0)::bigint AS total, COALESCE(SUM(rpl.amount_cents), 0)::bigint AS total,
COUNT(*)::int AS count COUNT(*)::int AS count
FROM payments_transactions pt FROM reporting_payment_lines rpl
JOIN orders_orders o ON o.id = pt.order_id LEFT JOIN pos_payment_methods pm ON pm.id = rpl.payment_method_id
LEFT JOIN pos_payment_methods pm ON pm.id::text = pt.provider_event_id WHERE rpl.cash_session_id = $1 AND rpl.status = 'payment'
AND pm.store_id = o.store_id
WHERE o.cash_session_id = $1 AND o.source = 'pos' AND pt.status = 'succeeded'
GROUP BY pm.code, pm.label`, GROUP BY pm.code, pm.label`,
[id], [id],
), ),
@@ -771,13 +769,11 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
), ),
pool.query<{ method_code: string; method_name: string; total: string; count: string }>( pool.query<{ method_code: string; method_name: string; total: string; count: string }>(
`SELECT pm.code AS method_code, pm.label AS method_name, `SELECT pm.code AS method_code, pm.label AS method_name,
COALESCE(SUM(pt.amount_cents), 0)::bigint AS total, COALESCE(SUM(rpl.amount_cents), 0)::bigint AS total,
COUNT(*)::int AS count COUNT(*)::int AS count
FROM payments_transactions pt FROM reporting_payment_lines rpl
JOIN orders_orders o ON o.id = pt.order_id LEFT JOIN pos_payment_methods pm ON pm.id = rpl.payment_method_id
LEFT JOIN pos_payment_methods pm ON pm.id::text = pt.provider_event_id WHERE rpl.cash_session_id = $1 AND rpl.status = 'payment'
AND pm.store_id = o.store_id
WHERE o.cash_session_id = $1 AND o.source = 'pos' AND pt.status = 'succeeded'
GROUP BY pm.code, pm.label`, GROUP BY pm.code, pm.label`,
[id], [id],
), ),

View File

@@ -1,12 +1,12 @@
{ {
"name": "mercadodevida-storefront", "name": "mercadodevida-storefront",
"version": "0.2.1", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mercadodevida-storefront", "name": "mercadodevida-storefront",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"next": "^16.0.5", "next": "^16.0.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mercadodevida-storefront", "name": "mercadodevida-storefront",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "mercadodevida customer storefront shell", "description": "mercadodevida customer storefront shell",