fix(reporting): resize trend chart and repair closures detail
This commit is contained in:
@@ -1 +1 @@
|
|||||||
0.2.1
|
0.2.2
|
||||||
|
|||||||
4
project/apps/admin/package-lock.json
generated
4
project/apps/admin/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -13,18 +13,25 @@ interface SalesSummary {
|
|||||||
byState: Record<string, { count: number; totalCents: number }>;
|
byState: Record<string, { count: number; totalCents: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClosedSession {
|
interface CashCloseReport {
|
||||||
|
session: {
|
||||||
id: string;
|
id: string;
|
||||||
openedAt: string;
|
openedAt: string;
|
||||||
closedAt: string;
|
closedAt: string | null;
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
};
|
||||||
|
storeId: string;
|
||||||
|
terminalId: string;
|
||||||
|
financial: {
|
||||||
openingCashCents: number;
|
openingCashCents: number;
|
||||||
closingCashCents: number | null;
|
closingCashCents: number;
|
||||||
actualCashCents: number | null;
|
actualCashCents: number;
|
||||||
differenceCents: number | null;
|
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);
|
||||||
try {
|
|
||||||
const data = await api.get<ClosedSession>(`/api/pos/reports/cash-close/${sessionId}`);
|
|
||||||
setReport(data);
|
|
||||||
} catch {
|
|
||||||
setReport(null);
|
setReport(null);
|
||||||
|
setReportError('');
|
||||||
|
try {
|
||||||
|
const data = await api.get<CashCloseReport>(`/api/pos/reports/cash-close/${sessionId}`);
|
||||||
|
setReport(data);
|
||||||
|
} catch (err) {
|
||||||
|
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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
4
project/apps/pos/package-lock.json
generated
4
project/apps/pos/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
4
project/frontend/package-lock.json
generated
4
project/frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
4
project/package-lock.json
generated
4
project/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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],
|
||||||
),
|
),
|
||||||
|
|||||||
4
project/storefront/package-lock.json
generated
4
project/storefront/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user