Dev #55

Merged
DeepnaPooja merged 17 commits from dev into production 2026-10-07 10:47:55 +05:30
4 changed files with 105 additions and 46 deletions
Showing only changes of commit b75cd01e90 - Show all commits
@@ -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>
</>
)
}
+4
View File
@@ -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),
+1
View File
@@ -71,6 +71,7 @@ export interface PermissionResponse {
export interface RegionResponse {
id: string
code: string
name: string
isActive: boolean
createdAt: string