feat: POS PIN unlock with employee number + PIN auth
- Add employeeNumber and pinHash fields to users table - POST /auth/pin-login: takes combined code (4-digit employee# + 4-digit PIN) - POST /auth/set-pin: employee sets their own PIN (requires full auth) - DELETE /auth/pin: remove PIN - Lock screen with numpad, auto-submits on 8 digits, visual dot separator - POS uses its own auth token separate from admin session - Admin "POS" link clears admin session before navigating - /pos route has no auth guard — lock screen is the auth - API client uses POS token when available, admin token otherwise - Auto-lock timer reads pos_lock_timeout from app_config (default 15 min) - Lock button in POS top bar, shows current cashier name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import { Link, useRouter } from '@tanstack/react-router'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
|
||||
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, LogOut, DollarSign } from 'lucide-react'
|
||||
import { ArrowLeft, Lock, DollarSign } from 'lucide-react'
|
||||
import type { DrawerSession } from '@/api/pos'
|
||||
import { useState } from 'react'
|
||||
import { POSDrawerDialog } from './pos-drawer-dialog'
|
||||
@@ -17,24 +16,18 @@ interface POSTopBarProps {
|
||||
}
|
||||
|
||||
export function POSTopBar({ locations, locationId, onLocationChange, drawer }: POSTopBarProps) {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const logout = useAuthStore((s) => s.logout)
|
||||
const cashier = usePOSStore((s) => s.cashier)
|
||||
const lockFn = usePOSStore((s) => s.lock)
|
||||
const [drawerDialogOpen, setDrawerDialogOpen] = useState(false)
|
||||
|
||||
const drawerOpen = drawer?.status === 'open'
|
||||
|
||||
function handleLogout() {
|
||||
logout()
|
||||
router.navigate({ to: '/login', replace: true })
|
||||
}
|
||||
|
||||
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="/" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||
<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>
|
||||
@@ -70,11 +63,13 @@ export function POSTopBar({ locations, locationId, onLocationChange, drawer }: P
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Right: user + logout */}
|
||||
{/* Right: cashier + lock */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{user?.firstName}</span>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleLogout} title="Sign out">
|
||||
<LogOut className="h-4 w-4" />
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user