Refactor/main flow #54
@@ -4,7 +4,6 @@ import { useEffect } from 'react'
|
||||
import { useForm, zodResolver, z } from '@/lib/form'
|
||||
import { FormField } from '@/components/shared/form-field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,8 +19,8 @@ import { regions as regionsApi, getApiError } from '@/lib/api/index'
|
||||
import type { RegionResponse } from '@/types/api'
|
||||
|
||||
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'),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
@@ -30,30 +29,39 @@ interface RegionSheetProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
region: RegionResponse | null
|
||||
readOnly?: boolean
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetProps) {
|
||||
export function RegionSheet({ open, onClose, region, readOnly = false, onSuccess }: RegionSheetProps) {
|
||||
const isEdit = !!region
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { name: '', isActive: true },
|
||||
defaultValues: { code: '', name: '' },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
try {
|
||||
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' })
|
||||
} else {
|
||||
await regionsApi.create(values)
|
||||
await regionsApi.create({ name: values.name, code: values.code?.trim() || undefined })
|
||||
toast.add({ title: 'Region created', type: 'success' })
|
||||
}
|
||||
onSuccess()
|
||||
@@ -67,14 +75,37 @@ export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetPro
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? 'Edit Region' : 'Add Region'}</DialogTitle>
|
||||
<DialogTitle>{readOnly ? 'View Region' : isEdit ? 'Edit Region' : 'Add Region'}</DialogTitle>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
<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
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -89,36 +120,20 @@ export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetPro
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
placeholder="e.g. Western Province"
|
||||
disabled={readOnly}
|
||||
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>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
{readOnly ? 'Close' : 'Cancel'}
|
||||
</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'}
|
||||
</Button>
|
||||
</Button>)}
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DataTable } from '@/components/shared/data-table'
|
||||
import type { TableConfig } from '@/components/shared/data-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import type { RegionResponse } from '@/types/api'
|
||||
@@ -23,6 +24,9 @@ export function RegionsClient() {
|
||||
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
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) => {
|
||||
setLoading(true)
|
||||
@@ -43,24 +47,34 @@ export function RegionsClient() {
|
||||
|
||||
function openCreate() {
|
||||
setEditing(null)
|
||||
setViewOnly(false)
|
||||
setSheetOpen(true)
|
||||
}
|
||||
|
||||
function openEdit(region: RegionResponse) {
|
||||
setEditing(region)
|
||||
setViewOnly(false)
|
||||
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 {
|
||||
await regionsApi.update(region.id, { isActive: !region.isActive })
|
||||
toast.add({
|
||||
title: region.isActive ? 'Region deactivated' : 'Region activated',
|
||||
type: 'success',
|
||||
})
|
||||
await regionsApi.remove(deleting.id)
|
||||
toast.add({ title: 'Region deleted', type: 'success' })
|
||||
setDeleting(null)
|
||||
fetchData(page, pageSize)
|
||||
} 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.',
|
||||
pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] },
|
||||
columns: [
|
||||
{
|
||||
key: 'code',
|
||||
title: 'Code',
|
||||
fields: [{ key: 'code', primary: true }],
|
||||
sortable: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Region',
|
||||
@@ -103,6 +124,11 @@ export function RegionsClient() {
|
||||
],
|
||||
rowActions: {
|
||||
actions: () => [
|
||||
{
|
||||
action: 'view',
|
||||
label: 'View region',
|
||||
onClick: openView,
|
||||
},
|
||||
{
|
||||
action: 'edit',
|
||||
label: 'Edit region',
|
||||
@@ -110,19 +136,12 @@ export function RegionsClient() {
|
||||
},
|
||||
{
|
||||
action: 'deactivate',
|
||||
label: 'Deactivate region',
|
||||
onClick: toggleActive,
|
||||
visible: (target) => target.isActive,
|
||||
},
|
||||
{
|
||||
action: 'activate',
|
||||
label: 'Activate region',
|
||||
onClick: toggleActive,
|
||||
visible: (target) => !target.isActive,
|
||||
label: 'Delete region',
|
||||
onClick: (region: RegionResponse) => setDeleting(region),
|
||||
},
|
||||
],
|
||||
},
|
||||
}), [pageSize, openEdit, toggleActive])
|
||||
}), [pageSize, openEdit, openView])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -154,8 +173,28 @@ export function RegionsClient() {
|
||||
open={sheetOpen}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
region={editing}
|
||||
readOnly={viewOnly}
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface PageParams {
|
||||
|
||||
export interface RegionBody {
|
||||
name: string
|
||||
code?: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
@@ -91,6 +92,9 @@ export const regions = {
|
||||
list: (params?: PageParams) =>
|
||||
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) =>
|
||||
api.get<RegionResponse>(`/regions/${id}`).then(r => r.data),
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface PermissionResponse {
|
||||
|
||||
export interface RegionResponse {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
|
||||
Reference in New Issue
Block a user