feat(POS-FIX-8): completed feature
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
PosPendingSale,
|
||||
PosReceipt,
|
||||
PosSaleResponse,
|
||||
RecoveredOrderItem,
|
||||
} from '@/types/checkout';
|
||||
|
||||
interface CartItem {
|
||||
@@ -117,6 +118,9 @@ export default function RegisterPage() {
|
||||
null,
|
||||
);
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||
// POS-FIX-8: delete confirmation for pending sales
|
||||
const [deleteConfirmFor, setDeleteConfirmFor] = useState<PosPendingSale | null>(null);
|
||||
const [recoveringSaleId, setRecoveringSaleId] = useState<string | null>(null);
|
||||
// POS-FIX-3/POS-FIX-5: close session
|
||||
const [showCloseSession, setShowCloseSession] = useState(false);
|
||||
const [closingActualCash, setClosingActualCash] = useState('');
|
||||
@@ -562,6 +566,54 @@ export default function RegisterPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// POS-FIX-8: recover a parked sale into the current cart
|
||||
const recoverSale = async (sale: PosPendingSale) => {
|
||||
setRecoveringSaleId(sale.id);
|
||||
setError('');
|
||||
try {
|
||||
const data = (await posApi.listOrderItems<{ items: RecoveredOrderItem[] }>(sale.id));
|
||||
const items: CartItem[] = data.items
|
||||
.filter((item) => !item.freeItem)
|
||||
.map((item) => ({
|
||||
lineId: `${item.id}-recovered-${Date.now()}`,
|
||||
kind: 'stock' as const,
|
||||
variantId: null,
|
||||
productId: null,
|
||||
sku: item.sku,
|
||||
name: item.name,
|
||||
ean: null,
|
||||
unitPriceCents: item.unitPriceCents,
|
||||
discountCents: item.discountCents,
|
||||
taxCents: 0,
|
||||
quantity: item.quantity - item.returnedQuantity,
|
||||
stock: null,
|
||||
}));
|
||||
if (cart.length > 0) {
|
||||
// Merge: append recovered items to existing cart
|
||||
setCart((prev) => [...prev, ...items]);
|
||||
} else {
|
||||
setCart(items);
|
||||
}
|
||||
void loadPendingSales();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'No se pudo recuperar la venta');
|
||||
} finally {
|
||||
setRecoveringSaleId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// POS-FIX-8: delete a parked sale
|
||||
const deleteSale = async (sale: PosPendingSale) => {
|
||||
setError('');
|
||||
try {
|
||||
await posApi.deleteSale(sale.id);
|
||||
setDeleteConfirmFor(null);
|
||||
void loadPendingSales();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'No se pudo eliminar la venta');
|
||||
}
|
||||
};
|
||||
|
||||
const resetCashier = () => {
|
||||
setReceipt(null);
|
||||
setCart([]);
|
||||
@@ -751,14 +803,24 @@ export default function RegisterPage() {
|
||||
<p className="text-xs text-gray-500">
|
||||
Total {formatPrice(sale.totalCents)} · pendiente {formatPrice(sale.outstandingCents)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openRestPayment(sale)}
|
||||
disabled={processingRest}
|
||||
className="mt-2 w-full rounded-lg bg-amber-500 px-3 py-1.5 text-xs font-bold text-white disabled:opacity-50"
|
||||
>
|
||||
Cobrar resto
|
||||
</button>
|
||||
<div className="mt-2 flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void recoverSale(sale)}
|
||||
disabled={recoveringSaleId === sale.id}
|
||||
className="flex-1 rounded-lg bg-[#2D6A4F] px-2 py-1.5 text-xs font-bold text-white disabled:opacity-50"
|
||||
>
|
||||
{recoveringSaleId === sale.id ? 'Cargando…' : 'Recuperar'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteConfirmFor(sale)}
|
||||
className="flex items-center justify-center rounded-lg bg-red-50 px-2 py-1.5 text-xs font-bold text-red-600 hover:bg-red-100"
|
||||
aria-label="Eliminar venta pendiente"
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -1350,6 +1412,41 @@ export default function RegisterPage() {
|
||||
</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">
|
||||
<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">¿Eliminar venta?</h2>
|
||||
<p className="mb-1 text-sm text-gray-600">
|
||||
Ticket {deleteConfirmFor.receiptNumber ?? deleteConfirmFor.id.slice(0, 8)}
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
Total {formatPrice(deleteConfirmFor.totalCents)} · pendiente {formatPrice(deleteConfirmFor.outstandingCents)}
|
||||
</p>
|
||||
<p className="mb-4 text-xs text-amber-600">
|
||||
⚠️ Se liberará el stock reservado y no se podrá deshacer.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteConfirmFor(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={() => void deleteSale(deleteConfirmFor)}
|
||||
className="flex-1 rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700"
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="mt-2 text-center text-xs text-red-600">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* POS-FIX-3/POS-FIX-5: close session — 2-step: PIN then cash */}
|
||||
{showCloseSession && (
|
||||
(config?.terminal?.settings?.closeSessionRequiresPin && !closingPinStep) ? (
|
||||
|
||||
@@ -93,6 +93,12 @@ export const posApi = {
|
||||
const tail = qs.toString();
|
||||
return apiFetch<T>(`/pos/sales${tail ? `?${tail}` : ''}`);
|
||||
},
|
||||
/** Delete a pending parked sale and restore inventory. */
|
||||
deleteSale: <T>(orderId: string) =>
|
||||
apiFetch<T>(`/pos/sales/${encodeURIComponent(orderId)}`, { method: 'DELETE' }),
|
||||
/** List order items (used for recovering a parked sale). */
|
||||
listOrderItems: <T>(orderId: string) =>
|
||||
apiFetch<T>(`/pos/sales/${encodeURIComponent(orderId)}/items`),
|
||||
/** Email the immutable generated receipt. */
|
||||
emailReceipt: <T>(orderId: string, email: string) =>
|
||||
apiFetch<T>(`/pos/sales/${encodeURIComponent(orderId)}/receipt/email`, {
|
||||
|
||||
@@ -88,3 +88,14 @@ export interface PosPendingSale {
|
||||
cashierEmail?: string | null;
|
||||
receiptNumber?: string | null;
|
||||
}
|
||||
|
||||
export interface RecoveredOrderItem {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
quantity: number;
|
||||
returnedQuantity: number;
|
||||
freeItem: boolean;
|
||||
unitPriceCents: number;
|
||||
discountCents: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user