Shared TableToolbar and RowActions; add create pages for orders, Rx, orgs, stock, team, and procurement. Co-authored-by: Cursor <cursoragent@cursor.com>
288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { toast } from 'sonner'
|
|
import { CheckCircle2, Plus, Truck, XCircle } from 'lucide-react'
|
|
import { PageShell } from '@/layouts/PageShell'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ClearFiltersButton, FilterSelect, TableToolbar } from '@/components/table/TableToolbar'
|
|
import { RowActions } from '@/components/table/RowActions'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Label } from '@/components/ui/label'
|
|
import { SelectNative } from '@/components/ui/select-native'
|
|
import { useAuthStore } from '@/zustand/authStore'
|
|
import { branches, orders as seedOrders, riders } from '@/mocks/data'
|
|
import { formatEtb } from '@/lib/utils'
|
|
import type { Order } from '@/types'
|
|
|
|
export function OrdersPage() {
|
|
const { t } = useTranslation()
|
|
const user = useAuthStore((s) => s.user)
|
|
const [rows, setRows] = useState<Order[]>([...seedOrders])
|
|
const [q, setQ] = useState('')
|
|
const [status, setStatus] = useState('all')
|
|
const [fulfillment, setFulfillment] = useState('all')
|
|
const [branchFilter, setBranchFilter] = useState('all')
|
|
const [channel, setChannel] = useState('all')
|
|
const [assignTarget, setAssignTarget] = useState<Order | null>(null)
|
|
const [riderId, setRiderId] = useState('')
|
|
|
|
const canPickBranch = user?.role !== 'pharmacist'
|
|
|
|
const filtered = useMemo(() => {
|
|
let list = rows
|
|
if (user?.role === 'pharmacist' && user.branchId) {
|
|
list = list.filter((o) => o.branchId === user.branchId)
|
|
} else if (branchFilter !== 'all') {
|
|
list = list.filter((o) => o.branchId === branchFilter)
|
|
}
|
|
if (status !== 'all') list = list.filter((o) => o.status === status)
|
|
if (fulfillment !== 'all') list = list.filter((o) => o.fulfillment === fulfillment)
|
|
if (channel !== 'all') list = list.filter((o) => o.channel === channel)
|
|
if (q) {
|
|
const needle = q.toLowerCase()
|
|
list = list.filter(
|
|
(o) => o.customerName.toLowerCase().includes(needle) || o.id.toLowerCase().includes(needle),
|
|
)
|
|
}
|
|
return list
|
|
}, [rows, user, q, status, fulfillment, branchFilter, channel])
|
|
|
|
const filtersActive =
|
|
status !== 'all' || fulfillment !== 'all' || branchFilter !== 'all' || channel !== 'all' || !!q
|
|
|
|
const confirm = (orderId: string) => {
|
|
setRows((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: 'confirmed' } : o)))
|
|
const target = seedOrders.find((o) => o.id === orderId)
|
|
if (target) target.status = 'confirmed'
|
|
toast.success('Order confirmed')
|
|
}
|
|
|
|
const cancel = (orderId: string) => {
|
|
setRows((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: 'cancelled' } : o)))
|
|
toast.message('Order cancelled')
|
|
}
|
|
|
|
const saveAssign = () => {
|
|
if (!assignTarget || !riderId) return
|
|
setRows((prev) =>
|
|
prev.map((o) =>
|
|
o.id === assignTarget.id
|
|
? { ...o, assignedRiderId: riderId, status: 'out_for_delivery' }
|
|
: o,
|
|
),
|
|
)
|
|
const target = seedOrders.find((o) => o.id === assignTarget.id)
|
|
if (target) {
|
|
target.assignedRiderId = riderId
|
|
target.status = 'out_for_delivery'
|
|
}
|
|
toast.success('Delivery person assigned')
|
|
setAssignTarget(null)
|
|
setRiderId('')
|
|
}
|
|
|
|
return (
|
|
<PageShell title={t('orders.title')} subtitle={t('orders.subtitle')}>
|
|
<Card className="overflow-hidden">
|
|
<TableToolbar
|
|
search={q}
|
|
onSearchChange={setQ}
|
|
searchPlaceholder="Search ID or customer…"
|
|
resultCount={filtered.length}
|
|
filters={
|
|
<>
|
|
<FilterSelect
|
|
label="Status"
|
|
value={status}
|
|
onChange={setStatus}
|
|
options={[
|
|
{ value: 'all', label: 'All statuses' },
|
|
{ value: 'confirmed', label: 'Confirmed' },
|
|
{ value: 'picking', label: 'Picking' },
|
|
{ value: 'out_for_delivery', label: 'Out for delivery' },
|
|
{ value: 'cancelled', label: 'Cancelled' },
|
|
]}
|
|
/>
|
|
<FilterSelect
|
|
label="Fulfillment"
|
|
value={fulfillment}
|
|
onChange={setFulfillment}
|
|
options={[
|
|
{ value: 'all', label: 'All types' },
|
|
{ value: 'delivery', label: 'Delivery' },
|
|
{ value: 'pickup', label: 'Pickup' },
|
|
]}
|
|
/>
|
|
{canPickBranch ? (
|
|
<FilterSelect
|
|
label="Branch"
|
|
value={branchFilter}
|
|
onChange={setBranchFilter}
|
|
options={[
|
|
{ value: 'all', label: t('common.allBranches') },
|
|
...branches.map((b) => ({ value: b.id, label: b.name })),
|
|
]}
|
|
/>
|
|
) : null}
|
|
<FilterSelect
|
|
label="Channel"
|
|
value={channel}
|
|
onChange={setChannel}
|
|
options={[
|
|
{ value: 'all', label: 'All channels' },
|
|
{ value: 'web', label: 'Web' },
|
|
{ value: 'mobile', label: 'Mobile' },
|
|
{ value: 'telegram', label: 'Telegram' },
|
|
{ value: 'b2b', label: 'B2B' },
|
|
]}
|
|
/>
|
|
<ClearFiltersButton
|
|
visible={filtersActive}
|
|
onClick={() => {
|
|
setQ('')
|
|
setStatus('all')
|
|
setFulfillment('all')
|
|
setBranchFilter('all')
|
|
setChannel('all')
|
|
}}
|
|
/>
|
|
</>
|
|
}
|
|
actions={
|
|
<Button asChild className="h-10 gap-2 shadow-none">
|
|
<Link to="/orders/new">
|
|
<Plus className="h-4 w-4" />
|
|
New order
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead>ID</TableHead>
|
|
<TableHead>Customer</TableHead>
|
|
<TableHead>{t('common.branch')}</TableHead>
|
|
<TableHead>Fulfillment</TableHead>
|
|
<TableHead>{t('common.status')}</TableHead>
|
|
<TableHead>Total</TableHead>
|
|
<TableHead className="text-right">{t('common.actions')}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filtered.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
|
|
{t('common.noResults')}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filtered.map((o) => (
|
|
<TableRow key={o.id}>
|
|
<TableCell className="font-medium">{o.id}</TableCell>
|
|
<TableCell>{o.customerName}</TableCell>
|
|
<TableCell>{branches.find((b) => b.id === o.branchId)?.name}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{o.fulfillment}</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={
|
|
o.status === 'cancelled'
|
|
? 'danger'
|
|
: o.status === 'out_for_delivery'
|
|
? 'warning'
|
|
: 'success'
|
|
}
|
|
>
|
|
{o.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>{formatEtb(o.totalEtb)}</TableCell>
|
|
<TableCell>
|
|
<RowActions
|
|
primary={[
|
|
{
|
|
label: 'Confirm',
|
|
icon: <CheckCircle2 className="h-3.5 w-3.5" />,
|
|
onClick: () => confirm(o.id),
|
|
disabled: o.status === 'confirmed' || o.status === 'cancelled',
|
|
},
|
|
]}
|
|
items={[
|
|
...(o.fulfillment === 'delivery'
|
|
? [
|
|
{
|
|
label: t('orders.assignRider'),
|
|
icon: <Truck className="h-3.5 w-3.5" />,
|
|
onClick: () => {
|
|
setAssignTarget(o)
|
|
setRiderId(o.assignedRiderId ?? '')
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
label: 'Cancel',
|
|
icon: <XCircle className="h-3.5 w-3.5" />,
|
|
variant: 'destructive' as const,
|
|
onClick: () => cancel(o.id),
|
|
disabled: o.status === 'cancelled',
|
|
},
|
|
]}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={!!assignTarget} onOpenChange={(open) => !open && setAssignTarget(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t('orders.assignRider')}</DialogTitle>
|
|
<DialogDescription>
|
|
{assignTarget?.id} · {assignTarget?.customerName}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Delivery person</Label>
|
|
<SelectNative value={riderId} onChange={(e) => setRiderId(e.target.value)}>
|
|
<option value="">Select rider…</option>
|
|
{riders.map((r) => (
|
|
<option key={r.id} value={r.id}>
|
|
{r.name} · {r.zone}
|
|
</option>
|
|
))}
|
|
</SelectNative>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setAssignTarget(null)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button disabled={!riderId} onClick={saveAssign}>
|
|
{t('common.assign')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</PageShell>
|
|
)
|
|
}
|