;
+ method?: string;
+ url?: string;
+ statusCode?: number;
+ durationMs?: number;
+ ip?: string;
+ remoteIp?: string;
+ userAgent?: string;
+ referer?: string | null;
raw: string;
}
@@ -39,6 +47,13 @@ function parseLine(line: string): LogEntry {
levelName: LEVEL_NAMES[level] ?? 'INFO',
msg: obj.msg ?? '',
err: obj.err,
+ method: obj.method,
+ url: obj.url,
+ statusCode: obj.statusCode,
+ durationMs: obj.durationMs,
+ ip: obj.ip ?? obj.remoteIp,
+ userAgent: obj.userAgent,
+ referer: obj.referer,
raw: line,
};
} catch {
@@ -206,6 +221,44 @@ export function ServerLogViewer({ backendUrl = 'http://192.168.18.93:3000' }: Se
{/* Message */}
+ {/* Extra fields for HTTP request logs */}
+ {entry.method && entry.url && (
+
+ = 500 ? 'text-red-400' :
+ entry.statusCode && entry.statusCode >= 400 ? 'text-amber-400' :
+ entry.statusCode && entry.statusCode >= 300 ? 'text-blue-400' :
+ 'text-green-400'
+ }`}>{entry.method}
+ {entry.url}
+ {entry.statusCode && (
+ = 500 ? 'text-red-400' :
+ entry.statusCode >= 400 ? 'text-amber-400' :
+ entry.statusCode >= 300 ? 'text-blue-400' :
+ 'text-green-400'
+ }`}>{entry.statusCode}
+ )}
+ {entry.durationMs !== undefined && (
+ {entry.durationMs}ms
+ )}
+ ยท
+ {entry.ip && (
+ ๐ {entry.ip}
+ )}
+ {entry.referer && (
+ โ {entry.referer.replace(/^https?:\/\//, '').slice(0, 60)}
+ )}
+ {entry.userAgent && (
+
+ {entry.userAgent.includes('Postman') ? '๐ฎ Postman' :
+ entry.userAgent.includes('curl') ? '๐ก curl' :
+ entry.userAgent.includes('node') ? 'โ๏ธ Node' :
+ '๐ฅ๏ธ Browser'}
+
+ )}
+
+ )}
{entry.msg}
{entry.err && (
diff --git a/project/apps/pos/.env.example b/project/apps/pos/.env.example
new file mode 100644
index 0000000..d2a5e83
--- /dev/null
+++ b/project/apps/pos/.env.example
@@ -0,0 +1,8 @@
+# URL del backend para el proxy del POS.
+# En producciรณn (detrรกs de Traefik): no es necesario, usa el mismo origen.
+# En desarrollo local o por IP LAN: REQUIRED โ el proxy no sabe dรณnde estรก el backend.
+# Copiar a .env.local y ajustar segรบn tu entorno:
+# Dev local: POS_BACKEND_URL=http://localhost:3000
+# Dev por IP: POS_BACKEND_URL=http://192.168.18.93:3000
+# Producciรณn: dejar vacรญo (Traefik sirve todo en el mismo dominio)
+# POS_BACKEND_URL=http://localhost:3000
diff --git a/project/apps/pos/next.config.ts b/project/apps/pos/next.config.ts
index 1c6bf50..c4d8103 100644
--- a/project/apps/pos/next.config.ts
+++ b/project/apps/pos/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
- allowedDevOrigins: ['192.168.18.93', 'localhost'],
+ allowedDevOrigins: ['192.168.18.93', 'localhost', '127.0.0.1', 'tpv-mv.rikrdo.com'],
turbopack: {
root: __dirname,
},
diff --git a/project/apps/pos/package.json b/project/apps/pos/package.json
index 30be720..2b06c5a 100644
--- a/project/apps/pos/package.json
+++ b/project/apps/pos/package.json
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --port 3006",
+ "dev": "next dev --port 3002",
"build": "next build",
- "start": "next start --port 3006"
+ "start": "next start --port 3002"
},
"dependencies": {
"next": "^16.3.1",
diff --git a/project/apps/pos/public/images/logo-main.png b/project/apps/pos/public/images/logo-main.png
new file mode 100644
index 0000000..ab3283d
Binary files /dev/null and b/project/apps/pos/public/images/logo-main.png differ
diff --git a/project/apps/pos/src/app/(terminal)/page.tsx b/project/apps/pos/src/app/(terminal)/page.tsx
index 4d387a8..9d32691 100644
--- a/project/apps/pos/src/app/(terminal)/page.tsx
+++ b/project/apps/pos/src/app/(terminal)/page.tsx
@@ -111,7 +111,10 @@ export default function RegisterPage() {
const [processing, setProcessing] = useState(false);
const [error, setError] = useState('');
const [pendingSales, setPendingSales] = useState([]);
+ const [todaySales, setTodaySales] = useState([]);
const [loadingPending, setLoadingPending] = useState(false);
+ const [salesTab, setSalesTab] = useState<'pending' | 'day'>('day');
+ const [printingSaleId, setPrintingSaleId] = useState(null);
const [restPaymentFor, setRestPaymentFor] = useState(null);
const [processingRest, setProcessingRest] = useState(false);
const [returnOrder, setReturnOrder] = useState<{ orderId: string; receipt: PosReceipt } | null>(
@@ -203,13 +206,38 @@ export default function RegisterPage() {
}
}, [config?.terminal?.id, config?.session?.status]);
+ // Load today's completed sales for the sidebar
+ const loadTodaySales = useCallback(async () => {
+ if (!config?.terminal) {
+ setTodaySales([]);
+ return;
+ }
+ try {
+ const data = await posApi.listSales<{ items: PosPendingSale[] }>({
+ terminalId: config.terminal.id,
+ });
+ // Filter to today's date (completed or any state)
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const todayItems = (data.items ?? []).filter((sale) => {
+ const saleDate = new Date(sale.createdAt);
+ saleDate.setHours(0, 0, 0, 0);
+ return saleDate.getTime() === today.getTime();
+ });
+ setTodaySales(todayItems);
+ } catch {
+ setTodaySales([]);
+ }
+ }, [config?.terminal?.id]);
+
// POS-FIX-9: removed 10s polling โ only manual refresh via button
// POS-FIX-6: re-run when config loads (terminalId changes from undefined to real id)
useEffect(() => {
if (!config?.terminal?.id || config.session?.status !== 'OPEN') return;
void loadPendingSales();
- }, [config?.terminal?.id, config?.session?.status, loadPendingSales]);
+ void loadTodaySales();
+ }, [config?.terminal?.id, config?.session?.status, loadPendingSales, loadTodaySales]);
useEffect(() => {
if (!config?.session || config.session.status !== 'OPEN') return;
@@ -580,6 +608,26 @@ export default function RegisterPage() {
}
};
+ // POS-FIX-12: reprint ticket โ load receipt modal for printing
+ const reprintSale = async (sale: PosPendingSale) => {
+ setPrintingSaleId(sale.id);
+ setError('');
+ try {
+ const result = await posApi.getReceipt<{ receipt: PosReceipt }>(sale.id);
+ // Show the receipt modal for printing
+ setReceipt(result.receipt);
+ // Also trigger print immediately after modal renders
+ setTimeout(() => {
+ const printBtn = document.querySelector('button[class*="bg-\[\#2D6A4F\]"]') as HTMLButtonElement | null;
+ if (printBtn) printBtn.click();
+ }, 100);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'No se pudo reimprimir el ticket');
+ } finally {
+ setPrintingSaleId(null);
+ }
+ };
+
// POS-FIX-8: recover a parked sale into the current cart
const recoverSale = async (sale: PosPendingSale) => {
// POS-FIX-9: if cart has items, show merge dialog instead of auto-merge
@@ -855,71 +903,264 @@ export default function RegisterPage() {
return (
|