Consolidate region management APIs

This commit is contained in:
2026-09-01 15:34:58 +05:30
parent 82a32de966
commit 7cf272e311
16 changed files with 880 additions and 509 deletions
@@ -0,0 +1,265 @@
'use client'
import { useEffect, useState } from 'react'
import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { FieldGroup } from '@/components/ui/field'
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 } from '@/types/api'
function currentPeriod() {
const now = new Date()
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
}
const schema = z.object({
regionId: z.string().min(1, 'Region is required'),
period: z.string(),
targetValue: z.string(),
budgetAmount: z.string(),
ruleConfig: z.string(),
agencyUserId: z.string(),
})
type FormValues = z.infer<typeof schema>
export interface RegionManageInitialValues {
regionId: string
period?: string
targetValue?: number
budgetAmount?: number
ruleConfig?: string
agencyUserId?: string
}
interface RegionManageEntrySheetProps {
open: boolean
onClose: () => void
onSuccess: () => void
initial?: RegionManageInitialValues | null
}
export function RegionManageEntrySheet({ open, onClose, onSuccess, initial }: RegionManageEntrySheetProps) {
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
const [agencyUsers, setAgencyUsers] = useState<UserResponse[]>([])
const isEdit = !!initial
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
regionId: '',
period: currentPeriod(),
targetValue: '',
budgetAmount: '',
ruleConfig: '',
agencyUserId: '',
},
})
useEffect(() => {
if (!open) return
form.reset({
regionId: initial?.regionId ?? '',
period: initial?.period ?? currentPeriod(),
targetValue: initial?.targetValue != null ? String(initial.targetValue) : '',
budgetAmount: initial?.budgetAmount != null ? String(initial.budgetAmount) : '',
ruleConfig: initial?.ruleConfig ?? '',
agencyUserId: initial?.agencyUserId ?? '',
})
Promise.all([
regionsApi.list({ page: 1, pageSize: 200 }),
usersApi.list({ page: 1, pageSize: 200 }),
])
.then(([r, u]) => {
setRegionsList(getPaginatedItems(r).filter((x) => x.isActive))
setAgencyUsers(getPaginatedItems(u).filter((x) => x.roleName === 'RegionalAgency'))
})
.catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }))
}, [open, initial])
async function onSubmit(values: FormValues) {
const body: {
period?: string
targetValue?: number
budgetAmount?: number
agencyUserId?: string
ruleConfig?: string
} = {}
const target = values.targetValue.trim() ? parseFloat(values.targetValue) : NaN
const budget = values.budgetAmount.trim() ? parseFloat(values.budgetAmount) : NaN
if (!Number.isNaN(target)) {
body.period = values.period
body.targetValue = target
}
if (!Number.isNaN(budget)) {
body.period = values.period
body.budgetAmount = budget
}
if (values.ruleConfig.trim()) body.ruleConfig = values.ruleConfig
if (values.agencyUserId) body.agencyUserId = values.agencyUserId
if (Object.keys(body).length === 0) {
toast.add({ title: 'Nothing to save', description: 'Fill in at least one field.', type: 'error' })
return
}
try {
await regionsApi.manage(values.regionId, body)
toast.add({ title: isEdit ? 'Region settings updated' : 'Entry saved', type: 'success' })
onSuccess()
onClose()
} catch (err) {
toast.add({ title: 'Failed to save entry', description: getApiError(err), type: 'error' })
}
}
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{isEdit ? 'Edit Manage Entry' : 'Add Manage Entry'}</DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<FieldGroup>
<FormField
control={form.control}
name="regionId"
label="Region"
required
render={({ field, invalid }) => (
<Select value={field.value as string} onValueChange={field.onChange} disabled={isEdit}>
<SelectTrigger aria-invalid={invalid}>
<SelectValue placeholder="Select a region">
{regionsList.find((r) => r.id === field.value)?.name}
</SelectValue>
</SelectTrigger>
<SelectContent>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<FormField
control={form.control}
name="period"
label="Period"
render={({ field }) => (
<Input
type="month"
value={field.value as string}
onChange={field.onChange}
className="w-40"
/>
)}
/>
<Separator />
<FormField
control={form.control}
name="targetValue"
label="Regional Target"
render={({ field }) => (
<Input
type="number"
value={field.value as string}
onChange={field.onChange}
placeholder="e.g. 500000"
/>
)}
/>
<FormField
control={form.control}
name="budgetAmount"
label="Regional Budget"
render={({ field }) => (
<Input
type="number"
value={field.value as string}
onChange={field.onChange}
placeholder="e.g. 200000"
/>
)}
/>
<Separator />
<FormField
control={form.control}
name="ruleConfig"
label="Regional Rule Config"
render={({ field }) => (
<Input
value={field.value as string}
onChange={field.onChange}
placeholder='0.15'
/>
)}
/>
<Separator />
<FormField
control={form.control}
name="agencyUserId"
label="Map Regional Agency"
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Select an agency user">
{agencyUsers.find((u) => u.id === field.value)?.fullName}
</SelectValue>
</SelectTrigger>
<SelectContent>
{agencyUsers.map((u) => (
<SelectItem key={u.id} value={u.id}>{u.fullName}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{agencyUsers.length === 0 && (
<p className="text-xs text-muted-foreground">No users with the RegionalAgency role found.</p>
)}
</FieldGroup>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving…' : 'Save'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,218 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { HugeiconsIcon } from '@hugeicons/react'
import { Add01Icon } from '@hugeicons/core-free-icons'
import { DataTable } from '@/components/shared/data-table'
import type { TableConfig } from '@/components/shared/data-table'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { regions as regionsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { RegionManageEntryResponse, UserResponse } from '@/types/api'
import { RegionManageEntrySheet, type RegionManageInitialValues } from './region-manage-entry-sheet'
interface RegionManageRow {
regionId: string
regionName: string
period: string | null
targetValue: number | null
budgetAmount: number | null
ruleConfig: string | null
agencyUserIds: string[]
entries: RegionManageEntryResponse[]
}
function groupByRegion(entries: RegionManageEntryResponse[]): RegionManageRow[] {
const byRegion = new Map<string, RegionManageEntryResponse[]>()
for (const e of entries) {
const list = byRegion.get(e.regionId) ?? []
list.push(e)
byRegion.set(e.regionId, list)
}
const latestOf = (list: RegionManageEntryResponse[], type: RegionManageEntryResponse['entryType']) =>
list
.filter((e) => e.entryType === type)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]
return Array.from(byRegion.entries()).map(([regionId, list]) => {
const target = latestOf(list, 'Target')
const budget = latestOf(list, 'Budget')
const taxRule = latestOf(list, 'TaxRule')
const agencyMaps = list.filter((e) => e.entryType === 'AgencyMap')
return {
regionId,
regionName: list[0].regionName,
period: target?.period ?? budget?.period ?? null,
targetValue: target?.amount ?? null,
budgetAmount: budget?.amount ?? null,
ruleConfig: taxRule?.ruleConfig ?? null,
agencyUserIds: agencyMaps.map((m) => m.agencyUserId).filter((id): id is string => !!id),
entries: list,
}
})
}
export function RegionManageListClient() {
const [entries, setEntries] = useState<RegionManageEntryResponse[]>([])
const [loading, setLoading] = useState(true)
const [agencyUsers, setAgencyUsers] = useState<UserResponse[]>([])
const [sheetOpen, setSheetOpen] = useState(false)
const [editingInitial, setEditingInitial] = useState<RegionManageInitialValues | null>(null)
const fetchData = useCallback(async () => {
setLoading(true)
try {
const res = await regionsApi.manageEntries()
setEntries(res)
} catch (err) {
toast.add({ title: 'Failed to load manage entries', description: getApiError(err), type: 'error' })
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
useEffect(() => {
usersApi.list({ page: 1, pageSize: 200 })
.then((res) => setAgencyUsers(getPaginatedItems(res)))
.catch(() => toast.add({ title: 'Failed to load users', type: 'error' }))
}, [])
const rows = useMemo(() => groupByRegion(entries), [entries])
function openAdd() {
setEditingInitial(null)
setSheetOpen(true)
}
function openEdit(row: RegionManageRow) {
setEditingInitial({
regionId: row.regionId,
period: row.period ?? undefined,
targetValue: row.targetValue ?? undefined,
budgetAmount: row.budgetAmount ?? undefined,
ruleConfig: row.ruleConfig ?? undefined,
agencyUserId: row.agencyUserIds[0] ?? undefined,
})
setSheetOpen(true)
}
async function handleClear(row: RegionManageRow) {
try {
await Promise.all(
row.entries.map((e) => regionsApi.removeManageEntry(e.entryType, e.id))
)
toast.add({ title: 'Region settings cleared', type: 'success' })
fetchData()
} catch (err) {
toast.add({ title: 'Failed to clear settings', description: getApiError(err), type: 'error' })
}
}
const tableConfig = useMemo<TableConfig<RegionManageRow>>(() => ({
rowKey: 'regionId',
stickyHeader: true,
emptyMessage: 'No manage entries yet. Add one to get started.',
pagination: { enabled: true, pageSize: 20, pageSizeOptions: [10, 20, 50] },
columns: [
{
key: 'regionName',
title: 'Region',
fields: [{ key: 'regionName', primary: true }],
minWidth: 160,
},
{
key: 'period',
title: 'Period',
fields: ['period'],
width: 100,
cellRender: (row) => row.period ?? '—',
},
{
key: 'targetValue',
title: 'Regional Target',
fields: ['targetValue'],
width: 140,
cellRender: (row) => row.targetValue ?? '—',
},
{
key: 'budgetAmount',
title: 'Regional Budget',
fields: ['budgetAmount'],
width: 140,
cellRender: (row) => row.budgetAmount ?? '—',
},
{
key: 'ruleConfig',
title: 'Regional Rule Config',
fields: ['ruleConfig'],
minWidth: 180,
cellRender: (row) => row.ruleConfig ?? '—',
},
{
key: 'agencyUserIds',
title: 'Map Regional Agency',
fields: ['agencyUserIds'],
minWidth: 180,
cellRender: (row) => {
if (row.agencyUserIds.length === 0) return '—'
return row.agencyUserIds
.map((id) => agencyUsers.find((u) => u.id === id)?.fullName ?? id)
.join(', ')
},
},
],
rowActions: {
actions: () => [
{
action: 'edit',
label: 'Edit region settings',
onClick: openEdit,
},
{
action: 'deactivate',
label: 'Clear region settings',
onClick: handleClear,
},
],
},
}), [agencyUsers])
return (
<>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-foreground">Region Manage</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Targets, budgets, tax rules, and agency mappings across regions
</p>
</div>
<Button onClick={openAdd}>
<HugeiconsIcon icon={Add01Icon} size={16} />
Add Entry
</Button>
</div>
<DataTable
config={tableConfig}
data={rows}
loading={loading}
/>
<RegionManageEntrySheet
open={sheetOpen}
onClose={() => setSheetOpen(false)}
onSuccess={fetchData}
initial={editingInitial}
/>
</>
)
}
@@ -0,0 +1,5 @@
import { RegionManageListClient } from './_components/region-manage-list-client'
export default function RegionManagePage() {
return <RegionManageListClient />
}
@@ -1,276 +0,0 @@
'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,7 +13,6 @@ 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[]>([])
@@ -25,9 +24,6 @@ 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 {
@@ -55,11 +51,6 @@ 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 })
@@ -112,11 +103,6 @@ export function RegionsClient() {
],
rowActions: {
actions: () => [
{
action: 'confirm',
label: 'Manage targets, budgets & agencies',
onClick: openManage,
},
{
action: 'edit',
label: 'Edit region',
@@ -136,7 +122,7 @@ export function RegionsClient() {
},
],
},
}), [pageSize, openEdit, openManage, toggleActive])
}), [pageSize, openEdit, toggleActive])
return (
<>
@@ -170,12 +156,6 @@ export function RegionsClient() {
region={editing}
onSuccess={() => fetchData(page, pageSize)}
/>
<RegionManageSheet
open={manageOpen}
onClose={() => setManageOpen(false)}
region={managing}
/>
</>
)
}
+1
View File
@@ -138,6 +138,7 @@ export const NAV_GROUPS: NavGroup[] = [
icon: Building02Icon,
items: [
{ title: 'Regions', href: '/network/regions', icon: Location01Icon, description: 'Geographic territories the distribution network operates in', permission: 'regions.view' },
{ title: 'Region Manage', href: '/network/region-manage', icon: ClipboardListIcon, description: 'Targets, budgets, tax rules, and agency mapping per region', permission: 'regions.view' },
{ title: 'Routes', href: '/network/routes', icon: Route01Icon, description: 'Delivery paths within a region that rides are assigned to', permission: 'routes.view' },
{ title: 'Vehicles', href: '/network/vehicles', icon: Car01Icon, description: 'Fleet vehicles available for delivery rides', permission: 'vehicles.view' },
{ title: 'Warehouses', href: '/network/warehouses', icon: Building02Icon, description: 'Storage locations where stock is held and loaded from', permission: 'warehouses.view' },
+13 -13
View File
@@ -2,12 +2,11 @@ import { api } from '@/lib/api'
import type {
PaginatedResponse,
RegionResponse,
RegionalTargetResponse,
RegionalBudgetResponse,
RegionAgencyMapResponse,
RegionalTaxRuleResponse,
ManageRegionResponse,
RegionHierarchyResponse,
RegionDashboardResponse,
RegionManageEntryResponse,
RegionManageEntryType,
RouteResponse,
RouteVisitResponse,
VehicleResponse,
@@ -104,17 +103,18 @@ export const regions = {
remove: (id: string) =>
api.delete(`/regions/${id}`),
setTarget: (id: string, period: string, targetValue: number) =>
api.put<RegionalTargetResponse>(`/regions/${id}/targets/${period}`, { targetValue }).then(r => r.data),
// Consolidated replacement for the old separate setTarget/setBudget/mapAgency/setTaxRule
// calls — pass only the fields you want to change (Period is required alongside targetValue
// and/or budgetAmount).
manage: (id: string, body: { period?: string; targetValue?: number; budgetAmount?: number; agencyUserId?: string; ruleConfig?: string }) =>
api.put<ManageRegionResponse>(`/regions/${id}/manage`, body).then(r => r.data),
setBudget: (id: string, period: string, budgetAmount: number) =>
api.put<RegionalBudgetResponse>(`/regions/${id}/budgets/${period}`, { budgetAmount }).then(r => r.data),
// Flat CRUD-list view over targets/budgets/tax-rules/agency-maps across all regions.
manageEntries: () =>
api.get<RegionManageEntryResponse[]>('/regions/manage-entries').then(r => r.data),
mapAgency: (id: string, agencyUserId: string) =>
api.post<RegionAgencyMapResponse>(`/regions/${id}/agencies`, { agencyUserId }).then(r => r.data),
setTaxRule: (id: string, ruleConfig: string) =>
api.put<RegionalTaxRuleResponse>(`/regions/${id}/tax-rule`, { ruleConfig }).then(r => r.data),
removeManageEntry: (entryType: RegionManageEntryType, id: string) =>
api.delete(`/regions/manage-entries/${entryType}/${id}`),
hierarchy: (id: string) =>
api.get<RegionHierarchyResponse>(`/regions/${id}/hierarchy`).then(r => r.data),
+24
View File
@@ -125,6 +125,14 @@ export interface RegionalTaxRuleResponse {
ruleConfig: string
}
export interface ManageRegionResponse {
regionId: string
target: RegionalTargetResponse | null
budget: RegionalBudgetResponse | null
agencyMap: RegionAgencyMapResponse | null
taxRule: RegionalTaxRuleResponse | null
}
export interface RegionHierarchyRouteItem {
routeId: string
routeName: string
@@ -145,6 +153,22 @@ export interface RegionDashboardResponse {
orderCount: number
actualSales: number
lastOrderDate: string | null
taxRuleConfig: string | null
agencyUserIds: string[]
}
export type RegionManageEntryType = 'Target' | 'Budget' | 'TaxRule' | 'AgencyMap'
export interface RegionManageEntryResponse {
id: string
regionId: string
regionName: string
entryType: RegionManageEntryType
period: string | null
amount: number | null
ruleConfig: string | null
agencyUserId: string | null
createdAt: string
}
// ─── Vehicles / Warehouses ───────────────────────────────────────────────────