feat(POS-FIX-7): completed feature

This commit is contained in:
chattie
2026-08-24 07:23:55 +02:00
parent cd2a8ab54d
commit 2467a02dd4
14 changed files with 305 additions and 81 deletions

View File

@@ -116,6 +116,7 @@ export default function RegisterPage() {
const [returnOrder, setReturnOrder] = useState<{ orderId: string; receipt: PosReceipt } | null>(
null,
);
const [showClearConfirm, setShowClearConfirm] = useState(false);
// POS-FIX-3/POS-FIX-5: close session
const [showCloseSession, setShowCloseSession] = useState(false);
const [closingActualCash, setClosingActualCash] = useState('');
@@ -471,6 +472,62 @@ export default function RegisterPage() {
}
};
const parkSale = async () => {
if (!config?.session) {
setError('No hay sesión abierta');
return;
}
if (cart.length === 0) {
setError('Carrito vacío');
return;
}
setProcessing(true);
setError('');
try {
const result = await posApi.createSale<PosSaleResponse>({
idempotencyKey: generateIdempotencyKey(),
cashSessionId: config.session.id,
terminalId: config.terminal.id,
items: cart.map((item) =>
item.kind === 'free'
? {
kind: 'free',
name: item.name,
unitPriceCents: item.unitPriceCents,
quantity: item.quantity,
}
: {
kind: 'stock',
variantId: item.variantId,
quantity: item.quantity,
discountCents: item.discountCents,
},
),
payments: [],
...(customer ? { customerId: customer.id } : {}),
});
setRestPaymentFor(null);
void loadPendingSales();
// Refresh the pending panel so the new parked sale appears
setLoadingPending(true);
try {
const data = (await posApi.listSales({ state: 'PENDING', terminalId: config.terminal.id })) as {
items: PosPendingSale[];
};
setPendingSales(data.items ?? []);
} catch {
// Non-fatal: the sale was created, panel refresh is best-effort
} finally {
setLoadingPending(false);
}
resetCashier();
} catch (err) {
setError(err instanceof Error ? err.message : 'No se pudo aparcar la venta');
} finally {
setProcessing(false);
}
};
const openRestPayment = (sale: PosPendingSale) => {
setRestPaymentFor(sale);
setError('');
@@ -512,6 +569,7 @@ export default function RegisterPage() {
setCustomer(null);
setError('');
setSearch('');
setShowClearConfirm(false);
};
const searchCustomers = async (query: string) => {
@@ -1144,6 +1202,14 @@ export default function RegisterPage() {
))}
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => void parkSale()}
disabled={processing || cart.length === 0}
className="min-h-16 rounded-xl bg-amber-500 text-lg font-bold text-white disabled:opacity-40"
>
{processing ? 'Guardando…' : 'Guardar pendiente'}
</button>
<button
type="button"
onClick={() => void confirmSale()}
@@ -1156,21 +1222,17 @@ export default function RegisterPage() {
}
className="min-h-16 rounded-xl bg-[#1B4332] text-lg font-bold text-white disabled:opacity-40"
>
{processing
? 'Confirmando…'
: paidCents < totals.total
? 'Guardar pendiente'
: 'Cobrar e imprimir'}
</button>
<button
type="button"
onClick={() => resetCashier()}
disabled={processing || cart.length === 0}
className="min-h-16 rounded-xl border border-gray-300 bg-white text-sm font-bold text-gray-700 disabled:opacity-40"
>
Vaciar caja
{processing ? 'Confirmando…' : 'Cobrar e imprimir'}
</button>
</div>
<button
type="button"
onClick={() => setShowClearConfirm(true)}
disabled={processing || cart.length === 0}
className="mt-2 w-full rounded-xl border border-gray-300 bg-white py-2 text-sm font-bold text-red-600 disabled:opacity-40"
>
Vaciar caja
</button>
</aside>
{showDiscountPanel && selectedItem && (
@@ -1256,6 +1318,38 @@ export default function RegisterPage() {
onClose={() => setPaymentMethod(null)}
/>
)}
{/* POS-FIX-7: confirmar antes de vaciar */}
{showClearConfirm && (
<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">¿Vaciar ticket?</h2>
<p className="mb-4 text-sm text-gray-600">
Se borrarán {cart.length} {cart.length === 1 ? 'artículo' : 'artículos'} del ticket actual.
Esta acción no se puede deshacer.
</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => setShowClearConfirm(false)}
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={() => {
setShowClearConfirm(false);
resetCashier();
}}
className="flex-1 rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700"
>
Vaciar
</button>
</div>
</div>
</div>
)}
{/* POS-FIX-3/POS-FIX-5: close session — 2-step: PIN then cash */}
{showCloseSession && (
(config?.terminal?.settings?.closeSessionRequiresPin && !closingPinStep) ? (