feat: unified station mode (POS + Repairs + Lessons) #14
@@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '@/lib/api-client'
|
||||||
|
import { usePOSStore } from '@/stores/pos.store'
|
||||||
|
import type { RepairTicket } from '@/types/repair'
|
||||||
|
import type { PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||||
|
import { RepairWorkbench } from './repair-workbench'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Wrench } from 'lucide-react'
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
new: 'New', intake: 'Intake', diagnosing: 'Diagnosing',
|
||||||
|
pending_approval: 'Pending', approved: 'Approved',
|
||||||
|
in_progress: 'In Progress', pending_parts: 'Parts',
|
||||||
|
ready: 'Ready',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RepairTechView() {
|
||||||
|
const cashier = usePOSStore((s) => s.cashier)
|
||||||
|
const [selectedTicketId, setSelectedTicketId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Fetch tickets assigned to current user (active statuses only)
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['repair-tickets', 'tech-assigned', cashier?.id],
|
||||||
|
queryFn: () => api.get<PaginatedResponse<RepairTicket>>('/v1/repair-tickets', {
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
sort: 'created_at',
|
||||||
|
order: 'asc',
|
||||||
|
q: undefined,
|
||||||
|
// Filter to active statuses — the API will return all if no status filter, we filter client-side
|
||||||
|
}),
|
||||||
|
enabled: !!cashier?.id,
|
||||||
|
staleTime: 15_000,
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Filter to tickets assigned to this technician in active statuses
|
||||||
|
const activeStatuses = ['diagnosing', 'pending_approval', 'approved', 'in_progress', 'pending_parts', 'ready']
|
||||||
|
const myTickets = (data?.data ?? []).filter(t =>
|
||||||
|
t.assignedTechnicianId === cashier?.id && activeStatuses.includes(t.status)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-select first ticket
|
||||||
|
if (!selectedTicketId && myTickets.length > 0) {
|
||||||
|
setSelectedTicketId(myTickets[0].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myTickets.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<Wrench className="h-12 w-12 mx-auto opacity-20" />
|
||||||
|
<p className="text-lg font-medium">No assigned tickets</p>
|
||||||
|
<p className="text-sm">Tickets assigned to you will appear here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Ticket selector */}
|
||||||
|
{myTickets.length > 1 && (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-border shrink-0 bg-muted/30">
|
||||||
|
<span className="text-sm text-muted-foreground">Ticket:</span>
|
||||||
|
<Select value={selectedTicketId ?? ''} onValueChange={setSelectedTicketId}>
|
||||||
|
<SelectTrigger className="h-8 w-72">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{myTickets.map(t => (
|
||||||
|
<SelectItem key={t.id} value={t.id}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
#{t.ticketNumber} — {t.customerName}
|
||||||
|
<Badge variant="outline" className="text-[10px]">{STATUS_LABELS[t.status] ?? t.status}</Badge>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="text-xs text-muted-foreground">{myTickets.length} active</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Workbench */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
|
{selectedTicketId && <RepairWorkbench ticketId={selectedTicketId} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import {
|
||||||
|
repairTicketDetailOptions, repairTicketKeys, repairTicketMutations,
|
||||||
|
repairLineItemListOptions, repairLineItemMutations,
|
||||||
|
repairServiceTemplateListOptions,
|
||||||
|
repairNoteListOptions, repairNoteMutations,
|
||||||
|
} from '@/api/repairs'
|
||||||
|
import { StatusProgress } from '@/components/repairs/status-progress'
|
||||||
|
import { TicketPhotos } from '@/components/repairs/ticket-photos'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { ChevronRight, Plus, Camera, MessageSquare, Wrench, Trash2 } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const STATUS_FLOW = ['new', 'intake', 'diagnosing', 'pending_approval', 'approved', 'in_progress', 'ready', 'picked_up']
|
||||||
|
|
||||||
|
interface RepairWorkbenchProps {
|
||||||
|
ticketId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RepairWorkbench({ ticketId }: RepairWorkbenchProps) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [activeSection, setActiveSection] = useState<'work' | 'parts' | 'photos' | 'notes'>('work')
|
||||||
|
const [newNote, setNewNote] = useState('')
|
||||||
|
|
||||||
|
// Add line item state
|
||||||
|
const [addingItem, setAddingItem] = useState(false)
|
||||||
|
const [newItemType, setNewItemType] = useState('part')
|
||||||
|
const [newItemDesc, setNewItemDesc] = useState('')
|
||||||
|
const [newItemQty, setNewItemQty] = useState('1')
|
||||||
|
const [newItemPrice, setNewItemPrice] = useState('')
|
||||||
|
|
||||||
|
const { data: ticket } = useQuery({
|
||||||
|
...repairTicketDetailOptions(ticketId),
|
||||||
|
staleTime: 15_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: lineItemsData } = useQuery({
|
||||||
|
...repairLineItemListOptions(ticketId, { page: 1, limit: 100, q: undefined, sort: undefined, order: 'asc' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: notesData } = useQuery(repairNoteListOptions(ticketId))
|
||||||
|
|
||||||
|
const { data: templatesData } = useQuery({
|
||||||
|
...repairServiceTemplateListOptions({ page: 1, limit: 50, q: undefined, sort: 'sort_order', order: 'asc' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusMutation = useMutation({
|
||||||
|
mutationFn: (status: string) => repairTicketMutations.updateStatus(ticketId, status),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: repairTicketKeys.detail(ticketId) })
|
||||||
|
toast.success('Status updated')
|
||||||
|
},
|
||||||
|
onError: (err) => toast.error(err.message),
|
||||||
|
})
|
||||||
|
|
||||||
|
const addNoteMutation = useMutation({
|
||||||
|
mutationFn: () => repairNoteMutations.create(ticketId, { content: newNote, visibility: 'internal' }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['repair-tickets', ticketId, 'notes'] })
|
||||||
|
setNewNote('')
|
||||||
|
toast.success('Note added')
|
||||||
|
},
|
||||||
|
onError: (err) => toast.error(err.message),
|
||||||
|
})
|
||||||
|
|
||||||
|
const addLineItemMutation = useMutation({
|
||||||
|
mutationFn: () => repairLineItemMutations.create(ticketId, {
|
||||||
|
itemType: newItemType,
|
||||||
|
description: newItemDesc,
|
||||||
|
qty: parseInt(newItemQty) || 1,
|
||||||
|
unitPrice: parseFloat(newItemPrice) || 0,
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['repair-tickets', ticketId, 'line-items'] })
|
||||||
|
setAddingItem(false)
|
||||||
|
setNewItemDesc('')
|
||||||
|
setNewItemPrice('')
|
||||||
|
toast.success('Item added')
|
||||||
|
},
|
||||||
|
onError: (err) => toast.error(err.message),
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteLineItemMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => repairLineItemMutations.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['repair-tickets', ticketId, 'line-items'] })
|
||||||
|
toast.success('Item removed')
|
||||||
|
},
|
||||||
|
onError: (err) => toast.error(err.message),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineItems = lineItemsData?.data ?? []
|
||||||
|
const notes = notesData?.data ?? []
|
||||||
|
const templates = templatesData?.data ?? []
|
||||||
|
const currentIdx = STATUS_FLOW.indexOf(ticket.status)
|
||||||
|
const nextStatus = currentIdx >= 0 && currentIdx < STATUS_FLOW.length - 1 ? STATUS_FLOW[currentIdx + 1] : null
|
||||||
|
const isTerminal = ['picked_up', 'delivered', 'cancelled'].includes(ticket.status)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-border shrink-0 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold">#{ticket.ticketNumber}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">{ticket.customerName} — {ticket.itemDescription ?? 'No item'}</p>
|
||||||
|
</div>
|
||||||
|
{nextStatus && !isTerminal && (
|
||||||
|
<Button size="lg" className="h-12 gap-2 text-base" onClick={() => statusMutation.mutate(nextStatus)}>
|
||||||
|
Next Step <ChevronRight className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<StatusProgress currentStatus={ticket.status} onStatusClick={(s) => !isTerminal && statusMutation.mutate(s)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section toggle */}
|
||||||
|
<div className="flex gap-1 px-4 py-2 border-b border-border shrink-0 bg-muted/30">
|
||||||
|
{[
|
||||||
|
{ key: 'work' as const, icon: Wrench, label: 'Work' },
|
||||||
|
{ key: 'parts' as const, icon: Plus, label: 'Parts' },
|
||||||
|
{ key: 'photos' as const, icon: Camera, label: 'Photos' },
|
||||||
|
{ key: 'notes' as const, icon: MessageSquare, label: `Notes (${notes.length})` },
|
||||||
|
].map((s) => (
|
||||||
|
<Button
|
||||||
|
key={s.key}
|
||||||
|
variant={activeSection === s.key ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-9 gap-1.5"
|
||||||
|
onClick={() => setActiveSection(s.key)}
|
||||||
|
>
|
||||||
|
<s.icon className="h-3.5 w-3.5" />
|
||||||
|
{s.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
{activeSection === 'work' && (
|
||||||
|
<div className="space-y-4 max-w-2xl">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mb-1">Problem Description</p>
|
||||||
|
<p className="text-sm">{ticket.problemDescription}</p>
|
||||||
|
</div>
|
||||||
|
{ticket.conditionIn && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mb-1">Condition at Intake</p>
|
||||||
|
<Badge variant="outline">{ticket.conditionIn}</Badge>
|
||||||
|
{ticket.conditionInNotes && <p className="text-sm text-muted-foreground mt-1">{ticket.conditionInNotes}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ticket.technicianNotes && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mb-1">Technician Notes</p>
|
||||||
|
<p className="text-sm">{ticket.technicianNotes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Separator />
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{ticket.estimatedCost && <div><p className="text-xs text-muted-foreground">Estimate</p><p className="text-lg font-bold">${ticket.estimatedCost}</p></div>}
|
||||||
|
{ticket.actualCost && <div><p className="text-xs text-muted-foreground">Actual</p><p className="text-lg font-bold">${ticket.actualCost}</p></div>}
|
||||||
|
{ticket.promisedDate && <div><p className="text-xs text-muted-foreground">Promised</p><p className="text-lg font-bold">{new Date(ticket.promisedDate).toLocaleDateString()}</p></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'parts' && (
|
||||||
|
<div className="space-y-4 max-w-2xl">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium">Line Items</h3>
|
||||||
|
<Button size="sm" onClick={() => setAddingItem(true)}>
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1" />Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Template quick-add */}
|
||||||
|
{templates.length > 0 && (
|
||||||
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
|
{templates.slice(0, 8).map(t => (
|
||||||
|
<Button
|
||||||
|
key={t.id}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 text-xs"
|
||||||
|
onClick={async () => {
|
||||||
|
await repairLineItemMutations.create(ticketId, {
|
||||||
|
itemType: t.itemType,
|
||||||
|
description: t.description ?? t.name,
|
||||||
|
qty: 1,
|
||||||
|
unitPrice: parseFloat(t.defaultPrice),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['repair-tickets', ticketId, 'line-items'] })
|
||||||
|
toast.success(`Added ${t.name}`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{addingItem && (
|
||||||
|
<div className="flex items-end gap-2 p-3 border rounded-lg bg-muted/30">
|
||||||
|
<div className="w-24 space-y-1">
|
||||||
|
<Label className="text-xs">Type</Label>
|
||||||
|
<Select value={newItemType} onValueChange={setNewItemType}>
|
||||||
|
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="labor">Labor</SelectItem>
|
||||||
|
<SelectItem value="part">Part</SelectItem>
|
||||||
|
<SelectItem value="flat_rate">Flat Rate</SelectItem>
|
||||||
|
<SelectItem value="misc">Misc</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<Label className="text-xs">Description</Label>
|
||||||
|
<Input className="h-8" value={newItemDesc} onChange={(e) => setNewItemDesc(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="w-16 space-y-1">
|
||||||
|
<Label className="text-xs">Qty</Label>
|
||||||
|
<Input className="h-8" type="number" value={newItemQty} onChange={(e) => setNewItemQty(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="w-24 space-y-1">
|
||||||
|
<Label className="text-xs">Price</Label>
|
||||||
|
<Input className="h-8" type="number" step="0.01" value={newItemPrice} onChange={(e) => setNewItemPrice(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<Button size="sm" className="h-8" onClick={() => addLineItemMutation.mutate()} disabled={!newItemDesc}>Add</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8" onClick={() => setAddingItem(false)}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{lineItems.map((item) => (
|
||||||
|
<div key={item.id} className="flex items-center justify-between py-2 px-3 rounded hover:bg-muted/50">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="text-[10px] h-5">{item.itemType}</Badge>
|
||||||
|
<span className="text-sm">{item.description}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">x{item.qty}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">${item.totalPrice}</span>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => deleteLineItemMutation.mutate(item.id)}>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{lineItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex justify-between px-3 py-2 font-semibold">
|
||||||
|
<span>Total</span>
|
||||||
|
<span>${lineItems.reduce((s, i) => s + parseFloat(i.totalPrice), 0).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{lineItems.length === 0 && !addingItem && (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">No line items yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'photos' && (
|
||||||
|
<TicketPhotos ticketId={ticketId} currentStatus={ticket.status} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeSection === 'notes' && (
|
||||||
|
<div className="space-y-4 max-w-2xl">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Textarea
|
||||||
|
value={newNote}
|
||||||
|
onChange={(e) => setNewNote(e.target.value)}
|
||||||
|
placeholder="Add a note..."
|
||||||
|
rows={2}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => addNoteMutation.mutate()}
|
||||||
|
disabled={!newNote.trim() || addNoteMutation.isPending}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{notes.map((note) => (
|
||||||
|
<div key={note.id} className="border rounded-lg p-3">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium">{note.authorName}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{new Date(note.createdAt).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{note.content}</p>
|
||||||
|
{note.visibility === 'internal' && <Badge variant="outline" className="text-[10px] mt-1">Internal</Badge>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{notes.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">No notes yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RepairDeskView } from './repair-desk-view'
|
import { RepairDeskView } from './repair-desk-view'
|
||||||
|
import { RepairTechView } from './repair-tech-view'
|
||||||
|
|
||||||
interface RepairsStationProps {
|
interface RepairsStationProps {
|
||||||
permissions: string[]
|
permissions: string[]
|
||||||
@@ -7,6 +8,11 @@ interface RepairsStationProps {
|
|||||||
export function RepairsStation({ permissions }: RepairsStationProps) {
|
export function RepairsStation({ permissions }: RepairsStationProps) {
|
||||||
const canEdit = permissions.includes('repairs.edit')
|
const canEdit = permissions.includes('repairs.edit')
|
||||||
|
|
||||||
// TODO: Phase 3 — if user only has repairs.view (technician), show RepairTechView instead
|
// Front desk (can edit/intake) gets the desk view with queue + intake
|
||||||
return <RepairDeskView canEdit={canEdit} />
|
// Technician (view only) gets the focused workbench
|
||||||
|
if (canEdit) {
|
||||||
|
return <RepairDeskView canEdit={canEdit} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RepairTechView />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user