Dev #18
@@ -0,0 +1,276 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { regions as regionsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import type {
|
||||
RegionResponse,
|
||||
UserResponse,
|
||||
RegionDashboardResponse,
|
||||
RegionHierarchyResponse,
|
||||
} from '@/types/api'
|
||||
|
||||
interface RegionManageSheetProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
region: RegionResponse | null
|
||||
}
|
||||
|
||||
function currentPeriod() {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function RegionManageSheet({ open, onClose, region }: RegionManageSheetProps) {
|
||||
const [period, setPeriod] = useState(currentPeriod())
|
||||
const [targetValue, setTargetValue] = useState('')
|
||||
const [budgetAmount, setBudgetAmount] = useState('')
|
||||
const [ruleConfig, setRuleConfig] = useState('')
|
||||
const [agencyUserId, setAgencyUserId] = useState('')
|
||||
const [agencyUsers, setAgencyUsers] = useState<UserResponse[]>([])
|
||||
const [dashboard, setDashboard] = useState<RegionDashboardResponse | null>(null)
|
||||
const [hierarchy, setHierarchy] = useState<RegionHierarchyResponse | null>(null)
|
||||
const [saving, setSaving] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !region) return
|
||||
|
||||
setPeriod(currentPeriod())
|
||||
setTargetValue('')
|
||||
setBudgetAmount('')
|
||||
setRuleConfig('')
|
||||
setAgencyUserId('')
|
||||
|
||||
usersApi.list({ page: 1, pageSize: 200 })
|
||||
.then((res) => setAgencyUsers(getPaginatedItems(res).filter((u) => u.roleName === 'RegionalAgency')))
|
||||
.catch(() => toast.add({ title: 'Failed to load agency users', type: 'error' }))
|
||||
|
||||
regionsApi.hierarchy(region.id)
|
||||
.then(setHierarchy)
|
||||
.catch(() => toast.add({ title: 'Failed to load hierarchy', type: 'error' }))
|
||||
}, [open, region])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !region) return
|
||||
regionsApi.dashboard(region.id, period)
|
||||
.then(setDashboard)
|
||||
.catch(() => toast.add({ title: 'Failed to load dashboard', type: 'error' }))
|
||||
}, [open, region, period])
|
||||
|
||||
if (!region) return null
|
||||
|
||||
async function handleSetTarget() {
|
||||
const value = parseFloat(targetValue)
|
||||
if (Number.isNaN(value)) return
|
||||
setSaving('target')
|
||||
try {
|
||||
await regionsApi.setTarget(region!.id, period, value)
|
||||
toast.add({ title: 'Target updated', type: 'success' })
|
||||
regionsApi.dashboard(region!.id, period).then(setDashboard)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to set target', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetBudget() {
|
||||
const value = parseFloat(budgetAmount)
|
||||
if (Number.isNaN(value)) return
|
||||
setSaving('budget')
|
||||
try {
|
||||
await regionsApi.setBudget(region!.id, period, value)
|
||||
toast.add({ title: 'Budget updated', type: 'success' })
|
||||
regionsApi.dashboard(region!.id, period).then(setDashboard)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to set budget', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetTaxRule() {
|
||||
if (!ruleConfig.trim()) return
|
||||
setSaving('tax-rule')
|
||||
try {
|
||||
await regionsApi.setTaxRule(region!.id, ruleConfig)
|
||||
toast.add({ title: 'Tax rule updated', type: 'success' })
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to set tax rule', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMapAgency() {
|
||||
if (!agencyUserId) return
|
||||
setSaving('agency')
|
||||
try {
|
||||
await regionsApi.mapAgency(region!.id, agencyUserId)
|
||||
toast.add({ title: 'Agency mapped to region', type: 'success' })
|
||||
setAgencyUserId('')
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to map agency', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="overflow-y-auto sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage {region.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Targets, budgets, tax rules, agency mapping, and dashboard for this region.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="period">Period</Label>
|
||||
<Input
|
||||
id="period"
|
||||
type="month"
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value)}
|
||||
className="w-40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Regional Target</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 500000"
|
||||
value={targetValue}
|
||||
onChange={(e) => setTargetValue(e.target.value)}
|
||||
/>
|
||||
<Button type="button" onClick={handleSetTarget} disabled={saving === 'target'}>
|
||||
{saving === 'target' ? 'Saving…' : 'Set Target'}
|
||||
</Button>
|
||||
</div>
|
||||
{dashboard?.targetValue != null && (
|
||||
<p className="text-xs text-muted-foreground">Current: {dashboard.targetValue}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Regional Budget</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 200000"
|
||||
value={budgetAmount}
|
||||
onChange={(e) => setBudgetAmount(e.target.value)}
|
||||
/>
|
||||
<Button type="button" onClick={handleSetBudget} disabled={saving === 'budget'}>
|
||||
{saving === 'budget' ? 'Saving…' : 'Set Budget'}
|
||||
</Button>
|
||||
</div>
|
||||
{dashboard?.budgetAmount != null && (
|
||||
<p className="text-xs text-muted-foreground">Current: {dashboard.budgetAmount}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Tax Rule Config (JSON)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder='e.g. {"rate": 0.15}'
|
||||
value={ruleConfig}
|
||||
onChange={(e) => setRuleConfig(e.target.value)}
|
||||
/>
|
||||
<Button type="button" onClick={handleSetTaxRule} disabled={saving === 'tax-rule'}>
|
||||
{saving === 'tax-rule' ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Map Regional Agency</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select value={agencyUserId} onValueChange={(v) => setAgencyUserId(v ?? '')}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an agency user">
|
||||
{agencyUsers.find((u) => u.id === agencyUserId)?.fullName}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{agencyUsers.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.fullName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button type="button" onClick={handleMapAgency} disabled={saving === 'agency'}>
|
||||
{saving === 'agency' ? 'Saving…' : 'Map'}
|
||||
</Button>
|
||||
</div>
|
||||
{agencyUsers.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">No users with the RegionalAgency role found.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Hierarchy</Label>
|
||||
{hierarchy && hierarchy.routes.length > 0 ? (
|
||||
<ul className="text-sm space-y-1">
|
||||
{hierarchy.routes.map((r) => (
|
||||
<li key={r.routeId}>
|
||||
{r.routeName} — {r.storeIds.length} store{r.storeIds.length === 1 ? '' : 's'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">No routes under this region yet.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Dashboard ({period})</Label>
|
||||
{dashboard ? (
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>Orders: {dashboard.orderCount}</div>
|
||||
<div>Actual Sales: {dashboard.actualSales}</div>
|
||||
<div>Target: {dashboard.targetValue ?? '—'}</div>
|
||||
<div>Budget: {dashboard.budgetAmount ?? '—'}</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">Loading dashboard…</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api
|
||||
import type { RegionResponse } from '@/types/api'
|
||||
|
||||
import { RegionSheet } from './region-sheet'
|
||||
import { RegionManageSheet } from './region-manage-sheet'
|
||||
|
||||
export function RegionsClient() {
|
||||
const [data, setData] = useState<RegionResponse[]>([])
|
||||
@@ -24,6 +25,9 @@ export function RegionsClient() {
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<RegionResponse | null>(null)
|
||||
|
||||
const [manageOpen, setManageOpen] = useState(false)
|
||||
const [managing, setManaging] = useState<RegionResponse | null>(null)
|
||||
|
||||
const fetchData = useCallback(async (p: number, ps: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -51,6 +55,11 @@ export function RegionsClient() {
|
||||
setSheetOpen(true)
|
||||
}
|
||||
|
||||
function openManage(region: RegionResponse) {
|
||||
setManaging(region)
|
||||
setManageOpen(true)
|
||||
}
|
||||
|
||||
async function toggleActive(region: RegionResponse) {
|
||||
try {
|
||||
await regionsApi.update(region.id, { isActive: !region.isActive })
|
||||
@@ -103,6 +112,11 @@ export function RegionsClient() {
|
||||
],
|
||||
rowActions: {
|
||||
actions: () => [
|
||||
{
|
||||
action: 'confirm',
|
||||
label: 'Manage targets, budgets & agencies',
|
||||
onClick: openManage,
|
||||
},
|
||||
{
|
||||
action: 'edit',
|
||||
label: 'Edit region',
|
||||
@@ -122,7 +136,7 @@ export function RegionsClient() {
|
||||
},
|
||||
],
|
||||
},
|
||||
}), [pageSize, openEdit, toggleActive])
|
||||
}), [pageSize, openEdit, openManage, toggleActive])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -156,6 +170,12 @@ export function RegionsClient() {
|
||||
region={editing}
|
||||
onSuccess={() => fetchData(page, pageSize)}
|
||||
/>
|
||||
|
||||
<RegionManageSheet
|
||||
open={manageOpen}
|
||||
onClose={() => setManageOpen(false)}
|
||||
region={managing}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user