feat(POS-FIX-7): completed feature
This commit is contained in:
@@ -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) ? (
|
||||
|
||||
Reference in New Issue
Block a user