- Add thermal/full-page receipt format toggle (per-device, localStorage) - Full-page receipt uses clean invoice layout matching repair PDF style - Settings page reorganized into tabbed sections (Store, Locations, Modules, Receipt, POS Security, Advanced) - Manager override system: configurable PIN prompt for void, refund, discount, cash in/out - Discount threshold setting: require manager approval above X% - Consumable product type: tracked for internal job costing, excluded from POS search, receipts, and customer-facing totals - Repair line item dialog: product picker dropdown for parts/consumables from inventory - Repair → POS checkout: load ready-for-pickup tickets into repair_payment transactions with proper tax categories (labor=service, parts=goods) - Transaction completion auto-updates repair ticket status to picked_up - POS Repairs dialog with Pickup and New Intake tabs, customer account lookup - Inline price adjustment on cart items: % off, $ off, or set price with live preview - Order-level discount button with same three input modes - Backend: migration 0043 (consumable enum + is_consumable flag), createFromRepairTicket service, ready-for-pickup endpoint - Fix: backend dev script uses --env-file for turbo compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import { Link } from '@tanstack/react-router'
|
|
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, Lock, DollarSign, Receipt, FileText } 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 cashier = usePOSStore((s) => s.cashier)
|
|
const lockFn = usePOSStore((s) => s.lock)
|
|
const receiptFormat = usePOSStore((s) => s.receiptFormat)
|
|
const setReceiptFormat = usePOSStore((s) => s.setReceiptFormat)
|
|
const [drawerDialogOpen, setDrawerDialogOpen] = useState(false)
|
|
|
|
const drawerOpen = drawer?.status === 'open'
|
|
const isThermal = receiptFormat === 'thermal'
|
|
|
|
return (
|
|
<>
|
|
<div className="h-12 border-b border-border bg-card flex items-center justify-between px-3 shrink-0">
|
|
{/* Left: back + location */}
|
|
<div className="flex items-center gap-3">
|
|
<Link to="/login" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Admin</span>
|
|
</Link>
|
|
|
|
{locations.length > 1 ? (
|
|
<Select value={locationId ?? ''} onValueChange={onLocationChange}>
|
|
<SelectTrigger className="h-8 w-48 text-sm">
|
|
<SelectValue placeholder="Select location" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{locations.map((loc) => (
|
|
<SelectItem key={loc.id} value={loc.id}>{loc.name}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : locations.length === 1 ? (
|
|
<span className="text-sm font-medium">{locations[0].name}</span>
|
|
) : null}
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
|
onClick={() => setReceiptFormat(isThermal ? 'full' : 'thermal')}
|
|
title={isThermal ? 'Receipt: Thermal — click to switch to Full Page' : 'Receipt: Full Page — click to switch to Thermal'}
|
|
>
|
|
{isThermal ? <Receipt className="h-3.5 w-3.5" /> : <FileText className="h-3.5 w-3.5" />}
|
|
<span className="hidden sm:inline">{isThermal ? 'Thermal' : 'Full Page'}</span>
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Center: drawer status */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="flex items-center gap-2"
|
|
onClick={() => setDrawerDialogOpen(true)}
|
|
>
|
|
<DollarSign className="h-4 w-4" />
|
|
{drawerOpen ? (
|
|
<Badge variant="default" className="text-xs">Drawer Open</Badge>
|
|
) : (
|
|
<Badge variant="outline" className="text-xs">Drawer Closed</Badge>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Right: cashier + lock */}
|
|
<div className="flex items-center gap-2">
|
|
{cashier && (
|
|
<span className="text-sm text-muted-foreground">{cashier.firstName}</span>
|
|
)}
|
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={lockFn} title="Lock POS">
|
|
<Lock className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<POSDrawerDialog
|
|
open={drawerDialogOpen}
|
|
onOpenChange={setDrawerDialogOpen}
|
|
drawer={drawer}
|
|
/>
|
|
</>
|
|
)
|
|
}
|