Files
lunarfront-app/packages/admin/src/components/pos/pos-manager-override.tsx
ryan be8cc0ad8b fix: code review fixes + unit/API tests for repair-POS integration
Code review fixes:
- Wrap createFromRepairTicket() in DB transaction for atomicity
- Wrap complete() inventory + status updates in DB transaction
- Repair ticket status update now atomic with transaction completion
- Add Zod validation on from-repair route body
- Fix requiresDiscountOverride: threshold and manual_discount are independent checks
- Order discount distributes proportionally across line items (not first-only)
- Extract shared receipt calculations into useReceiptData/useBarcode hooks
- Add error handling for barcode generation

Tests:
- Unit: consumable tax category mapping, exempt rate short-circuit
- API: ready-for-pickup listing + search, from-repair transaction creation,
  consumable exclusion from line items, tax rate verification (labor=service,
  part=goods), duplicate prevention, ticket auto-pickup on payment completion,
  isConsumable product filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:05:19 +00:00

202 lines
6.9 KiB
TypeScript

import { useState, useCallback, useEffect, useRef } from 'react'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { api } from '@/lib/api-client'
import { Delete, ShieldCheck } from 'lucide-react'
interface ManagerOverrideDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
action: string
onAuthorized: () => void
}
interface PinUser {
id: string
role: string
firstName: string
lastName: string
}
export function ManagerOverrideDialog({ open, onOpenChange, action, onAuthorized }: ManagerOverrideDialogProps) {
const [code, setCode] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (open) {
setCode('')
setError('')
containerRef.current?.focus()
}
}, [open])
const handleDigit = useCallback((digit: string) => {
setError('')
setCode((p) => (p.length >= 10 ? p : p + digit))
}, [])
const handleBackspace = useCallback(() => {
setError('')
setCode((p) => p.slice(0, -1))
}, [])
const handleClear = useCallback(() => {
setError('')
setCode('')
}, [])
const handleSubmit = useCallback(async (submitCode: string) => {
if (submitCode.length < 8) {
setError('Enter manager employee # + PIN')
return
}
setLoading(true)
setError('')
try {
const res = await api.post<{ user: PinUser; token: string }>('/v1/auth/pin-login', { code: submitCode })
if (res.user.role === 'admin' || res.user.role === 'manager') {
onAuthorized()
onOpenChange(false)
} else {
setError('Manager or admin access required')
setCode('')
}
} catch {
setError('Invalid code')
setCode('')
} finally {
setLoading(false)
}
}, [onAuthorized, onOpenChange])
useEffect(() => {
if (code.length === 8) {
handleSubmit(code)
}
}, [code, handleSubmit])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key >= '0' && e.key <= '9') handleDigit(e.key)
else if (e.key === 'Backspace') handleBackspace()
else if (e.key === 'Enter' && code.length >= 8) handleSubmit(code)
else if (e.key === 'Escape') handleClear()
}, [handleDigit, handleBackspace, handleSubmit, handleClear, code])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xs">
<div
ref={containerRef}
onKeyDown={handleKeyDown}
tabIndex={0}
className="outline-none space-y-4"
>
<div className="text-center space-y-1">
<ShieldCheck className="h-8 w-8 mx-auto text-amber-500" />
<DialogTitle className="text-base">Manager Override</DialogTitle>
<p className="text-xs text-muted-foreground">{action}</p>
</div>
{/* Code dots */}
<div className="flex justify-center items-center gap-2">
<div className="flex gap-1.5">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={`e${i}`}
className={`w-3 h-3 rounded-full border-2 ${
i < Math.min(code.length, 4) ? 'bg-foreground border-foreground' : 'border-muted-foreground/30'
}`}
/>
))}
</div>
<span className="text-muted-foreground/40">-</span>
<div className="flex gap-1.5">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={`p${i}`}
className={`w-3 h-3 rounded-full border-2 ${
i < Math.max(0, code.length - 4) ? 'bg-foreground border-foreground' : 'border-muted-foreground/30'
}`}
/>
))}
</div>
</div>
{error && <p className="text-xs text-destructive text-center">{error}</p>}
{/* Numpad */}
<div className="grid grid-cols-3 gap-1.5">
{['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((d) => (
<Button key={d} variant="outline" className="h-12 text-lg font-medium" onClick={() => handleDigit(d)} disabled={loading}>
{d}
</Button>
))}
<Button variant="outline" className="h-12 text-xs" onClick={handleClear} disabled={loading}>Clear</Button>
<Button variant="outline" className="h-12 text-lg font-medium" onClick={() => handleDigit('0')} disabled={loading}>0</Button>
<Button variant="outline" className="h-12" onClick={handleBackspace} disabled={loading}>
<Delete className="h-4 w-4" />
</Button>
</div>
{loading && <p className="text-xs text-muted-foreground text-center">Verifying...</p>}
</div>
</DialogContent>
</Dialog>
)
}
// --- Config types & helpers ---
export const OVERRIDE_ACTIONS = [
{ key: 'void_transaction', label: 'Void Transaction', description: 'Cancel an in-progress sale' },
{ key: 'refund', label: 'Refund', description: 'Process a return or refund' },
{ key: 'manual_discount', label: 'Manual Discount', description: 'Apply a discount not from a preset' },
{ key: 'price_override', label: 'Price Override', description: 'Change an item price at the register' },
{ key: 'no_sale_drawer', label: 'No-Sale Drawer Open', description: 'Open the drawer without a transaction' },
{ key: 'cash_in_out', label: 'Cash In / Cash Out', description: 'Add or remove cash from the drawer' },
] as const
export type OverrideAction = typeof OVERRIDE_ACTIONS[number]['key']
const STORAGE_KEY = 'pos_manager_overrides'
export function getRequiredOverrides(): Set<OverrideAction> {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (!stored) return new Set()
return new Set(JSON.parse(stored) as OverrideAction[])
} catch {
return new Set()
}
}
export function setRequiredOverrides(actions: Set<OverrideAction>) {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...actions]))
}
export function requiresOverride(action: OverrideAction): boolean {
return getRequiredOverrides().has(action)
}
// Discount threshold — discounts above this percentage require manager override
const DISCOUNT_THRESHOLD_KEY = 'pos_discount_threshold_pct'
export function getDiscountThreshold(): number {
const stored = localStorage.getItem(DISCOUNT_THRESHOLD_KEY)
return stored ? parseInt(stored, 10) : 0 // 0 = disabled
}
export function setDiscountThreshold(pct: number) {
localStorage.setItem(DISCOUNT_THRESHOLD_KEY, String(pct))
}
export function requiresDiscountOverride(discountPct: number): boolean {
// Check percentage threshold first
const threshold = getDiscountThreshold()
if (threshold > 0 && discountPct >= threshold) return true
// Fall back to the blanket manual_discount toggle
return requiresOverride('manual_discount')
}