feat(pos): add point of sale UI
- Add ProductGrid component with category filters and search - Add Cart component with quantity controls and totals - Add PaymentDialog with cash/card/transfer payment methods - Add CashRegisterStatus for opening/closing register - Add POS page with 70/30 layout (products/cart) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
199
apps/web/components/pos/cart.tsx
Normal file
199
apps/web/components/pos/cart.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatCurrency } from "@/lib/utils";
|
||||
|
||||
export interface CartItem {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
stock: number;
|
||||
trackStock: boolean;
|
||||
}
|
||||
|
||||
interface CartProps {
|
||||
items: CartItem[];
|
||||
onUpdateQuantity: (productId: string, quantity: number) => void;
|
||||
onRemoveItem: (productId: string) => void;
|
||||
onClearCart: () => void;
|
||||
onCheckout: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function Cart({
|
||||
items,
|
||||
onUpdateQuantity,
|
||||
onRemoveItem,
|
||||
onClearCart,
|
||||
onCheckout,
|
||||
disabled = false,
|
||||
}: CartProps) {
|
||||
const subtotal = items.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0
|
||||
);
|
||||
|
||||
const handleIncrement = (item: CartItem) => {
|
||||
// Check stock if tracking
|
||||
if (item.trackStock && item.quantity >= item.stock) {
|
||||
return;
|
||||
}
|
||||
onUpdateQuantity(item.id, item.quantity + 1);
|
||||
};
|
||||
|
||||
const handleDecrement = (item: CartItem) => {
|
||||
if (item.quantity > 1) {
|
||||
onUpdateQuantity(item.id, item.quantity - 1);
|
||||
} else {
|
||||
onRemoveItem(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Carrito</CardTitle>
|
||||
{items.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClearCart}
|
||||
disabled={disabled}
|
||||
className="text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
Vaciar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 overflow-y-auto p-0">
|
||||
{items.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-primary-400 p-6">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">🛒</div>
|
||||
<p>El carrito esta vacio</p>
|
||||
<p className="text-sm mt-1">Selecciona productos para agregar</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-primary-100">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-primary-800 text-sm truncate">
|
||||
{item.name}
|
||||
</h4>
|
||||
<p className="text-primary-500 text-xs mt-0.5">
|
||||
{formatCurrency(item.price)} c/u
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-400 hover:text-red-500 hover:bg-red-50 shrink-0"
|
||||
onClick={() => onRemoveItem(item.id)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.519.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleDecrement(item)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path d="M6.75 9.25a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-6.5z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<span className="w-10 text-center font-medium text-primary-800">
|
||||
{item.quantity}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleIncrement(item)}
|
||||
disabled={
|
||||
disabled ||
|
||||
(item.trackStock && item.quantity >= item.stock)
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path d="M10.75 6.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Subtotal */}
|
||||
<span className="font-bold text-primary-800">
|
||||
{formatCurrency(item.price * item.quantity)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stock Warning */}
|
||||
{item.trackStock && item.quantity >= item.stock && (
|
||||
<p className="text-xs text-amber-600 mt-2">
|
||||
Stock maximo alcanzado
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex-col gap-4 border-t p-4">
|
||||
{/* Total */}
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<span className="text-lg font-medium text-primary-600">Total:</span>
|
||||
<span className="text-2xl font-bold text-primary-800">
|
||||
{formatCurrency(subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Checkout Button */}
|
||||
<Button
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={items.length === 0 || disabled}
|
||||
onClick={onCheckout}
|
||||
>
|
||||
Cobrar
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user