This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Admin/src/pages/orders/OrdersPage.tsx
kirukib f2aff8c377 Improve list pages with filters, row actions, and create flows
Shared TableToolbar and RowActions; add create pages for orders, Rx, orgs, stock, team, and procurement.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 19:09:41 +03:00

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>
)
}