From bd3a25aa1c30ff1cd5d4a16726c2f4ae2804eee0 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 19:29:37 +0000 Subject: [PATCH 1/3] feat: add POS register screen with full-screen touch-optimized layout Standalone register at /pos bypassing the admin sidebar layout: - Two-panel layout: product search/grid (60%) + cart/payment (40%) - Product search with barcode scan support (UPC lookup on Enter) - Custom item entry dialog for ad-hoc items - Cart with line items, tax, totals, and remove-item support - Payment dialogs: cash (quick amounts + change calc), card, check - Drawer open/close with balance reconciliation and over/short - Auto-creates pending transaction on first item added - POS link added to admin sidebar nav (module-gated) - Zustand store for POS session state, React Query for server data Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/admin/src/api/pos.ts | 163 +++++++++++ .../src/components/pos/pos-cart-panel.tsx | 179 ++++++++++++ .../src/components/pos/pos-drawer-dialog.tsx | 139 +++++++++ .../src/components/pos/pos-item-panel.tsx | 264 ++++++++++++++++++ .../src/components/pos/pos-payment-dialog.tsx | 202 ++++++++++++++ .../admin/src/components/pos/pos-register.tsx | 72 +++++ .../admin/src/components/pos/pos-top-bar.tsx | 91 ++++++ packages/admin/src/routeTree.gen.ts | 21 ++ packages/admin/src/routes/_authenticated.tsx | 8 +- packages/admin/src/routes/pos.tsx | 21 ++ packages/admin/src/stores/pos.store.ts | 21 ++ 11 files changed, 1180 insertions(+), 1 deletion(-) create mode 100644 packages/admin/src/api/pos.ts create mode 100644 packages/admin/src/components/pos/pos-cart-panel.tsx create mode 100644 packages/admin/src/components/pos/pos-drawer-dialog.tsx create mode 100644 packages/admin/src/components/pos/pos-item-panel.tsx create mode 100644 packages/admin/src/components/pos/pos-payment-dialog.tsx create mode 100644 packages/admin/src/components/pos/pos-register.tsx create mode 100644 packages/admin/src/components/pos/pos-top-bar.tsx create mode 100644 packages/admin/src/routes/pos.tsx create mode 100644 packages/admin/src/stores/pos.store.ts diff --git a/packages/admin/src/api/pos.ts b/packages/admin/src/api/pos.ts new file mode 100644 index 0000000..3c525aa --- /dev/null +++ b/packages/admin/src/api/pos.ts @@ -0,0 +1,163 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' + +// --- Types --- + +export interface Transaction { + id: string + locationId: string | null + transactionNumber: string + accountId: string | null + repairTicketId: string | null + repairBatchId: string | null + transactionType: string + status: string + subtotal: string + discountTotal: string + taxTotal: string + total: string + paymentMethod: string | null + amountTendered: string | null + changeGiven: string | null + checkNumber: string | null + roundingAdjustment: string + taxExempt: boolean + taxExemptReason: string | null + processedBy: string + drawerSessionId: string | null + notes: string | null + completedAt: string | null + createdAt: string + updatedAt: string + lineItems?: TransactionLineItem[] +} + +export interface TransactionLineItem { + id: string + transactionId: string + productId: string | null + inventoryUnitId: string | null + description: string + qty: number + unitPrice: string + discountAmount: string + discountReason: string | null + taxRate: string + taxAmount: string + lineTotal: string + createdAt: string +} + +export interface DrawerSession { + id: string + locationId: string | null + openedBy: string + closedBy: string | null + openingBalance: string + closingBalance: string | null + expectedBalance: string | null + overShort: string | null + denominations: Record | null + status: string + notes: string | null + openedAt: string + closedAt: string | null +} + +export interface Discount { + id: string + name: string + discountType: string + discountValue: string + appliesTo: string + requiresApprovalAbove: string | null + isActive: boolean +} + +export interface Product { + id: string + name: string + sku: string | null + upc: string | null + description: string | null + sellingPrice: string | null + costPrice: string | null + qtyOnHand: number | null + taxCategory: string + isSerialized: boolean + isActive: boolean +} + +// --- Query Keys --- + +export const posKeys = { + transaction: (id: string) => ['pos', 'transaction', id] as const, + drawer: (locationId: string) => ['pos', 'drawer', locationId] as const, + products: (search: string) => ['pos', 'products', search] as const, + discounts: ['pos', 'discounts'] as const, +} + +// --- Query Options --- + +export function transactionOptions(id: string | null) { + return queryOptions({ + queryKey: posKeys.transaction(id ?? ''), + queryFn: () => api.get(`/v1/transactions/${id}`), + enabled: !!id, + }) +} + +export function currentDrawerOptions(locationId: string | null) { + return queryOptions({ + queryKey: posKeys.drawer(locationId ?? ''), + queryFn: () => api.get('/v1/drawer/current', { locationId }), + enabled: !!locationId, + retry: false, + }) +} + +export function productSearchOptions(search: string) { + return queryOptions({ + queryKey: posKeys.products(search), + queryFn: () => api.get<{ data: Product[]; pagination: any }>('/v1/products', { q: search, limit: 24, isActive: true }), + enabled: search.length >= 1, + }) +} + +export function discountListOptions() { + return queryOptions({ + queryKey: posKeys.discounts, + queryFn: () => api.get('/v1/discounts/all'), + }) +} + +// --- Mutations --- + +export const posMutations = { + createTransaction: (data: { transactionType: string; locationId?: string }) => + api.post('/v1/transactions', data), + + addLineItem: (txnId: string, data: { productId?: string; inventoryUnitId?: string; description: string; qty: number; unitPrice: number }) => + api.post(`/v1/transactions/${txnId}/line-items`, data), + + removeLineItem: (txnId: string, lineItemId: string) => + api.del(`/v1/transactions/${txnId}/line-items/${lineItemId}`), + + applyDiscount: (txnId: string, data: { discountId?: string; amount: number; reason: string; lineItemId?: string }) => + api.post(`/v1/transactions/${txnId}/discounts`, data), + + complete: (txnId: string, data: { paymentMethod: string; amountTendered?: number; checkNumber?: string }) => + api.post(`/v1/transactions/${txnId}/complete`, data), + + void: (txnId: string) => + api.post(`/v1/transactions/${txnId}/void`, {}), + + openDrawer: (data: { locationId?: string; openingBalance: number }) => + api.post('/v1/drawer/open', data), + + closeDrawer: (id: string, data: { closingBalance: number; denominations?: Record; notes?: string }) => + api.post(`/v1/drawer/${id}/close`, data), + + lookupUpc: (upc: string) => + api.get(`/v1/products/lookup/upc/${upc}`), +} diff --git a/packages/admin/src/components/pos/pos-cart-panel.tsx b/packages/admin/src/components/pos/pos-cart-panel.tsx new file mode 100644 index 0000000..3f530da --- /dev/null +++ b/packages/admin/src/components/pos/pos-cart-panel.tsx @@ -0,0 +1,179 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { usePOSStore } from '@/stores/pos.store' +import { posMutations, posKeys, type Transaction } from '@/api/pos' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { X, Banknote, CreditCard, FileText, Ban } from 'lucide-react' +import { toast } from 'sonner' +import { useState } from 'react' +import { POSPaymentDialog } from './pos-payment-dialog' + +interface POSCartPanelProps { + transaction: Transaction | null +} + +export function POSCartPanel({ transaction }: POSCartPanelProps) { + const queryClient = useQueryClient() + const { currentTransactionId, setTransaction } = usePOSStore() + const [paymentMethod, setPaymentMethod] = useState(null) + const lineItems = transaction?.lineItems ?? [] + + const removeItemMutation = useMutation({ + mutationFn: (lineItemId: string) => + posMutations.removeLineItem(currentTransactionId!, lineItemId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) + }, + onError: (err) => toast.error(err.message), + }) + + const voidMutation = useMutation({ + mutationFn: () => posMutations.void(currentTransactionId!), + onSuccess: () => { + setTransaction(null) + toast.success('Transaction voided') + }, + onError: (err) => toast.error(err.message), + }) + + const subtotal = parseFloat(transaction?.subtotal ?? '0') + const discountTotal = parseFloat(transaction?.discountTotal ?? '0') + const taxTotal = parseFloat(transaction?.taxTotal ?? '0') + const total = parseFloat(transaction?.total ?? '0') + const hasItems = lineItems.length > 0 + const isPending = transaction?.status === 'pending' + + function handlePaymentComplete() { + setPaymentMethod(null) + setTransaction(null) + } + + return ( +
+ {/* Header */} +
+
+

Current Sale

