Add region code support

This commit is contained in:
2026-10-06 15:15:04 +05:30
parent c52ecfdebc
commit b75cd01e90
4 changed files with 105 additions and 46 deletions
@@ -4,7 +4,6 @@ import { useEffect } from 'react'
import { useForm, zodResolver, z } from '@/lib/form' import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field' import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, Dialog,
@@ -20,8 +19,8 @@ import { regions as regionsApi, getApiError } from '@/lib/api/index'
import type { RegionResponse } from '@/types/api' import type { RegionResponse } from '@/types/api'
const schema = z.object({ const schema = z.object({
code: z.string().max(50, 'Code must be under 50 characters').optional(),
name: z.string().min(1, 'Name is required').max(100, 'Name must be under 100 characters'), name: z.string().min(1, 'Name is required').max(100, 'Name must be under 100 characters'),
isActive: z.boolean(),
}) })
type FormValues = z.infer<typeof schema> type FormValues = z.infer<typeof schema>
@@ -30,30 +29,39 @@ interface RegionSheetProps {
open: boolean open: boolean
onClose: () => void onClose: () => void
region: RegionResponse | null region: RegionResponse | null
readOnly?: boolean
onSuccess: () => void onSuccess: () => void
} }
export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetProps) { export function RegionSheet({ open, onClose, region, readOnly = false, onSuccess }: RegionSheetProps) {
const isEdit = !!region const isEdit = !!region
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { name: '', isActive: true }, defaultValues: { code: '', name: '' },
}) })
useEffect(() => { useEffect(() => {
if (open) { if (open) {
form.reset(region ? { name: region.name, isActive: region.isActive } : { name: '', isActive: true }) form.reset(region ? { code: region.code, name: region.name } : { code: '', name: '' })
if (!region) {
// Suggest the next code; the user can overwrite it, or clear it to let the server generate one.
regionsApi.nextCode()
.then((res) => {
if (!form.getValues('code')) form.setValue('code', res.code)
})
.catch(() => {})
}
} }
}, [open, region]) }, [open, region])
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
try { try {
if (isEdit) { if (isEdit) {
await regionsApi.update(region!.id, values) await regionsApi.update(region!.id, { name: values.name, code: values.code?.trim() || undefined })
toast.add({ title: 'Region updated', type: 'success' }) toast.add({ title: 'Region updated', type: 'success' })
} else { } else {
await regionsApi.create(values) await regionsApi.create({ name: values.name, code: values.code?.trim() || undefined })
toast.add({ title: 'Region created', type: 'success' }) toast.add({ title: 'Region created', type: 'success' })
} }
onSuccess() onSuccess()
@@ -67,14 +75,37 @@ export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetPro
<Dialog open={open} onOpenChange={(v) => !v && onClose()}> <Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-lg"> <DialogContent className="overflow-y-auto sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>{isEdit ? 'Edit Region' : 'Add Region'}</DialogTitle> <DialogTitle>{readOnly ? 'View Region' : isEdit ? 'Edit Region' : 'Add Region'}</DialogTitle>
<DialogDescription> <DialogDescription>
{isEdit ? 'Update the region details below.' : 'Add a new geographic region to the distribution network.'} {readOnly ? 'Region details.' : isEdit ? 'Update the region details below.' : 'Add a new geographic region to the distribution network.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6"> <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<FieldGroup> <FieldGroup>
<FormField
control={form.control}
name="code"
label="Region Code"
render={({ field }) => (
<Input
id="code"
name={field.name}
ref={field.ref}
value={(field.value as string) ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. REG-0001"
disabled={readOnly}
/>
)}
/>
{!isEdit && !readOnly && (
<p className="-mt-3 text-xs text-muted-foreground">
Suggested automatically — edit it, or clear it to auto-generate on save.
</p>
)}
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -89,36 +120,20 @@ export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetPro
onChange={field.onChange} onChange={field.onChange}
onBlur={field.onBlur} onBlur={field.onBlur}
placeholder="e.g. Western Province" placeholder="e.g. Western Province"
disabled={readOnly}
aria-invalid={invalid} aria-invalid={invalid}
/> />
)} )}
/> />
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
id="isActive"
checked={field.value as boolean}
onCheckedChange={field.onChange}
/>
<label htmlFor="isActive" className="text-sm font-medium cursor-pointer">
Active
</label>
</div>
)}
/>
</FieldGroup> </FieldGroup>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={onClose}> <Button type="button" variant="outline" onClick={onClose}>
Cancel {readOnly ? 'Close' : 'Cancel'}
</Button> </Button>
<Button type="submit" disabled={form.formState.isSubmitting}> {!readOnly && (<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Region'} {form.formState.isSubmitting ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Region'}
</Button> </Button>)}
</DialogFooter> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
@@ -8,6 +8,7 @@ import { DataTable } from '@/components/shared/data-table'
import type { TableConfig } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { toast } from '@/components/ui/toast' import { toast } from '@/components/ui/toast'
import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index' import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { RegionResponse } from '@/types/api' import type { RegionResponse } from '@/types/api'
@@ -23,6 +24,9 @@ export function RegionsClient() {
const [sheetOpen, setSheetOpen] = useState(false) const [sheetOpen, setSheetOpen] = useState(false)
const [editing, setEditing] = useState<RegionResponse | null>(null) const [editing, setEditing] = useState<RegionResponse | null>(null)
const [viewOnly, setViewOnly] = useState(false)
const [deleting, setDeleting] = useState<RegionResponse | null>(null)
const [deleteBusy, setDeleteBusy] = useState(false)
const fetchData = useCallback(async (p: number, ps: number) => { const fetchData = useCallback(async (p: number, ps: number) => {
setLoading(true) setLoading(true)
@@ -43,24 +47,34 @@ export function RegionsClient() {
function openCreate() { function openCreate() {
setEditing(null) setEditing(null)
setViewOnly(false)
setSheetOpen(true) setSheetOpen(true)
} }
function openEdit(region: RegionResponse) { function openEdit(region: RegionResponse) {
setEditing(region) setEditing(region)
setViewOnly(false)
setSheetOpen(true) setSheetOpen(true)
} }
async function toggleActive(region: RegionResponse) { function openView(region: RegionResponse) {
setEditing(region)
setViewOnly(true)
setSheetOpen(true)
}
async function confirmDelete() {
if (!deleting) return
setDeleteBusy(true)
try { try {
await regionsApi.update(region.id, { isActive: !region.isActive }) await regionsApi.remove(deleting.id)
toast.add({ toast.add({ title: 'Region deleted', type: 'success' })
title: region.isActive ? 'Region deactivated' : 'Region activated', setDeleting(null)
type: 'success',
})
fetchData(page, pageSize) fetchData(page, pageSize)
} catch (err) { } catch (err) {
toast.add({ title: 'Failed to update region', description: getApiError(err), type: 'error' }) toast.add({ title: 'Failed to delete region', description: getApiError(err), type: 'error' })
} finally {
setDeleteBusy(false)
} }
} }
@@ -71,6 +85,13 @@ export function RegionsClient() {
emptyMessage: 'No regions yet. Add one to get started.', emptyMessage: 'No regions yet. Add one to get started.',
pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] },
columns: [ columns: [
{
key: 'code',
title: 'Code',
fields: [{ key: 'code', primary: true }],
sortable: true,
width: 120,
},
{ {
key: 'name', key: 'name',
title: 'Region', title: 'Region',
@@ -103,6 +124,11 @@ export function RegionsClient() {
], ],
rowActions: { rowActions: {
actions: () => [ actions: () => [
{
action: 'view',
label: 'View region',
onClick: openView,
},
{ {
action: 'edit', action: 'edit',
label: 'Edit region', label: 'Edit region',
@@ -110,19 +136,12 @@ export function RegionsClient() {
}, },
{ {
action: 'deactivate', action: 'deactivate',
label: 'Deactivate region', label: 'Delete region',
onClick: toggleActive, onClick: (region: RegionResponse) => setDeleting(region),
visible: (target) => target.isActive,
},
{
action: 'activate',
label: 'Activate region',
onClick: toggleActive,
visible: (target) => !target.isActive,
}, },
], ],
}, },
}), [pageSize, openEdit, toggleActive]) }), [pageSize, openEdit, openView])
return ( return (
<> <>
@@ -154,8 +173,28 @@ export function RegionsClient() {
open={sheetOpen} open={sheetOpen}
onClose={() => setSheetOpen(false)} onClose={() => setSheetOpen(false)}
region={editing} region={editing}
readOnly={viewOnly}
onSuccess={() => fetchData(page, pageSize)} onSuccess={() => fetchData(page, pageSize)}
/> />
<Dialog open={!!deleting} onOpenChange={(v) => !v && !deleteBusy && setDeleting(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Delete region?</DialogTitle>
<DialogDescription>
{deleting ? `${deleting.code} – ${deleting.name} will be removed from the regions list.` : null}
{' '}This action cannot be undone from this screen.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDeleting(null)} disabled={deleteBusy}>
Cancel
</Button>
<Button type="button" variant="destructive" onClick={confirmDelete} disabled={deleteBusy}>
{deleteBusy ? 'Deleting…' : 'Delete'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</> </>
) )
} }
+4
View File
@@ -84,6 +84,7 @@ export interface PageParams {
export interface RegionBody { export interface RegionBody {
name: string name: string
code?: string
isActive?: boolean isActive?: boolean
} }
@@ -91,6 +92,9 @@ export const regions = {
list: (params?: PageParams) => list: (params?: PageParams) =>
api.get<PaginatedResponse<RegionResponse>>('/regions', { params }).then(r => r.data), api.get<PaginatedResponse<RegionResponse>>('/regions', { params }).then(r => r.data),
nextCode: () =>
api.get<{ code: string }>('/regions/next-code').then(r => r.data),
get: (id: string) => get: (id: string) =>
api.get<RegionResponse>(`/regions/${id}`).then(r => r.data), api.get<RegionResponse>(`/regions/${id}`).then(r => r.data),
+1
View File
@@ -71,6 +71,7 @@ export interface PermissionResponse {
export interface RegionResponse { export interface RegionResponse {
id: string id: string
code: string
name: string name: string
isActive: boolean isActive: boolean
createdAt: string createdAt: string