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",
"version": "0.2.1",
"version": "0.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@mercadodevida/admin",
"version": "0.2.1",
"version": "0.2.2",
"dependencies": {
"@lexical/history": "^0.49.0",
"@lexical/html": "^0.49.0",

View File

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

View File

@@ -13,18 +13,25 @@ interface SalesSummary {
byState: Record<string, { count: number; totalCents: number }>;
}
interface ClosedSession {
id: string;
openedAt: string;
closedAt: string;
userId: string;
status: string;
openingCashCents: number;
closingCashCents: number | null;
actualCashCents: number | null;
differenceCents: number | null;
interface CashCloseReport {
session: {
id: string;
openedAt: string;
closedAt: string | null;
userId: string;
status: string;
};
storeId: string;
terminalId: string;
financial: {
openingCashCents: number;
closingCashCents: number;
actualCashCents: number;
expectedCashCents: number;
differenceCents: number;
};
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 };
}
@@ -53,10 +60,11 @@ interface SessionRow {
export default function ReportingClosuresPage() {
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 [loadingReport, setLoadingReport] = useState(false);
const [error, setError] = useState('');
const [reportError, setReportError] = useState('');
const [selectedStore, setSelectedStore] = useState('');
const [stores, setStores] = useState<Array<{ id: string; name: string }>>([]);
const [filterDays, setFilterDays] = useState(30);
@@ -85,11 +93,14 @@ export default function ReportingClosuresPage() {
const loadReport = async (sessionId: string) => {
setLoadingReport(true);
setReport(null);
setReportError('');
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);
} catch {
} catch (err) {
setReport(null);
setReportError(err instanceof Error ? err.message : 'Error al cargar reporte de cierre');
} finally {
setLoadingReport(false);
}
@@ -108,7 +119,7 @@ export default function ReportingClosuresPage() {
if (selectedStore) void 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';
return (
@@ -163,7 +174,7 @@ export default function ReportingClosuresPage() {
type="button"
onClick={() => void loadReport(s.id)}
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-gray-200 hover:border-[#2D6A4F] hover:bg-gray-50'
}`}
@@ -193,14 +204,14 @@ export default function ReportingClosuresPage() {
{/* Report detail */}
<div className="lg:col-span-3">
{!report ? (
<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 ? (
{loadingReport ? (
<div className="flex h-64 items-center justify-center text-gray-400 text-sm">
Cargando reporte
</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">
{/* Financial summary */}
@@ -208,9 +219,9 @@ export default function ReportingClosuresPage() {
<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">
{[
['Saldo inicial', fmt(report.openingCashCents)],
['Saldo inicial', fmt(report.financial.openingCashCents)],
['Ventas', fmt(report.sales.completedTotalCents)],
['Saldo esperado', fmt(report.openingCashCents + report.sales.completedTotalCents)],
['Saldo esperado', fmt(report.financial.expectedCashCents)],
].map(([label, value]) => (
<div key={label}>
<p className="text-xs text-gray-500">{label}</p>
@@ -219,7 +230,7 @@ export default function ReportingClosuresPage() {
))}
<div>
<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>
<p className="text-xs text-gray-500">Diferencia</p>
@@ -257,7 +268,7 @@ export default function ReportingClosuresPage() {
</div>
{/* By payment method */}
{report.paymentsByMethod.length > 0 && (
{report.payments.length > 0 && (
<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>
<table className="w-full text-sm">
@@ -269,10 +280,10 @@ export default function ReportingClosuresPage() {
</tr>
</thead>
<tbody>
{report.paymentsByMethod.map(p => (
{report.payments.map(p => (
<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 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>
</tr>
))}

View File

@@ -79,7 +79,7 @@ function TrendDashboard({ filters }: { filters: FilterState }) {
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 }) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "mercadodevida-backend",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"type": "module",
"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 }>(
`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
FROM payments_transactions pt
JOIN orders_orders o ON o.id = pt.order_id
LEFT JOIN pos_payment_methods pm ON pm.id::text = pt.provider_event_id
AND pm.store_id = o.store_id
WHERE o.cash_session_id = $1 AND o.source = 'pos' AND pt.status = 'succeeded'
FROM reporting_payment_lines rpl
LEFT JOIN pos_payment_methods pm ON pm.id = rpl.payment_method_id
WHERE rpl.cash_session_id = $1 AND rpl.status = 'payment'
GROUP BY pm.code, pm.label`,
[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 }>(
`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
FROM payments_transactions pt
JOIN orders_orders o ON o.id = pt.order_id
LEFT JOIN pos_payment_methods pm ON pm.id::text = pt.provider_event_id
AND pm.store_id = o.store_id
WHERE o.cash_session_id = $1 AND o.source = 'pos' AND pt.status = 'succeeded'
FROM reporting_payment_lines rpl
LEFT JOIN pos_payment_methods pm ON pm.id = rpl.payment_method_id
WHERE rpl.cash_session_id = $1 AND rpl.status = 'payment'
GROUP BY pm.code, pm.label`,
[id],
),

View File

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

View File

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