+ {transaction && ( + + {transaction.transactionNumber} + + )} +
+
+ + {/* Line items */} +
+ {lineItems.length === 0 ? ( +
+ No items yet +
+ ) : ( +
+ {lineItems.map((item) => ( +
+
+

{item.description}

+

+ {item.qty} x ${parseFloat(item.unitPrice).toFixed(2)} + {parseFloat(item.taxAmount) > 0 && ( + tax ${parseFloat(item.taxAmount).toFixed(2)} + )} +

+
+ + ${parseFloat(item.lineTotal).toFixed(2)} + + {isPending && ( + + )} +
+ ))} +
+ )} +
+ + {/* Totals + payment */} +
+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+ {discountTotal > 0 && ( +
+ Discount + -${discountTotal.toFixed(2)} +
+ )} +
+ Tax + ${taxTotal.toFixed(2)} +
+ +
+ Total + ${total.toFixed(2)} +
+
+ + {/* Payment buttons */} +
+ + + + +
+
+ + {/* Payment dialog */} + {paymentMethod && transaction && ( + { if (!open) setPaymentMethod(null) }} + paymentMethod={paymentMethod} + transaction={transaction} + onComplete={handlePaymentComplete} + /> + )} +
+ ) +} diff --git a/packages/admin/src/components/pos/pos-drawer-dialog.tsx b/packages/admin/src/components/pos/pos-drawer-dialog.tsx new file mode 100644 index 0000000..7ad2425 --- /dev/null +++ b/packages/admin/src/components/pos/pos-drawer-dialog.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import { 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' +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 { toast } from 'sonner' + +interface POSDrawerDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + drawer: DrawerSession | null +} + +export function POSDrawerDialog({ open, onOpenChange, drawer }: POSDrawerDialogProps) { + const queryClient = useQueryClient() + const { locationId, setDrawerSession } = usePOSStore() + const isOpen = drawer?.status === 'open' + + const [openingBalance, setOpeningBalance] = useState('200') + const [closingBalance, setClosingBalance] = useState('') + const [notes, setNotes] = useState('') + + const openMutation = useMutation({ + mutationFn: () => + posMutations.openDrawer({ + locationId: locationId ?? undefined, + openingBalance: parseFloat(openingBalance) || 0, + }), + onSuccess: (session) => { + setDrawerSession(session.id) + queryClient.invalidateQueries({ queryKey: posKeys.drawer(locationId ?? '') }) + toast.success('Drawer opened') + onOpenChange(false) + }, + onError: (err) => toast.error(err.message), + }) + + const closeMutation = useMutation({ + mutationFn: () => + posMutations.closeDrawer(drawer!.id, { + closingBalance: parseFloat(closingBalance) || 0, + notes: notes || undefined, + }), + onSuccess: (session) => { + setDrawerSession(null) + queryClient.invalidateQueries({ queryKey: posKeys.drawer(locationId ?? '') }) + const overShort = parseFloat(session.overShort ?? '0') + if (Math.abs(overShort) < 0.01) { + toast.success('Drawer closed - balanced') + } else { + toast.warning(`Drawer closed - ${overShort > 0 ? 'over' : 'short'} $${Math.abs(overShort).toFixed(2)}`) + } + onOpenChange(false) + }, + onError: (err) => toast.error(err.message), + }) + + return ( + + + + {isOpen ? 'Close Drawer' : 'Open Drawer'} + + + {isOpen ? ( +
+
+
+ Opening Balance + ${parseFloat(drawer!.openingBalance).toFixed(2)} +
+
+ Opened + {new Date(drawer!.openedAt).toLocaleTimeString()} +
+
+ +
+ + setClosingBalance(e.target.value)} + placeholder="Count the cash in the drawer" + className="h-11 text-lg" + autoFocus + /> +
+
+ + setNotes(e.target.value)} + placeholder="End of shift notes" + className="h-11" + /> +
+ +
+ ) : ( +
+
+ + setOpeningBalance(e.target.value)} + placeholder="Starting cash amount" + className="h-11 text-lg" + autoFocus + /> +
+ +
+ )} +
+
+ ) +} diff --git a/packages/admin/src/components/pos/pos-item-panel.tsx b/packages/admin/src/components/pos/pos-item-panel.tsx new file mode 100644 index 0000000..bf4bb7f --- /dev/null +++ b/packages/admin/src/components/pos/pos-item-panel.tsx @@ -0,0 +1,264 @@ +import { useState, useRef, useCallback } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { usePOSStore } from '@/stores/pos.store' +import { productSearchOptions, posMutations, posKeys, type Transaction, type Product } from '@/api/pos' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Label } from '@/components/ui/label' +import { Skeleton } from '@/components/ui/skeleton' +import { Search, ScanBarcode, Wrench, PenLine } from 'lucide-react' +import { toast } from 'sonner' + +interface POSItemPanelProps { + transaction: Transaction | null +} + +export function POSItemPanel({ transaction }: POSItemPanelProps) { + const queryClient = useQueryClient() + const { currentTransactionId, setTransaction, locationId } = usePOSStore() + const [search, setSearch] = useState('') + const [customOpen, setCustomOpen] = useState(false) + const [customDesc, setCustomDesc] = useState('') + const [customPrice, setCustomPrice] = useState('') + const [customQty, setCustomQty] = useState('1') + const searchRef = useRef(null) + + // Debounced product search + const { data: productsData, isLoading: searchLoading } = useQuery({ + ...productSearchOptions(search), + enabled: search.length >= 1, + }) + const products = productsData?.data ?? [] + + // Add line item mutation + const addItemMutation = useMutation({ + mutationFn: async (product: Product) => { + let txnId = currentTransactionId + // Auto-create transaction if none exists + if (!txnId) { + const txn = await posMutations.createTransaction({ + transactionType: 'sale', + locationId: locationId ?? undefined, + }) + txnId = txn.id + setTransaction(txnId) + } + return posMutations.addLineItem(txnId, { + productId: product.id, + description: product.name, + qty: 1, + unitPrice: parseFloat(product.sellingPrice ?? '0'), + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) + }, + onError: (err) => toast.error(err.message), + }) + + // Custom item mutation + const addCustomMutation = useMutation({ + mutationFn: async () => { + let txnId = currentTransactionId + if (!txnId) { + const txn = await posMutations.createTransaction({ + transactionType: 'sale', + locationId: locationId ?? undefined, + }) + txnId = txn.id + setTransaction(txnId) + } + return posMutations.addLineItem(txnId, { + description: customDesc, + qty: parseInt(customQty) || 1, + unitPrice: parseFloat(customPrice) || 0, + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) + setCustomOpen(false) + setCustomDesc('') + setCustomPrice('') + setCustomQty('1') + }, + onError: (err) => toast.error(err.message), + }) + + // UPC scan + const scanMutation = useMutation({ + mutationFn: async (upc: string) => { + const product = await posMutations.lookupUpc(upc) + let txnId = currentTransactionId + if (!txnId) { + const txn = await posMutations.createTransaction({ + transactionType: 'sale', + locationId: locationId ?? undefined, + }) + txnId = txn.id + setTransaction(txnId) + } + return posMutations.addLineItem(txnId, { + productId: product.id, + description: product.name, + qty: 1, + unitPrice: parseFloat(product.sellingPrice ?? '0'), + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) + setSearch('') + toast.success('Item scanned') + }, + onError: (err) => toast.error(err.message), + }) + + const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => { + // Barcode scanners typically send Enter after the code + if (e.key === 'Enter' && search.length >= 6) { + // Looks like a UPC — try scanning + scanMutation.mutate(search) + } + }, [search, scanMutation]) + + return ( +
+ {/* Search bar */} +
+
+ + setSearch(e.target.value)} + onKeyDown={handleSearchKeyDown} + placeholder="Search products or scan barcode..." + className="pl-10 h-11 text-base" + autoFocus + /> +
+
+ + {/* Product grid */} +
+ {searchLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : products.length > 0 ? ( +
+ {products.map((product) => ( + + ))} +
+ ) : search.length >= 1 ? ( +
+ No products found for "{search}" +
+ ) : ( +
+ Search for products to add to the sale +
+ )} +
+ + {/* Quick action buttons */} +
+ + + +
+ + {/* Custom item dialog */} + + + + Add Custom Item + +
{ e.preventDefault(); addCustomMutation.mutate() }} + className="space-y-4" + > +
+ + setCustomDesc(e.target.value)} + placeholder="Item description" + required + className="h-11" + /> +
+
+
+ + setCustomPrice(e.target.value)} + placeholder="0.00" + required + className="h-11" + /> +
+
+ + setCustomQty(e.target.value)} + className="h-11" + /> +
+
+ +
+
+
+
+ ) +} diff --git a/packages/admin/src/components/pos/pos-payment-dialog.tsx b/packages/admin/src/components/pos/pos-payment-dialog.tsx new file mode 100644 index 0000000..06aa9f3 --- /dev/null +++ b/packages/admin/src/components/pos/pos-payment-dialog.tsx @@ -0,0 +1,202 @@ +import { useState } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { usePOSStore } from '@/stores/pos.store' +import { posMutations, posKeys, type Transaction } from '@/api/pos' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +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 { CheckCircle } from 'lucide-react' +import { toast } from 'sonner' + +interface POSPaymentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + paymentMethod: string + transaction: Transaction + onComplete: () => void +} + +export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transaction, onComplete }: POSPaymentDialogProps) { + const queryClient = useQueryClient() + const { currentTransactionId } = usePOSStore() + const total = parseFloat(transaction.total) + const [amountTendered, setAmountTendered] = useState('') + const [checkNumber, setCheckNumber] = useState('') + const [completed, setCompleted] = useState(false) + const [result, setResult] = useState(null) + + const completeMutation = useMutation({ + mutationFn: () => { + const data: { paymentMethod: string; amountTendered?: number; checkNumber?: string } = { + paymentMethod, + } + if (paymentMethod === 'cash') { + data.amountTendered = parseFloat(amountTendered) || 0 + } + if (paymentMethod === 'check') { + data.checkNumber = checkNumber || undefined + } + return posMutations.complete(currentTransactionId!, data) + }, + onSuccess: (txn) => { + queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) + setResult(txn) + setCompleted(true) + }, + onError: (err) => toast.error(err.message), + }) + + const tenderedAmount = parseFloat(amountTendered) || 0 + const changeDue = paymentMethod === 'cash' ? Math.max(0, tenderedAmount - total) : 0 + const canComplete = paymentMethod === 'cash' + ? tenderedAmount >= total + : true + + function handleDone() { + onComplete() + onOpenChange(false) + } + + const QUICK_AMOUNTS = [1, 5, 10, 20, 50, 100] + + if (completed && result) { + const changeGiven = parseFloat(result.changeGiven ?? '0') + const roundingAdj = parseFloat(result.roundingAdjustment ?? '0') + + return ( + handleDone()}> + +
+ +

Sale Complete

+

{result.transactionNumber}

+ +
+
+ Total + ${parseFloat(result.total).toFixed(2)} +
+ {roundingAdj !== 0 && ( +
+ Rounding + {roundingAdj > 0 ? '+' : ''}{roundingAdj.toFixed(2)} +
+ )} + {paymentMethod === 'cash' && ( + <> +
+ Tendered + ${parseFloat(result.amountTendered ?? '0').toFixed(2)} +
+ {changeGiven > 0 && ( +
+ Change Due + ${changeGiven.toFixed(2)} +
+ )} + + )} +
+ + +
+
+
+ ) + } + + return ( + + + + + {paymentMethod === 'cash' ? 'Cash Payment' : paymentMethod === 'check' ? 'Check Payment' : 'Card Payment'} + + + +
+
+ Total Due + ${total.toFixed(2)} +
+ + + + {paymentMethod === 'cash' && ( + <> +
+ + setAmountTendered(e.target.value)} + placeholder="0.00" + className="h-12 text-xl text-right font-mono" + autoFocus + /> +
+
+ {QUICK_AMOUNTS.map((amt) => ( + + ))} +
+ + {tenderedAmount >= total && ( +
+ Change + ${changeDue.toFixed(2)} +
+ )} + + )} + + {paymentMethod === 'check' && ( +
+ + setCheckNumber(e.target.value)} + placeholder="Check #" + className="h-11" + autoFocus + /> +
+ )} + + {paymentMethod === 'card_present' && ( +

+ Process card payment on terminal, then confirm below. +

+ )} + + +
+
+
+ ) +} diff --git a/packages/admin/src/components/pos/pos-register.tsx b/packages/admin/src/components/pos/pos-register.tsx new file mode 100644 index 0000000..92cc852 --- /dev/null +++ b/packages/admin/src/components/pos/pos-register.tsx @@ -0,0 +1,72 @@ +import { useEffect } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import { usePOSStore } from '@/stores/pos.store' +import { currentDrawerOptions, transactionOptions } from '@/api/pos' +import { POSTopBar } from './pos-top-bar' +import { POSItemPanel } from './pos-item-panel' +import { POSCartPanel } from './pos-cart-panel' + +interface Location { + id: string + name: string +} + +function locationsOptions() { + return queryOptions({ + queryKey: ['locations'], + queryFn: () => api.get<{ data: Location[] }>('/v1/locations'), + }) +} + +export function POSRegister() { + const { locationId, setLocation, currentTransactionId, drawerSessionId, setDrawerSession } = usePOSStore() + const queryClient = useQueryClient() + + // Fetch locations + const { data: locationsData } = useQuery(locationsOptions()) + const locations = locationsData?.data ?? [] + + // Auto-select first location + useEffect(() => { + if (!locationId && locations.length > 0) { + setLocation(locations[0].id) + } + }, [locationId, locations, setLocation]) + + // Fetch current drawer for selected location + const { data: drawer } = useQuery({ + ...currentDrawerOptions(locationId), + retry: false, + }) + + // Sync drawer session ID + useEffect(() => { + if (drawer?.id && drawer.status === 'open') { + setDrawerSession(drawer.id) + } + }, [drawer, setDrawerSession]) + + // Fetch current transaction + const { data: transaction } = useQuery(transactionOptions(currentTransactionId)) + + return ( +
+ +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/packages/admin/src/components/pos/pos-top-bar.tsx b/packages/admin/src/components/pos/pos-top-bar.tsx new file mode 100644 index 0000000..250308b --- /dev/null +++ b/packages/admin/src/components/pos/pos-top-bar.tsx @@ -0,0 +1,91 @@ +import { Link, useRouter } from '@tanstack/react-router' +import { useAuthStore } from '@/stores/auth.store' +import { usePOSStore } from '@/stores/pos.store' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { ArrowLeft, LogOut, DollarSign } from 'lucide-react' +import type { DrawerSession } from '@/api/pos' +import { useState } from 'react' +import { POSDrawerDialog } from './pos-drawer-dialog' + +interface POSTopBarProps { + locations: { id: string; name: string }[] + locationId: string | null + onLocationChange: (id: string) => void + drawer: DrawerSession | null +} + +export function POSTopBar({ locations, locationId, onLocationChange, drawer }: POSTopBarProps) { + const router = useRouter() + const user = useAuthStore((s) => s.user) + const logout = useAuthStore((s) => s.logout) + const [drawerDialogOpen, setDrawerDialogOpen] = useState(false) + + const drawerOpen = drawer?.status === 'open' + + function handleLogout() { + logout() + router.navigate({ to: '/login', replace: true }) + } + + return ( + <> +
+ {/* Left: back + location */} +
+ + + Admin + + + {locations.length > 1 ? ( + + ) : locations.length === 1 ? ( + {locations[0].name} + ) : null} +
+ + {/* Center: drawer status */} + + + {/* Right: user + logout */} +
+ {user?.firstName} + +
+
+ + + + ) +} diff --git a/packages/admin/src/routeTree.gen.ts b/packages/admin/src/routeTree.gen.ts index 56f3fee..7be384e 100644 --- a/packages/admin/src/routeTree.gen.ts +++ b/packages/admin/src/routeTree.gen.ts @@ -9,6 +9,7 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as PosRouteImport } from './routes/pos' import { Route as LoginRouteImport } from './routes/login' import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index' @@ -56,6 +57,11 @@ import { Route as AuthenticatedAccountsAccountIdMembersRouteImport } from './rou import { Route as AuthenticatedAccountsAccountIdEnrollmentsRouteImport } from './routes/_authenticated/accounts/$accountId/enrollments' import { Route as AuthenticatedLessonsScheduleInstructorsInstructorIdRouteImport } from './routes/_authenticated/lessons/schedule/instructors/$instructorId' +const PosRoute = PosRouteImport.update({ + id: '/pos', + path: '/pos', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -323,6 +329,7 @@ const AuthenticatedLessonsScheduleInstructorsInstructorIdRoute = export interface FileRoutesByFullPath { '/': typeof AuthenticatedIndexRoute '/login': typeof LoginRoute + '/pos': typeof PosRoute '/help': typeof AuthenticatedHelpRoute '/profile': typeof AuthenticatedProfileRoute '/settings': typeof AuthenticatedSettingsRoute @@ -369,6 +376,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/login': typeof LoginRoute + '/pos': typeof PosRoute '/help': typeof AuthenticatedHelpRoute '/profile': typeof AuthenticatedProfileRoute '/settings': typeof AuthenticatedSettingsRoute @@ -417,6 +425,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_authenticated': typeof AuthenticatedRouteWithChildren '/login': typeof LoginRoute + '/pos': typeof PosRoute '/_authenticated/help': typeof AuthenticatedHelpRoute '/_authenticated/profile': typeof AuthenticatedProfileRoute '/_authenticated/settings': typeof AuthenticatedSettingsRoute @@ -467,6 +476,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/login' + | '/pos' | '/help' | '/profile' | '/settings' @@ -513,6 +523,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/login' + | '/pos' | '/help' | '/profile' | '/settings' @@ -560,6 +571,7 @@ export interface FileRouteTypes { | '__root__' | '/_authenticated' | '/login' + | '/pos' | '/_authenticated/help' | '/_authenticated/profile' | '/_authenticated/settings' @@ -609,10 +621,18 @@ export interface FileRouteTypes { export interface RootRouteChildren { AuthenticatedRoute: typeof AuthenticatedRouteWithChildren LoginRoute: typeof LoginRoute + PosRoute: typeof PosRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/pos': { + id: '/pos' + path: '/pos' + fullPath: '/pos' + preLoaderRoute: typeof PosRouteImport + parentRoute: typeof rootRouteImport + } '/login': { id: '/login' path: '/login' @@ -1069,6 +1089,7 @@ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { AuthenticatedRoute: AuthenticatedRouteWithChildren, LoginRoute: LoginRoute, + PosRoute: PosRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/admin/src/routes/_authenticated.tsx b/packages/admin/src/routes/_authenticated.tsx index 16adec0..8f342cc 100644 --- a/packages/admin/src/routes/_authenticated.tsx +++ b/packages/admin/src/routes/_authenticated.tsx @@ -8,7 +8,7 @@ import { myPermissionsOptions } from '@/api/rbac' import { moduleListOptions } from '@/api/modules' import { Avatar } from '@/components/shared/avatar-upload' import { Button } from '@/components/ui/button' -import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings, PanelLeftClose, PanelLeft, CalendarDays, GraduationCap, CalendarRange, BookOpen, BookMarked, Package2, Tag, Truck } from 'lucide-react' +import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings, PanelLeftClose, PanelLeft, CalendarDays, GraduationCap, CalendarRange, BookOpen, BookMarked, Package2, Tag, Truck, ShoppingCart } from 'lucide-react' export const Route = createFileRoute('/_authenticated')({ beforeLoad: () => { @@ -145,6 +145,7 @@ function AuthenticatedLayout() { const canViewLessons = !permissionsLoaded || hasPermission('lessons.view') const canViewInventory = !permissionsLoaded || hasPermission('inventory.view') const canViewUsers = !permissionsLoaded || hasPermission('users.view') + const canViewPOS = !permissionsLoaded || hasPermission('pos.view') const [collapsed, setCollapsed] = useState(false) @@ -173,6 +174,11 @@ function AuthenticatedLayout() { {/* Scrollable nav links */}
+ {isModuleEnabled('pos') && canViewPOS && ( +
+ } label="Point of Sale" collapsed={collapsed} /> +
+ )} {canViewAccounts && ( } label="Accounts" collapsed={collapsed} /> diff --git a/packages/admin/src/routes/pos.tsx b/packages/admin/src/routes/pos.tsx new file mode 100644 index 0000000..600eddd --- /dev/null +++ b/packages/admin/src/routes/pos.tsx @@ -0,0 +1,21 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' +import { useAuthStore } from '@/stores/auth.store' +import { POSRegister } from '@/components/pos/pos-register' + +export const Route = createFileRoute('/pos')({ + beforeLoad: () => { + const { token } = useAuthStore.getState() + if (!token) { + throw redirect({ to: '/login' }) + } + }, + component: POSPage, +}) + +function POSPage() { + return ( +
+ +
+ ) +} diff --git a/packages/admin/src/stores/pos.store.ts b/packages/admin/src/stores/pos.store.ts new file mode 100644 index 0000000..8e8d565 --- /dev/null +++ b/packages/admin/src/stores/pos.store.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand' + +interface POSState { + currentTransactionId: string | null + locationId: string | null + drawerSessionId: string | null + setTransaction: (id: string | null) => void + setLocation: (id: string) => void + setDrawerSession: (id: string | null) => void + reset: () => void +} + +export const usePOSStore = create((set) => ({ + currentTransactionId: null, + locationId: null, + drawerSessionId: null, + setTransaction: (id) => set({ currentTransactionId: id }), + setLocation: (id) => set({ locationId: id }), + setDrawerSession: (id) => set({ drawerSessionId: id }), + reset: () => set({ currentTransactionId: null }), +})) From 1673e18fe803b2a49d0f879c9f3855f8c391ef01 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 19:54:07 +0000 Subject: [PATCH 2/3] fix: require open drawer to complete transactions, fix product price field - Backend enforces open drawer at location before completing any transaction - Frontend disables payment buttons when drawer is closed with warning message - Fix product price field name (price, not sellingPrice) in POS API types - Fix seed UUIDs to use valid UUID v4 format (version nibble must be 1-8) - Fix Vite allowedHosts for dev.lunarfront.tech access - Add e2e test for drawer enforcement (39 POS tests now pass) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/admin/src/api/pos.ts | 2 +- .../src/components/pos/pos-cart-panel.tsx | 78 ++++++++++--------- .../src/components/pos/pos-item-panel.tsx | 12 +-- packages/admin/vite.config.ts | 1 + packages/backend/api-tests/suites/pos.ts | 57 ++++++++++++++ packages/backend/src/db/seeds/dev-seed.ts | 4 +- .../backend/src/db/seeds/music-store-seed.ts | 2 +- .../src/services/transaction.service.ts | 13 ++++ 8 files changed, 125 insertions(+), 44 deletions(-) diff --git a/packages/admin/src/api/pos.ts b/packages/admin/src/api/pos.ts index 3c525aa..9a48b95 100644 --- a/packages/admin/src/api/pos.ts +++ b/packages/admin/src/api/pos.ts @@ -80,7 +80,7 @@ export interface Product { sku: string | null upc: string | null description: string | null - sellingPrice: string | null + price: string | null costPrice: string | null qtyOnHand: number | null taxCategory: string diff --git a/packages/admin/src/components/pos/pos-cart-panel.tsx b/packages/admin/src/components/pos/pos-cart-panel.tsx index 3f530da..36caad4 100644 --- a/packages/admin/src/components/pos/pos-cart-panel.tsx +++ b/packages/admin/src/components/pos/pos-cart-panel.tsx @@ -18,6 +18,9 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) { const [paymentMethod, setPaymentMethod] = useState(null) const lineItems = transaction?.lineItems ?? [] + const drawerSessionId = usePOSStore((s) => s.drawerSessionId) + const drawerOpen = !!drawerSessionId + const removeItemMutation = useMutation({ mutationFn: (lineItemId: string) => posMutations.removeLineItem(currentTransactionId!, lineItemId), @@ -126,41 +129,46 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) {
{/* Payment buttons */} -
- - - - +
+ {!drawerOpen && hasItems && ( +

Open the drawer before accepting payment

+ )} +
+ + + + +
diff --git a/packages/admin/src/components/pos/pos-item-panel.tsx b/packages/admin/src/components/pos/pos-item-panel.tsx index bf4bb7f..6b1381b 100644 --- a/packages/admin/src/components/pos/pos-item-panel.tsx +++ b/packages/admin/src/components/pos/pos-item-panel.tsx @@ -48,11 +48,12 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) { productId: product.id, description: product.name, qty: 1, - unitPrice: parseFloat(product.sellingPrice ?? '0'), + unitPrice: parseFloat(product.price ?? '0'), }) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) + const txnId = usePOSStore.getState().currentTransactionId + queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') }) }, onError: (err) => toast.error(err.message), }) @@ -76,7 +77,8 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) { }) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') }) + const txnId = usePOSStore.getState().currentTransactionId + queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') }) setCustomOpen(false) setCustomDesc('') setCustomPrice('') @@ -102,7 +104,7 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) { productId: product.id, description: product.name, qty: 1, - unitPrice: parseFloat(product.sellingPrice ?? '0'), + unitPrice: parseFloat(product.price ?? '0'), }) }, onSuccess: () => { @@ -158,7 +160,7 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) { > {product.name}
- ${parseFloat(product.sellingPrice ?? '0').toFixed(2)} + ${parseFloat(product.price ?? '0').toFixed(2)} {product.sku && {product.sku}}
{product.isSerialized ? ( diff --git a/packages/admin/vite.config.ts b/packages/admin/vite.config.ts index b705dae..a67d2af 100644 --- a/packages/admin/vite.config.ts +++ b/packages/admin/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ }, server: { port: 5173, + allowedHosts: ['dev.lunarfront.tech'], proxy: { '/v1': { target: 'http://localhost:8000', diff --git a/packages/backend/api-tests/suites/pos.ts b/packages/backend/api-tests/suites/pos.ts index 6822d39..dd0d5b7 100644 --- a/packages/backend/api-tests/suites/pos.ts +++ b/packages/backend/api-tests/suites/pos.ts @@ -273,6 +273,36 @@ suite('POS', { tags: ['pos'] }, (t) => { // ─── Complete Transaction ────────────────────────────────────────────────── + t.test('rejects completing transaction without open drawer', { tags: ['transactions', 'complete', 'validation', 'drawer'] }, async () => { + // Ensure no drawer is open at LOCATION_ID + const current = await t.api.get('/v1/drawer/current', { locationId: LOCATION_ID }) + if (current.status === 200 && current.data.id) { + await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 0 }) + } + + const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID }) + await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { + description: 'No Drawer Item', + qty: 1, + unitPrice: 10, + }) + + const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { + paymentMethod: 'cash', + amountTendered: 20, + }) + t.assert.status(res, 400) + + // Void to clean up + await t.api.post(`/v1/transactions/${txn.data.id}/void`) + }) + + // Open a drawer for the remaining complete tests + t.test('opens drawer for complete tests', { tags: ['transactions', 'complete', 'setup'] }, async () => { + const res = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 200 }) + t.assert.status(res, 201) + }) + t.test('completes a cash transaction with change', { tags: ['transactions', 'complete', 'cash'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID }) await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { @@ -427,6 +457,18 @@ suite('POS', { tags: ['pos'] }, (t) => { // ─── Cash Rounding ───────────────────────────────────────────────────────── + // Close the LOCATION_ID drawer and open one at ROUNDING_LOCATION_ID + t.test('setup drawer for rounding tests', { tags: ['transactions', 'rounding', 'setup'] }, async () => { + // Close drawer at LOCATION_ID + const current = await t.api.get('/v1/drawer/current', { locationId: LOCATION_ID }) + if (current.status === 200 && current.data.id) { + await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 200 }) + } + // Open drawer at ROUNDING_LOCATION_ID + const res = await t.api.post('/v1/drawer/open', { locationId: ROUNDING_LOCATION_ID, openingBalance: 200 }) + t.assert.status(res, 201) + }) + t.test('cash rounding adjusts total to nearest nickel', { tags: ['transactions', 'rounding'] }, async () => { // Create transaction at the rounding-enabled location const txn = await t.api.post('/v1/transactions', { @@ -484,6 +526,10 @@ suite('POS', { tags: ['pos'] }, (t) => { }) t.test('no rounding at non-rounding location', { tags: ['transactions', 'rounding'] }, async () => { + // Open drawer at LOCATION_ID for this test + const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 }) + t.assert.status(drawer, 201) + const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID, @@ -500,6 +546,17 @@ suite('POS', { tags: ['pos'] }, (t) => { }) t.assert.status(res, 200) t.assert.equal(parseFloat(res.data.roundingAdjustment), 0) + + // Cleanup + await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 100 }) + }) + + // Close rounding location drawer + t.test('cleanup rounding drawer', { tags: ['transactions', 'rounding', 'setup'] }, async () => { + const current = await t.api.get('/v1/drawer/current', { locationId: ROUNDING_LOCATION_ID }) + if (current.status === 200 && current.data.id) { + await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 200 }) + } }) // ─── Full POS Flow ──────────────────────────────────────────────────────── diff --git a/packages/backend/src/db/seeds/dev-seed.ts b/packages/backend/src/db/seeds/dev-seed.ts index 1fb15ef..8a49f49 100644 --- a/packages/backend/src/db/seeds/dev-seed.ts +++ b/packages/backend/src/db/seeds/dev-seed.ts @@ -7,7 +7,7 @@ import postgres from 'postgres' const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront' -const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001' +const COMPANY_ID = 'a0000000-0000-4000-8000-000000000001' const sql = postgres(DB_URL) @@ -18,7 +18,7 @@ async function seed() { const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}` if (!company) { await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Demo Store', 'America/Chicago')` - await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')` + await sql`INSERT INTO location (id, name, tax_rate, service_tax_rate) VALUES ('a0000000-0000-4000-8000-000000000002', 'Main Store', '0.0825', '0.0825')` console.log(' Created company and location') // Seed RBAC diff --git a/packages/backend/src/db/seeds/music-store-seed.ts b/packages/backend/src/db/seeds/music-store-seed.ts index 4b58f81..e8a3692 100644 --- a/packages/backend/src/db/seeds/music-store-seed.ts +++ b/packages/backend/src/db/seeds/music-store-seed.ts @@ -8,7 +8,7 @@ import postgres from 'postgres' const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront' -const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001' +const COMPANY_ID = 'a0000000-0000-4000-8000-000000000001' const sql = postgres(DB_URL) diff --git a/packages/backend/src/services/transaction.service.ts b/packages/backend/src/services/transaction.service.ts index 7e53298..959666c 100644 --- a/packages/backend/src/services/transaction.service.ts +++ b/packages/backend/src/services/transaction.service.ts @@ -5,6 +5,7 @@ import { transactionLineItems, discountAudits, discounts, + drawerSessions, } from '../db/schema/pos.js' import { products, inventoryUnits } from '../db/schema/inventory.js' import { companies, locations } from '../db/schema/stores.js' @@ -231,6 +232,18 @@ export const TransactionService = { if (!txn) throw new NotFoundError('Transaction') if (txn.status !== 'pending') throw new ConflictError('Transaction is not pending') + // Require an open drawer session at the transaction's location + if (txn.locationId) { + const [openDrawer] = await db + .select({ id: drawerSessions.id }) + .from(drawerSessions) + .where(and(eq(drawerSessions.locationId, txn.locationId), eq(drawerSessions.status, 'open'))) + .limit(1) + if (!openDrawer) { + throw new ValidationError('Cannot complete transaction without an open drawer at this location') + } + } + // Validate cash payment (with optional nickel rounding) let changeGiven: string | undefined let roundingAdjustment = 0 From a0be16d8481851f6e41cd97fd897b77be28f4dcb Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 20:12:17 +0000 Subject: [PATCH 3/3] fix: resolve all frontend lint errors and warnings Replace all `any` types with proper types across 36 files: - TanStack Router search params: `{} as Record` - API response pagination: proper typed interface - DataTable column casts: remove unnecessary `as any` - Function params and event handlers: use specific types - Remove unused imports and variables in POS components Frontend lint now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/admin/src/api/pos.ts | 2 +- .../src/components/pos/pos-item-panel.tsx | 2 +- .../admin/src/components/pos/pos-register.tsx | 5 ++-- .../admin/src/components/pos/pos-top-bar.tsx | 2 +- packages/admin/src/hooks/use-pagination.ts | 6 ++--- packages/admin/src/routes/_authenticated.tsx | 2 +- .../accounts/$accountId/enrollments.tsx | 6 ++--- .../accounts/$accountId/members.tsx | 2 +- .../src/routes/_authenticated/files/index.tsx | 2 +- .../admin/src/routes/_authenticated/index.tsx | 2 +- .../_authenticated/inventory/$productId.tsx | 12 +++++----- .../routes/_authenticated/inventory/index.tsx | 12 +++++----- .../inventory/suppliers/index.tsx | 2 +- .../lessons/enrollments/$enrollmentId.tsx | 24 +++++++++++++------ .../lessons/enrollments/index.tsx | 12 +++++----- .../lessons/enrollments/new.tsx | 6 ++--- .../_authenticated/lessons/plans/$planId.tsx | 2 +- .../_authenticated/lessons/plans/index.tsx | 2 +- .../_authenticated/lessons/schedule/index.tsx | 18 +++++++------- .../schedule/instructors/$instructorId.tsx | 4 ++-- .../lessons/sessions/$sessionId.tsx | 13 ++++++---- .../_authenticated/lessons/sessions/index.tsx | 8 +++---- .../lessons/templates/$templateId.tsx | 10 ++++---- .../lessons/templates/index.tsx | 8 +++---- .../_authenticated/lessons/templates/new.tsx | 6 ++--- .../_authenticated/members/$memberId.tsx | 16 ++++++------- .../routes/_authenticated/members/index.tsx | 4 ++-- .../repair-batches/$batchId.tsx | 6 ++--- .../_authenticated/repair-batches/index.tsx | 2 +- .../_authenticated/repair-batches/new.tsx | 6 ++--- .../_authenticated/repairs/$ticketId.tsx | 2 +- .../routes/_authenticated/repairs/index.tsx | 4 ++-- .../src/routes/_authenticated/repairs/new.tsx | 8 +++---- .../routes/_authenticated/roles/$roleId.tsx | 4 ++-- .../src/routes/_authenticated/roles/new.tsx | 4 ++-- packages/admin/src/routes/login.tsx | 4 ++-- 36 files changed, 122 insertions(+), 108 deletions(-) diff --git a/packages/admin/src/api/pos.ts b/packages/admin/src/api/pos.ts index 9a48b95..f71623a 100644 --- a/packages/admin/src/api/pos.ts +++ b/packages/admin/src/api/pos.ts @@ -119,7 +119,7 @@ export function currentDrawerOptions(locationId: string | null) { export function productSearchOptions(search: string) { return queryOptions({ queryKey: posKeys.products(search), - queryFn: () => api.get<{ data: Product[]; pagination: any }>('/v1/products', { q: search, limit: 24, isActive: true }), + queryFn: () => api.get<{ data: Product[]; pagination: { page: number; limit: number; total: number; totalPages: number } }>('/v1/products', { q: search, limit: 24, isActive: true }), enabled: search.length >= 1, }) } diff --git a/packages/admin/src/components/pos/pos-item-panel.tsx b/packages/admin/src/components/pos/pos-item-panel.tsx index 6b1381b..4949c18 100644 --- a/packages/admin/src/components/pos/pos-item-panel.tsx +++ b/packages/admin/src/components/pos/pos-item-panel.tsx @@ -14,7 +14,7 @@ interface POSItemPanelProps { transaction: Transaction | null } -export function POSItemPanel({ transaction }: POSItemPanelProps) { +export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) { const queryClient = useQueryClient() const { currentTransactionId, setTransaction, locationId } = usePOSStore() const [search, setSearch] = useState('') diff --git a/packages/admin/src/components/pos/pos-register.tsx b/packages/admin/src/components/pos/pos-register.tsx index 92cc852..4bd8645 100644 --- a/packages/admin/src/components/pos/pos-register.tsx +++ b/packages/admin/src/components/pos/pos-register.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query' import { api } from '@/lib/api-client' import { usePOSStore } from '@/stores/pos.store' @@ -21,8 +21,7 @@ function locationsOptions() { } export function POSRegister() { - const { locationId, setLocation, currentTransactionId, drawerSessionId, setDrawerSession } = usePOSStore() - const queryClient = useQueryClient() + const { locationId, setLocation, currentTransactionId, setDrawerSession } = usePOSStore() // Fetch locations const { data: locationsData } = useQuery(locationsOptions()) diff --git a/packages/admin/src/components/pos/pos-top-bar.tsx b/packages/admin/src/components/pos/pos-top-bar.tsx index 250308b..4fa48a9 100644 --- a/packages/admin/src/components/pos/pos-top-bar.tsx +++ b/packages/admin/src/components/pos/pos-top-bar.tsx @@ -1,6 +1,6 @@ import { Link, useRouter } from '@tanstack/react-router' import { useAuthStore } from '@/stores/auth.store' -import { usePOSStore } from '@/stores/pos.store' + import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' diff --git a/packages/admin/src/hooks/use-pagination.ts b/packages/admin/src/hooks/use-pagination.ts index fa5858b..e8ea822 100644 --- a/packages/admin/src/hooks/use-pagination.ts +++ b/packages/admin/src/hooks/use-pagination.ts @@ -23,12 +23,12 @@ export function usePagination() { function setParams(updates: Partial) { navigate({ - search: ((prev: PaginationSearch) => ({ + search: ((prev: Record) => ({ ...prev, ...updates, // Reset to page 1 when search or sort changes - page: updates.q !== undefined || updates.sort !== undefined ? 1 : (updates.page ?? prev.page), - })) as any, + page: updates.q !== undefined || updates.sort !== undefined ? 1 : (updates.page ?? (prev as PaginationSearch).page), + })) as (prev: Record) => Record, replace: true, }) } diff --git a/packages/admin/src/routes/_authenticated.tsx b/packages/admin/src/routes/_authenticated.tsx index 8f342cc..e94bfb2 100644 --- a/packages/admin/src/routes/_authenticated.tsx +++ b/packages/admin/src/routes/_authenticated.tsx @@ -67,7 +67,7 @@ function NavLink({ to, icon, label, collapsed }: { to: string; icon: React.React return ( } className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent" activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }} title={collapsed ? label : undefined} diff --git a/packages/admin/src/routes/_authenticated/accounts/$accountId/enrollments.tsx b/packages/admin/src/routes/_authenticated/accounts/$accountId/enrollments.tsx index b366be5..bb94538 100644 --- a/packages/admin/src/routes/_authenticated/accounts/$accountId/enrollments.tsx +++ b/packages/admin/src/routes/_authenticated/accounts/$accountId/enrollments.tsx @@ -20,7 +20,7 @@ function statusBadge(status: string) { } const columns: Column[] = [ - { key: 'member_name', header: 'Member', sortable: true, render: (e) => {(e as any).memberName ?? e.memberId} }, + { key: 'member_name', header: 'Member', sortable: true, render: (e) => {e.memberName ?? e.memberId} }, { key: 'status', header: 'Status', sortable: true, render: (e) => statusBadge(e.status) }, { key: 'start_date', header: 'Start', sortable: true, render: (e) => <>{new Date(e.startDate + 'T00:00:00').toLocaleDateString()} }, { key: 'rate', header: 'Rate', render: (e) => <>{e.rate ? `$${e.rate}${e.billingInterval ? ` / ${e.billingInterval} ${e.billingUnit}` : ''}` : } }, @@ -41,7 +41,7 @@ function AccountEnrollmentsTab() {

{data?.pagination.total ?? 0} enrollment(s)

{hasPermission('lessons.edit') && ( - )} @@ -55,7 +55,7 @@ function AccountEnrollmentsTab() { total={data?.data?.length ?? 0} onPageChange={() => {}} onSort={() => {}} - onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as any })} + onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/accounts/$accountId/members.tsx b/packages/admin/src/routes/_authenticated/accounts/$accountId/members.tsx index d8b0c92..589bc78 100644 --- a/packages/admin/src/routes/_authenticated/accounts/$accountId/members.tsx +++ b/packages/admin/src/routes/_authenticated/accounts/$accountId/members.tsx @@ -281,7 +281,7 @@ function MembersTab() { navigate({ to: '/members/$memberId', params: { memberId: m.id }, - search: {} as any, + search: {} as Record, })}> Edit diff --git a/packages/admin/src/routes/_authenticated/files/index.tsx b/packages/admin/src/routes/_authenticated/files/index.tsx index 063447b..8c06462 100644 --- a/packages/admin/src/routes/_authenticated/files/index.tsx +++ b/packages/admin/src/routes/_authenticated/files/index.tsx @@ -92,7 +92,7 @@ function FileManagerPage() { }) if (!res.ok) { const err = await res.json().catch(() => ({})) - toast.error(`Upload failed: ${(err as any).error?.message ?? file.name}`) + toast.error(`Upload failed: ${(err as { error?: { message?: string } }).error?.message ?? file.name}`) } } catch { toast.error(`Upload failed: ${file.name}`) diff --git a/packages/admin/src/routes/_authenticated/index.tsx b/packages/admin/src/routes/_authenticated/index.tsx index 6e054a3..2cc6eaa 100644 --- a/packages/admin/src/routes/_authenticated/index.tsx +++ b/packages/admin/src/routes/_authenticated/index.tsx @@ -2,6 +2,6 @@ import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated/')({ beforeLoad: () => { - throw redirect({ to: '/accounts', search: {} as any }) + throw redirect({ to: '/accounts', search: {} as Record }) }, }) diff --git a/packages/admin/src/routes/_authenticated/inventory/$productId.tsx b/packages/admin/src/routes/_authenticated/inventory/$productId.tsx index cb10925..8f35893 100644 --- a/packages/admin/src/routes/_authenticated/inventory/$productId.tsx +++ b/packages/admin/src/routes/_authenticated/inventory/$productId.tsx @@ -159,7 +159,7 @@ function ProductDetailPage() { }) function setTab(t: string) { - navigate({ to: '/inventory/$productId', params: { productId }, search: { tab: t } as any }) + navigate({ to: '/inventory/$productId', params: { productId }, search: { tab: t } as Record }) } function handleQtySave() { @@ -192,7 +192,7 @@ function ProductDetailPage() {
{/* Header */}
-
@@ -507,12 +507,12 @@ function SuppliersTab({ setAddOpen: (v: boolean) => void editTarget: ProductSupplier | null setEditTarget: (v: ProductSupplier | null) => void - addMutation: any - updateMutation: any - removeMutation: any + addMutation: { mutate: (data: Record) => void; isPending: boolean } + updateMutation: { mutate: (args: { id: string; data: Record }) => void; isPending: boolean } + removeMutation: { mutate: (id: string) => void; isPending: boolean } canEdit: boolean }) { - const { data: allSuppliersData } = useQuery(supplierListOptions({ page: 1, limit: 500, order: 'asc', sort: 'name' } as any)) + const { data: allSuppliersData } = useQuery(supplierListOptions({ page: 1, limit: 500, order: 'asc', sort: 'name' })) const allSuppliers = allSuppliersData?.data ?? [] const linkedIds = new Set(linkedSuppliers.map((s) => s.supplierId)) const availableSuppliers = allSuppliers.filter((s) => !linkedIds.has(s.id)) diff --git a/packages/admin/src/routes/_authenticated/inventory/index.tsx b/packages/admin/src/routes/_authenticated/inventory/index.tsx index 3ebbb7a..bb877fa 100644 --- a/packages/admin/src/routes/_authenticated/inventory/index.tsx +++ b/packages/admin/src/routes/_authenticated/inventory/index.tsx @@ -71,7 +71,7 @@ function InventoryPage() { queryClient.invalidateQueries({ queryKey: productKeys.all }) toast.success('Product created') setCreateOpen(false) - navigate({ to: '/inventory/$productId', params: { productId: product.id }, search: {} as any }) + navigate({ to: '/inventory/$productId', params: { productId: product.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -83,23 +83,23 @@ function InventoryPage() { function handleCategoryChange(v: string) { setCategoryFilter(v === 'all' ? '' : v) - navigate({ to: '/inventory', search: { ...search, categoryId: v === 'all' ? undefined : v, page: 1 } as any }) + navigate({ to: '/inventory', search: { ...search, categoryId: v === 'all' ? undefined : v, page: 1 } as Record }) } function handleActiveChange(v: string) { setActiveFilter(v === 'all' ? '' : v) - navigate({ to: '/inventory', search: { ...search, isActive: v === 'all' ? undefined : v, page: 1 } as any }) + navigate({ to: '/inventory', search: { ...search, isActive: v === 'all' ? undefined : v, page: 1 } as Record }) } function handleTypeChange(v: string) { setTypeFilter(v === 'all' ? '' : v) - navigate({ to: '/inventory', search: { ...search, type: v === 'all' ? undefined : v, page: 1 } as any }) + navigate({ to: '/inventory', search: { ...search, type: v === 'all' ? undefined : v, page: 1 } as Record }) } function handleLowStockChange(v: string) { const on = v === 'true' setLowStockFilter(on) - navigate({ to: '/inventory', search: { ...search, lowStock: on ? 'true' : undefined, page: 1 } as any }) + navigate({ to: '/inventory', search: { ...search, lowStock: on ? 'true' : undefined, page: 1 } as Record }) } const columns: Column[] = [ @@ -246,7 +246,7 @@ function InventoryPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(p) => navigate({ to: '/inventory/$productId', params: { productId: p.id }, search: {} as any })} + onRowClick={(p) => navigate({ to: '/inventory/$productId', params: { productId: p.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/inventory/suppliers/index.tsx b/packages/admin/src/routes/_authenticated/inventory/suppliers/index.tsx index 5d05eee..ac551f9 100644 --- a/packages/admin/src/routes/_authenticated/inventory/suppliers/index.tsx +++ b/packages/admin/src/routes/_authenticated/inventory/suppliers/index.tsx @@ -130,7 +130,7 @@ function SuppliersPage() { New Supplier { createMutation.mutate(data) }} loading={createMutation.isPending} /> diff --git a/packages/admin/src/routes/_authenticated/lessons/enrollments/$enrollmentId.tsx b/packages/admin/src/routes/_authenticated/lessons/enrollments/$enrollmentId.tsx index e904845..1c88f64 100644 --- a/packages/admin/src/routes/_authenticated/lessons/enrollments/$enrollmentId.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/enrollments/$enrollmentId.tsx @@ -21,7 +21,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u import { ArrowLeft, RefreshCw } from 'lucide-react' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' -import type { LessonSession, LessonPlan, LessonPlanTemplate } from '@/types/lesson' +import type { Enrollment, LessonSession, LessonPlan, LessonPlanTemplate } from '@/types/lesson' export const Route = createFileRoute('/_authenticated/lessons/enrollments/$enrollmentId')({ validateSearch: (search: Record) => ({ @@ -81,7 +81,7 @@ function EnrollmentDetailPage() { const tab = search.tab function setTab(t: string) { - navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId }, search: { tab: t } as any }) + navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId }, search: { tab: t } as Record }) } const { data: enrollment, isLoading } = useQuery(enrollmentDetailOptions(enrollmentId)) @@ -131,7 +131,7 @@ function EnrollmentDetailPage() { return (
-
@@ -193,7 +193,17 @@ const BILLING_UNITS = [ function DetailsTab({ enrollment, slotLabel, lessonTypeName, instructorName, canEdit, onSave, saving, onStatusChange, statusChanging, -}: any) { +}: { + enrollment: Enrollment + slotLabel: string + lessonTypeName: string | undefined + instructorName: string | undefined + canEdit: boolean + onSave: (data: Record) => void + saving: boolean + onStatusChange: (status: string) => void + statusChanging: boolean +}) { const [rate, setRate] = useState(enrollment.rate ?? '') const [billingInterval, setBillingInterval] = useState(String(enrollment.billingInterval ?? 1)) const [billingUnit, setBillingUnit] = useState(enrollment.billingUnit ?? 'month') @@ -334,7 +344,7 @@ function SessionsTab({ enrollmentId, onGenerate, generating }: { enrollmentId: s total={data?.data?.length ?? 0} onPageChange={() => {}} onSort={() => {}} - onRowClick={(s) => navigate({ to: '/lessons/sessions/$sessionId', params: { sessionId: s.id }, search: {} as any })} + onRowClick={(s) => navigate({ to: '/lessons/sessions/$sessionId', params: { sessionId: s.id }, search: {} as Record })} />
) @@ -373,7 +383,7 @@ function LessonPlanTab({ enrollmentId, memberId, canEdit }: { enrollmentId: stri queryClient.invalidateQueries({ queryKey: lessonPlanKeys.all }) toast.success('Plan created from template') setTemplatePickerOpen(false) - navigate({ to: '/lessons/plans/$planId', params: { planId: plan.id }, search: {} as any }) + navigate({ to: '/lessons/plans/$planId', params: { planId: plan.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -391,7 +401,7 @@ function LessonPlanTab({ enrollmentId, memberId, canEdit }: { enrollmentId: stri {Math.round(activePlan.progress)}% complete

-
diff --git a/packages/admin/src/routes/_authenticated/lessons/enrollments/index.tsx b/packages/admin/src/routes/_authenticated/lessons/enrollments/index.tsx index d543060..c62611f 100644 --- a/packages/admin/src/routes/_authenticated/lessons/enrollments/index.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/enrollments/index.tsx @@ -43,9 +43,9 @@ function statusBadge(status: string) { } const columns: Column[] = [ - { key: 'member_name', header: 'Member', sortable: true, render: (e) => {(e as any).memberName ?? e.memberId} }, - { key: 'instructor_name', header: 'Instructor', render: (e) => <>{(e as any).instructorName ?? e.instructorId} }, - { key: 'slot_info', header: 'Day / Time', render: (e) => <>{(e as any).slotInfo ?? '—'} }, + { key: 'member_name', header: 'Member', sortable: true, render: (e) => {e.memberName ?? e.memberId} }, + { key: 'instructor_name', header: 'Instructor', render: (e) => <>{e.instructorName ?? e.instructorId} }, + { key: 'slot_info', header: 'Day / Time', render: (e) => <>{e.slotInfo ?? '—'} }, { key: 'status', header: 'Status', sortable: true, render: (e) => statusBadge(e.status) }, { key: 'start_date', header: 'Start', sortable: true, render: (e) => <>{new Date(e.startDate + 'T00:00:00').toLocaleDateString()} }, { key: 'rate', header: 'Rate', render: (e) => <>{e.rate ? `$${e.rate}${e.billingInterval ? ` / ${e.billingInterval} ${e.billingUnit}` : ''}` : } }, @@ -72,7 +72,7 @@ function EnrollmentsListPage() { function handleStatusChange(v: string) { const s = v === 'all' ? '' : v setStatusFilter(s) - navigate({ to: '/lessons/enrollments', search: { ...search, status: s || undefined, page: 1 } as any }) + navigate({ to: '/lessons/enrollments', search: { ...search, status: s || undefined, page: 1 } as Record }) } return ( @@ -80,7 +80,7 @@ function EnrollmentsListPage() {

Enrollments

{hasPermission('lessons.edit') && ( - )} @@ -125,7 +125,7 @@ function EnrollmentsListPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as any })} + onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/lessons/enrollments/new.tsx b/packages/admin/src/routes/_authenticated/lessons/enrollments/new.tsx index af291f2..c47ba82 100644 --- a/packages/admin/src/routes/_authenticated/lessons/enrollments/new.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/enrollments/new.tsx @@ -108,7 +108,7 @@ function NewEnrollmentPage() { }, onSuccess: (enrollment) => { toast.success('Enrollment created') - navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: enrollment.id }, search: {} as any }) + navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: enrollment.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -141,7 +141,7 @@ function NewEnrollmentPage() { return (
-

New Enrollment

@@ -282,7 +282,7 @@ function NewEnrollmentPage() { -
diff --git a/packages/admin/src/routes/_authenticated/lessons/plans/$planId.tsx b/packages/admin/src/routes/_authenticated/lessons/plans/$planId.tsx index 2a203ee..17c1863 100644 --- a/packages/admin/src/routes/_authenticated/lessons/plans/$planId.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/plans/$planId.tsx @@ -93,7 +93,7 @@ function LessonPlanDetailPage() { return (
-
diff --git a/packages/admin/src/routes/_authenticated/lessons/plans/index.tsx b/packages/admin/src/routes/_authenticated/lessons/plans/index.tsx index 3d49e80..9c27b4e 100644 --- a/packages/admin/src/routes/_authenticated/lessons/plans/index.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/plans/index.tsx @@ -84,7 +84,7 @@ function LessonPlansPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(p) => navigate({ to: '/lessons/plans/$planId', params: { planId: p.id }, search: {} as any })} + onRowClick={(p) => navigate({ to: '/lessons/plans/$planId', params: { planId: p.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/lessons/schedule/index.tsx b/packages/admin/src/routes/_authenticated/lessons/schedule/index.tsx index f3bb71c..0dbe820 100644 --- a/packages/admin/src/routes/_authenticated/lessons/schedule/index.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/schedule/index.tsx @@ -49,7 +49,7 @@ function ScheduleHubPage() { const canAdmin = hasPermission('lessons.admin') function setTab(t: string) { - navigate({ to: '/lessons/schedule', search: { ...search, tab: t, page: 1 } as any }) + navigate({ to: '/lessons/schedule', search: { ...search, tab: t, page: 1 } as Record }) } return ( @@ -90,7 +90,7 @@ const instructorColumns: Column[] = [ }, ] -function InstructorsTab({ canAdmin, search: _search }: { canAdmin: boolean; search: any }) { +function InstructorsTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const navigate = useNavigate() const queryClient = useQueryClient() const { params, setPage, setSearch, setSort } = usePagination() @@ -152,7 +152,7 @@ function InstructorsTab({ canAdmin, search: _search }: { canAdmin: boolean; sear order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(i) => navigate({ to: '/lessons/schedule/instructors/$instructorId', params: { instructorId: i.id }, search: {} as any })} + onRowClick={(i) => navigate({ to: '/lessons/schedule/instructors/$instructorId', params: { instructorId: i.id }, search: {} as Record })} />
) @@ -169,7 +169,7 @@ const lessonTypeColumns: Column[] = [ { key: 'is_active', header: 'Status', render: (lt) => {lt.isActive ? 'Active' : 'Inactive'} }, ] -function LessonTypesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: any }) { +function LessonTypesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const queryClient = useQueryClient() const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') @@ -215,8 +215,8 @@ function LessonTypesTab({ canAdmin, search: _search }: { canAdmin: boolean; sear const columnsWithActions: Column[] = [ ...lessonTypeColumns, ...(canAdmin ? [{ - key: 'actions' as any, - header: '' as any, + key: 'actions', + header: '', render: (lt: LessonType) => (
@@ -298,7 +298,7 @@ const gradingScaleColumns: Column[] = [ { key: 'is_active', header: 'Status', render: (gs) => {gs.isActive ? 'Active' : 'Inactive'} }, ] -function GradingScalesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: any }) { +function GradingScalesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const queryClient = useQueryClient() const { params, setPage, setSort } = usePagination() const [createOpen, setCreateOpen] = useState(false) @@ -327,8 +327,8 @@ function GradingScalesTab({ canAdmin, search: _search }: { canAdmin: boolean; se const columnsWithActions: Column[] = [ ...gradingScaleColumns, ...(canAdmin ? [{ - key: 'actions' as any, - header: '' as any, + key: 'actions', + header: '', render: (gs: GradingScale) => (
diff --git a/packages/admin/src/routes/_authenticated/lessons/sessions/$sessionId.tsx b/packages/admin/src/routes/_authenticated/lessons/sessions/$sessionId.tsx index c5752e5..c6a9a69 100644 --- a/packages/admin/src/routes/_authenticated/lessons/sessions/$sessionId.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/sessions/$sessionId.tsx @@ -18,7 +18,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { ArrowLeft, CheckSquare, Square } from 'lucide-react' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' -import type { LessonPlan, LessonPlanSection } from '@/types/lesson' +import type { LessonPlan, LessonPlanSection, LessonSession } from '@/types/lesson' export const Route = createFileRoute('/_authenticated/lessons/sessions/$sessionId')({ component: SessionDetailPage, @@ -126,7 +126,7 @@ function SessionDetailPage() { return (
-
@@ -137,7 +137,7 @@ function SessionDetailPage() { } className="text-sm text-primary hover:underline" > View Enrollment @@ -209,7 +209,12 @@ function SessionDetailPage() { // ─── Notes Card ─────────────────────────────────────────────────────────────── -function NotesCard({ session, canEdit, onSave, saving }: any) { +function NotesCard({ session, canEdit, onSave, saving }: { + session: LessonSession + canEdit: boolean + onSave: (data: Record) => void + saving: boolean +}) { const [instructorNotes, setInstructorNotes] = useState(session.instructorNotes ?? '') const [memberNotes, setMemberNotes] = useState(session.memberNotes ?? '') const [homeworkAssigned, setHomeworkAssigned] = useState(session.homeworkAssigned ?? '') diff --git a/packages/admin/src/routes/_authenticated/lessons/sessions/index.tsx b/packages/admin/src/routes/_authenticated/lessons/sessions/index.tsx index d0d5833..0177076 100644 --- a/packages/admin/src/routes/_authenticated/lessons/sessions/index.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/sessions/index.tsx @@ -92,13 +92,13 @@ function SessionsPage() { const weekEnd = endOfWeek(weekStart, { weekStartsOn: 0 }) function setView(v: 'list' | 'week') { - navigate({ to: '/lessons/sessions', search: { ...search, view: v, page: 1 } as any }) + navigate({ to: '/lessons/sessions', search: { ...search, view: v, page: 1 } as Record }) } function handleStatusChange(v: string) { const s = v === 'all' ? '' : v setStatusFilter(s) - navigate({ to: '/lessons/sessions', search: { ...search, status: s || undefined, page: 1 } as any }) + navigate({ to: '/lessons/sessions', search: { ...search, status: s || undefined, page: 1 } as Record }) } // List query @@ -189,7 +189,7 @@ function SessionsPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(s) => navigate({ to: '/lessons/sessions/$sessionId', params: { sessionId: s.id }, search: {} as any })} + onRowClick={(s) => navigate({ to: '/lessons/sessions/$sessionId', params: { sessionId: s.id }, search: {} as Record })} /> )} @@ -249,7 +249,7 @@ function SessionsPage() { {daySessions.map((s) => (
@@ -93,7 +93,7 @@ function TemplateDetailPage() { // ─── Edit Form ──────────────────────────────────────────────────────────────── -function EditTemplateForm({ template, templateId, queryClient }: { template: LessonPlanTemplate; templateId: string; queryClient: any }) { +function EditTemplateForm({ template, templateId, queryClient }: { template: LessonPlanTemplate; templateId: string; queryClient: ReturnType }) { const [name, setName] = useState(template.name) const [description, setDescription] = useState(template.description ?? '') const [instrument, setInstrument] = useState(template.instrument ?? '') @@ -218,7 +218,7 @@ function InstantiateDialog({ template, templateId, open, onClose }: { }), onSuccess: (plan) => { toast.success('Plan created from template') - navigate({ to: '/lessons/plans/$planId', params: { planId: plan.id }, search: {} as any }) + navigate({ to: '/lessons/plans/$planId', params: { planId: plan.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -293,7 +293,7 @@ function InstantiateDialog({ template, templateId, open, onClose }: { Not linked to enrollment - {enrollments.map((e: any) => ( + {enrollments.map((e: Enrollment) => ( Enrollment {e.id.slice(-6)} ))} diff --git a/packages/admin/src/routes/_authenticated/lessons/templates/index.tsx b/packages/admin/src/routes/_authenticated/lessons/templates/index.tsx index 6819fc6..8844746 100644 --- a/packages/admin/src/routes/_authenticated/lessons/templates/index.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/templates/index.tsx @@ -81,8 +81,8 @@ function TemplatesListPage() { const columnsWithActions: Column[] = [ ...columns, ...(canAdmin ? [{ - key: 'actions' as any, - header: '' as any, + key: 'actions', + header: '', render: (t: LessonPlanTemplate) => ( )} @@ -126,7 +126,7 @@ function TemplatesListPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(t) => navigate({ to: '/lessons/templates/$templateId', params: { templateId: t.id }, search: {} as any })} + onRowClick={(t) => navigate({ to: '/lessons/templates/$templateId', params: { templateId: t.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/lessons/templates/new.tsx b/packages/admin/src/routes/_authenticated/lessons/templates/new.tsx index 5ddb1f4..ceaa5d4 100644 --- a/packages/admin/src/routes/_authenticated/lessons/templates/new.tsx +++ b/packages/admin/src/routes/_authenticated/lessons/templates/new.tsx @@ -45,7 +45,7 @@ function NewTemplatePage() { }), onSuccess: (template) => { toast.success('Template created') - navigate({ to: '/lessons/templates/$templateId', params: { templateId: template.id }, search: {} as any }) + navigate({ to: '/lessons/templates/$templateId', params: { templateId: template.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -63,7 +63,7 @@ function NewTemplatePage() { return (
-

New Template

@@ -112,7 +112,7 @@ function NewTemplatePage() { -
diff --git a/packages/admin/src/routes/_authenticated/members/$memberId.tsx b/packages/admin/src/routes/_authenticated/members/$memberId.tsx index 639a538..2e6b1a1 100644 --- a/packages/admin/src/routes/_authenticated/members/$memberId.tsx +++ b/packages/admin/src/routes/_authenticated/members/$memberId.tsx @@ -70,11 +70,11 @@ function statusBadge(status: string) { return {status} } -const enrollmentColumns: Column[] = [ +const enrollmentColumns: Column[] = [ { key: 'status', header: 'Status', sortable: true, render: (e) => statusBadge(e.status) }, - { key: 'instructor_name', header: 'Instructor', render: (e) => <>{(e as any).instructorName ?? e.instructorId} }, - { key: 'slot_info', header: 'Day / Time', render: (e) => <>{(e as any).slotInfo ?? '—'} }, - { key: 'lesson_type', header: 'Lesson', render: (e) => <>{(e as any).lessonTypeName ?? '—'} }, + { key: 'instructor_name', header: 'Instructor', render: (e) => <>{e.instructorName ?? e.instructorId} }, + { key: 'slot_info', header: 'Day / Time', render: (e) => <>{e.slotInfo ?? '—'} }, + { key: 'lesson_type', header: 'Lesson', render: (e) => <>{e.lessonTypeName ?? '—'} }, { key: 'start_date', header: 'Start', sortable: true, render: (e) => <>{new Date(e.startDate + 'T00:00:00').toLocaleDateString()} }, { key: 'rate', header: 'Rate', render: (e) => <>{e.rate ? `$${e.rate} / ${e.billingInterval} ${e.billingUnit}` : } }, ] @@ -161,7 +161,7 @@ function MemberDetailPage() { }) function setTab(t: string) { - navigate({ to: '/members/$memberId', params: { memberId }, search: { tab: t } as any }) + navigate({ to: '/members/$memberId', params: { memberId }, search: { tab: t } as Record }) } if (isLoading) { @@ -188,7 +188,7 @@ function MemberDetailPage() {
{/* Header */}
-
@@ -293,7 +293,7 @@ function MemberDetailPage() {

{enrollmentsData?.pagination.total ?? 0} enrollment(s)

{hasPermission('lessons.edit') && ( - )} @@ -307,7 +307,7 @@ function MemberDetailPage() { total={enrollmentsData?.data?.length ?? 0} onPageChange={() => {}} onSort={() => {}} - onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as any })} + onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as Record })} />
)} diff --git a/packages/admin/src/routes/_authenticated/members/index.tsx b/packages/admin/src/routes/_authenticated/members/index.tsx index 9c42873..014ca2e 100644 --- a/packages/admin/src/routes/_authenticated/members/index.tsx +++ b/packages/admin/src/routes/_authenticated/members/index.tsx @@ -84,7 +84,7 @@ function MembersListPage() { - navigate({ to: '/members/$memberId', params: { memberId: row.id }, search: {} as any })}> + navigate({ to: '/members/$memberId', params: { memberId: row.id }, search: {} as Record })}> Edit @@ -134,7 +134,7 @@ function MembersListPage() { order={params.order} onPageChange={setPage} onSort={setSort} - onRowClick={(member) => navigate({ to: '/members/$memberId', params: { memberId: member.id }, search: {} as any })} + onRowClick={(member) => navigate({ to: '/members/$memberId', params: { memberId: member.id }, search: {} as Record })} />
) diff --git a/packages/admin/src/routes/_authenticated/repair-batches/$batchId.tsx b/packages/admin/src/routes/_authenticated/repair-batches/$batchId.tsx index 0dfda66..f1ec150 100644 --- a/packages/admin/src/routes/_authenticated/repair-batches/$batchId.tsx +++ b/packages/admin/src/routes/_authenticated/repair-batches/$batchId.tsx @@ -96,12 +96,12 @@ function RepairBatchDetailPage() { const totalActual = tickets.reduce((sum, t) => sum + (t.actualCost ? parseFloat(t.actualCost) : 0), 0) function handleTicketClick(ticket: RepairTicket) { - navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as any }) + navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as Record }) } function handleAddRepair() { // Navigate to new repair with batch and account pre-linked - navigate({ to: '/repairs/new', search: { batchId, batchName: batch!.batchNumber ?? '', accountId: batch!.accountId, contactName: batch!.contactName ?? '' } as any }) + navigate({ to: '/repairs/new', search: { batchId, batchName: batch!.batchNumber ?? '', accountId: batch!.accountId, contactName: batch!.contactName ?? '' } as Record }) } async function generateBatchPdf() { @@ -233,7 +233,7 @@ function RepairBatchDetailPage() { return (
-
diff --git a/packages/admin/src/routes/_authenticated/repair-batches/index.tsx b/packages/admin/src/routes/_authenticated/repair-batches/index.tsx index 83a634c..0c1a459 100644 --- a/packages/admin/src/routes/_authenticated/repair-batches/index.tsx +++ b/packages/admin/src/routes/_authenticated/repair-batches/index.tsx @@ -75,7 +75,7 @@ function RepairBatchesListPage() { } function handleRowClick(batch: RepairBatch) { - navigate({ to: '/repair-batches/$batchId', params: { batchId: batch.id }, search: {} as any }) + navigate({ to: '/repair-batches/$batchId', params: { batchId: batch.id }, search: {} as Record }) } return ( diff --git a/packages/admin/src/routes/_authenticated/repair-batches/new.tsx b/packages/admin/src/routes/_authenticated/repair-batches/new.tsx index 26fb8a4..7395f35 100644 --- a/packages/admin/src/routes/_authenticated/repair-batches/new.tsx +++ b/packages/admin/src/routes/_authenticated/repair-batches/new.tsx @@ -50,7 +50,7 @@ function NewRepairBatchPage() { mutationFn: repairBatchMutations.create, onSuccess: (batch) => { toast.success('Repair batch created') - navigate({ to: '/repair-batches/$batchId', params: { batchId: batch.id }, search: {} as any }) + navigate({ to: '/repair-batches/$batchId', params: { batchId: batch.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -78,7 +78,7 @@ function NewRepairBatchPage() { return (
-

New Repair Batch

@@ -176,7 +176,7 @@ function NewRepairBatchPage() { -
diff --git a/packages/admin/src/routes/_authenticated/repairs/$ticketId.tsx b/packages/admin/src/routes/_authenticated/repairs/$ticketId.tsx index ebc0e1b..5aeff6a 100644 --- a/packages/admin/src/routes/_authenticated/repairs/$ticketId.tsx +++ b/packages/admin/src/routes/_authenticated/repairs/$ticketId.tsx @@ -175,7 +175,7 @@ function RepairTicketDetailPage() {
{/* Header */}
-
diff --git a/packages/admin/src/routes/_authenticated/repairs/index.tsx b/packages/admin/src/routes/_authenticated/repairs/index.tsx index ba07558..1d433eb 100644 --- a/packages/admin/src/routes/_authenticated/repairs/index.tsx +++ b/packages/admin/src/routes/_authenticated/repairs/index.tsx @@ -129,7 +129,7 @@ function RepairsListPage() { } function handleRowClick(ticket: RepairTicket) { - navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as any }) + navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as Record }) } return ( @@ -137,7 +137,7 @@ function RepairsListPage() {

Repairs

{hasPermission('repairs.edit') && ( - diff --git a/packages/admin/src/routes/_authenticated/repairs/new.tsx b/packages/admin/src/routes/_authenticated/repairs/new.tsx index 0dba6f2..2b8f72d 100644 --- a/packages/admin/src/routes/_authenticated/repairs/new.tsx +++ b/packages/admin/src/routes/_authenticated/repairs/new.tsx @@ -136,7 +136,7 @@ function NewRepairPage() { }, onSuccess: (ticket) => { toast.success('Repair ticket created') - navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as any }) + navigate({ to: '/repairs/$ticketId', params: { ticketId: ticket.id }, search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -210,7 +210,7 @@ function NewRepairPage() { return (
-

New Repair Ticket

@@ -314,7 +314,7 @@ function NewRepairPage() {
- setValue('conditionIn', v as 'excellent' | 'good' | 'fair' | 'poor')}> Excellent @@ -486,7 +486,7 @@ function NewRepairPage() { -
diff --git a/packages/admin/src/routes/_authenticated/roles/$roleId.tsx b/packages/admin/src/routes/_authenticated/roles/$roleId.tsx index 31ec29c..b1ba295 100644 --- a/packages/admin/src/routes/_authenticated/roles/$roleId.tsx +++ b/packages/admin/src/routes/_authenticated/roles/$roleId.tsx @@ -100,7 +100,7 @@ function RoleDetailPage() { return (
-
@@ -177,7 +177,7 @@ function RoleDetailPage() { -
diff --git a/packages/admin/src/routes/_authenticated/roles/new.tsx b/packages/admin/src/routes/_authenticated/roles/new.tsx index 7ad7be2..d7b34c4 100644 --- a/packages/admin/src/routes/_authenticated/roles/new.tsx +++ b/packages/admin/src/routes/_authenticated/roles/new.tsx @@ -29,7 +29,7 @@ function NewRolePage() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: rbacKeys.roles }) toast.success('Role created') - navigate({ to: '/roles', search: {} as any }) + navigate({ to: '/roles', search: {} as Record }) }, onError: (err) => toast.error(err.message), }) @@ -153,7 +153,7 @@ function NewRolePage() { -
diff --git a/packages/admin/src/routes/login.tsx b/packages/admin/src/routes/login.tsx index c97d9a2..8e66c2f 100644 --- a/packages/admin/src/routes/login.tsx +++ b/packages/admin/src/routes/login.tsx @@ -7,7 +7,7 @@ export const Route = createFileRoute('/login')({ beforeLoad: () => { const { token } = useAuthStore.getState() if (token) { - throw redirect({ to: '/accounts', search: {} as any }) + throw redirect({ to: '/accounts', search: {} as Record }) } }, component: LoginPage, @@ -30,7 +30,7 @@ function LoginPage() { const res = await login(email, password) setAuth(res.token, res.user) await router.invalidate() - await router.navigate({ to: '/accounts', search: {} as any, replace: true }) + await router.navigate({ to: '/accounts', search: {} as Record, replace: true }) } catch (err) { setError(err instanceof Error ? err.message : 'Login failed') } finally {