diff --git a/packages/admin/src/api/pos.ts b/packages/admin/src/api/pos.ts index f71623a..d3f421c 100644 --- a/packages/admin/src/api/pos.ts +++ b/packages/admin/src/api/pos.ts @@ -90,9 +90,20 @@ export interface Product { // --- Query Keys --- +export interface DrawerAdjustment { + id: string + drawerSessionId: string + type: string + amount: string + reason: string + createdBy: string + createdAt: string +} + export const posKeys = { transaction: (id: string) => ['pos', 'transaction', id] as const, drawer: (locationId: string) => ['pos', 'drawer', locationId] as const, + drawerAdjustments: (id: string) => ['pos', 'drawer-adjustments', id] as const, products: (search: string) => ['pos', 'products', search] as const, discounts: ['pos', 'discounts'] as const, } @@ -160,4 +171,10 @@ export const posMutations = { lookupUpc: (upc: string) => api.get(`/v1/products/lookup/upc/${upc}`), + + addAdjustment: (drawerId: string, data: { type: string; amount: number; reason: string }) => + api.post(`/v1/drawer/${drawerId}/adjustments`, data), + + getAdjustments: (drawerId: string) => + api.get<{ data: DrawerAdjustment[] }>(`/v1/drawer/${drawerId}/adjustments`), } diff --git a/packages/admin/src/components/pos/pos-drawer-dialog.tsx b/packages/admin/src/components/pos/pos-drawer-dialog.tsx index 7ad2425..e04f8f7 100644 --- a/packages/admin/src/components/pos/pos-drawer-dialog.tsx +++ b/packages/admin/src/components/pos/pos-drawer-dialog.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { usePOSStore } from '@/stores/pos.store' import { posMutations, posKeys, type DrawerSession } from '@/api/pos' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' @@ -7,6 +7,8 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' +import { Badge } from '@/components/ui/badge' +import { ArrowDownToLine, ArrowUpFromLine } from 'lucide-react' import { toast } from 'sonner' interface POSDrawerDialogProps { @@ -23,6 +25,17 @@ export function POSDrawerDialog({ open, onOpenChange, drawer }: POSDrawerDialogP const [openingBalance, setOpeningBalance] = useState('200') const [closingBalance, setClosingBalance] = useState('') const [notes, setNotes] = useState('') + const [adjustView, setAdjustView] = useState<'cash_in' | 'cash_out' | null>(null) + const [adjAmount, setAdjAmount] = useState('') + const [adjReason, setAdjReason] = useState('') + + // Fetch adjustments for open drawer + const { data: adjData } = useQuery({ + queryKey: posKeys.drawerAdjustments(drawer?.id ?? ''), + queryFn: () => posMutations.getAdjustments(drawer!.id), + enabled: !!drawer?.id && isOpen, + }) + const adjustments = adjData?.data ?? [] const openMutation = useMutation({ mutationFn: () => @@ -59,11 +72,78 @@ export function POSDrawerDialog({ open, onOpenChange, drawer }: POSDrawerDialogP onError: (err) => toast.error(err.message), }) + const adjustMutation = useMutation({ + mutationFn: () => + posMutations.addAdjustment(drawer!.id, { + type: adjustView!, + amount: parseFloat(adjAmount) || 0, + reason: adjReason, + }), + onSuccess: (adj) => { + queryClient.invalidateQueries({ queryKey: posKeys.drawerAdjustments(drawer!.id) }) + toast.success(`${adj.type === 'cash_in' ? 'Cash added' : 'Cash removed'}: $${parseFloat(adj.amount).toFixed(2)}`) + setAdjustView(null) + setAdjAmount('') + setAdjReason('') + }, + onError: (err) => toast.error(err.message), + }) + + // Adjustment entry view + if (adjustView && isOpen) { + const isCashIn = adjustView === 'cash_in' + return ( + + + + {isCashIn ? 'Cash In' : 'Cash Out'} + +
+
+ + setAdjAmount(e.target.value)} + placeholder="0.00" + className="h-11 text-lg" + autoFocus + /> +
+
+ + setAdjReason(e.target.value)} + placeholder={isCashIn ? 'e.g. Extra change' : 'e.g. Bank deposit'} + className="h-11" + /> +
+
+ + +
+
+
+
+ ) + } + return ( - {isOpen ? 'Close Drawer' : 'Open Drawer'} + {isOpen ? 'Drawer' : 'Open Drawer'} {isOpen ? ( @@ -78,7 +158,53 @@ export function POSDrawerDialog({ open, onOpenChange, drawer }: POSDrawerDialogP {new Date(drawer!.openedAt).toLocaleTimeString()} + + {/* Cash In / Cash Out buttons */} +
+ + +
+ + {/* Adjustment history */} + {adjustments.length > 0 && ( + <> + +
+ Adjustments + {adjustments.map((adj) => ( +
+
+ + {adj.type === 'cash_in' ? 'IN' : 'OUT'} + + {adj.reason} +
+ + {adj.type === 'cash_in' ? '+' : '-'}${parseFloat(adj.amount).toFixed(2)} + +
+ ))} +
+ + )} + + + {/* Close drawer */}
setClosingBalance(e.target.value)} placeholder="Count the cash in the drawer" className="h-11 text-lg" - autoFocus />
diff --git a/packages/admin/src/components/pos/pos-top-bar.tsx b/packages/admin/src/components/pos/pos-top-bar.tsx index 4fa48a9..21de3d6 100644 --- a/packages/admin/src/components/pos/pos-top-bar.tsx +++ b/packages/admin/src/components/pos/pos-top-bar.tsx @@ -64,9 +64,7 @@ export function POSTopBar({ locations, locationId, onLocationChange, drawer }: P > {drawerOpen ? ( - - Drawer Open — ${parseFloat(drawer!.openingBalance).toFixed(2)} - + Drawer Open ) : ( Drawer Closed )}