feat(POS-FIX-11): completed feature
This commit is contained in:
@@ -123,6 +123,8 @@ export default function RegisterPage() {
|
||||
const [recoveringSaleId, setRecoveringSaleId] = useState<string | null>(null);
|
||||
// POS-FIX-9: merge dialog when recovering with cart items
|
||||
const [mergePendingSale, setMergePendingSale] = useState<PosPendingSale | null>(null);
|
||||
// POS-FIX-11: park with name dialog
|
||||
const [pendingParkName, setPendingParkName] = useState<string | null>(null);
|
||||
// POS-FIX-3/POS-FIX-5: close session
|
||||
const [showCloseSession, setShowCloseSession] = useState(false);
|
||||
const [closingActualCash, setClosingActualCash] = useState('');
|
||||
@@ -481,6 +483,20 @@ export default function RegisterPage() {
|
||||
setError('Carrito vacío');
|
||||
return;
|
||||
}
|
||||
// POS-FIX-11: if no customer, ask for a name
|
||||
if (!customer) {
|
||||
setPendingParkName('');
|
||||
return;
|
||||
}
|
||||
await doParkSale();
|
||||
};
|
||||
|
||||
// POS-FIX-11: actual parking with optional name
|
||||
const doParkSale = async (name?: string) => {
|
||||
if (!config?.session) {
|
||||
setError('No hay sesión abierta');
|
||||
return;
|
||||
}
|
||||
setProcessing(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -505,8 +521,10 @@ export default function RegisterPage() {
|
||||
),
|
||||
payments: [],
|
||||
...(customer ? { customerId: customer.id } : {}),
|
||||
...(name ? { posLabel: name } : {}),
|
||||
});
|
||||
setRestPaymentFor(null);
|
||||
setPendingParkName(null);
|
||||
void loadPendingSales();
|
||||
// Refresh the pending panel so the new parked sale appears
|
||||
setLoadingPending(true);
|
||||
@@ -866,14 +884,19 @@ export default function RegisterPage() {
|
||||
<p className="text-xs text-gray-500">Sin ventas pendientes.</p>
|
||||
) : (
|
||||
<ul className="flex flex-1 flex-col gap-2 overflow-y-auto">
|
||||
{pendingSales.map((sale) => (
|
||||
{pendingSales.map((sale) => {
|
||||
const saleDate = new Date(sale.createdAt);
|
||||
const dateStr = saleDate.toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||
const timeStr = saleDate.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
||||
const displayName = sale.posLabel || sale.receiptNumber || sale.id.slice(0, 8);
|
||||
return (
|
||||
<li
|
||||
key={sale.id}
|
||||
className="rounded-xl border border-amber-200 bg-white p-3 text-sm shadow-sm"
|
||||
>
|
||||
<p className="font-semibold text-gray-800">{sale.receiptNumber ?? sale.id.slice(0, 8)}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Total {formatPrice(sale.totalCents)} · pendiente {formatPrice(sale.outstandingCents)}
|
||||
<p className="font-semibold text-gray-800">{displayName}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
📅 {dateStr} {timeStr} · Total {formatPrice(sale.totalCents)} · pendiente {formatPrice(sale.outstandingCents)}
|
||||
</p>
|
||||
<div className="mt-2 flex gap-1">
|
||||
<button
|
||||
@@ -894,7 +917,8 @@ export default function RegisterPage() {
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</aside>
|
||||
@@ -1484,6 +1508,53 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* POS-FIX-11: name dialog when parking without customer */}
|
||||
{pendingParkName !== null && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="w-full max-w-xs rounded-2xl bg-white p-6 shadow-xl">
|
||||
<h2 className="mb-2 text-lg font-bold text-gray-900">Nombre para identificar</h2>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
¿A nombre de quién es este ticket pendiente?
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={pendingParkName}
|
||||
onChange={(e) => setPendingParkName(e.target.value)}
|
||||
placeholder="Ej: María García"
|
||||
maxLength={100}
|
||||
className="mb-4 w-full rounded-xl border border-gray-300 px-3 py-2 text-sm outline-none focus:border-[#2D6A4F]"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && pendingParkName.trim()) {
|
||||
void doParkSale(pendingParkName.trim());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPendingParkName(null)}
|
||||
className="flex-1 rounded-xl border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (pendingParkName.trim()) {
|
||||
void doParkSale(pendingParkName.trim());
|
||||
}
|
||||
}}
|
||||
disabled={!pendingParkName.trim()}
|
||||
className="flex-1 rounded-xl bg-[#2D6A4F] px-4 py-2 text-sm font-bold text-white disabled:opacity-50"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* POS-FIX-8: delete confirmation for pending sale */}
|
||||
{deleteConfirmFor && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
|
||||
@@ -87,6 +87,7 @@ export interface PosPendingSale {
|
||||
createdAt: string;
|
||||
cashierEmail?: string | null;
|
||||
receiptNumber?: string | null;
|
||||
posLabel?: string | null;
|
||||
}
|
||||
|
||||
export interface RecoveredOrderItem {
|
||||
|
||||
Reference in New Issue
Block a user