diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx new file mode 100644 index 0000000..71217d6 --- /dev/null +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx @@ -0,0 +1,134 @@ +'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, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { claims as claimsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { ClaimResponse, UserResponse } from '@/types/api' + +const schema = z.object({ + settlementAmount: z.number().min(0, 'Must be 0 or more'), + settledByUserId: z.string().min(1, 'Settled by is required'), +}) + +type FormValues = z.infer + +interface ClaimSettleDialogProps { + open: boolean + onClose: () => void + onSuccess: () => void + claim: ClaimResponse | null +} + +export function ClaimSettleDialog({ open, onClose, onSuccess, claim }: ClaimSettleDialogProps) { + const [usersList, setUsersList] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { settlementAmount: 0, settledByUserId: '' }, + }) + + useEffect(() => { + if (!open) return + form.reset({ settlementAmount: 0, settledByUserId: '' }) + usersApi.list({ pageSize: 200 }) + .then((res) => setUsersList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) + }, [open]) + + if (!claim) return null + + async function onSubmit(values: FormValues) { + try { + await claimsApi.settle(claim!.id, values) + toast.add({ title: 'Claim settled', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to settle claim', description: getApiError(err), type: 'error' }) + } + } + + return ( + { if (!v) onClose() }}> + + + Settle Claim + {claim.details} + + +
+ + ( + field.onChange(Number(e.target.value))} + onBlur={field.onBlur} + aria-invalid={invalid} + /> + )} + /> + + ( + + )} + /> + + + + + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx new file mode 100644 index 0000000..e648b38 --- /dev/null +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx @@ -0,0 +1,137 @@ +'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, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { claims as claimsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { ClaimResponse, UserResponse } from '@/types/api' + +const schema = z.object({ + agencyUserId: z.string().min(1, 'Agency user is required'), + details: z.string().min(1, 'Details are required'), +}) + +type FormValues = z.infer + +interface ClaimSheetProps { + open: boolean + onClose: () => void + onSuccess: () => void + claim?: ClaimResponse | null +} + +export function ClaimSheet({ open, onClose, onSuccess, claim }: ClaimSheetProps) { + const [usersList, setUsersList] = useState([]) + const isEdit = Boolean(claim) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { agencyUserId: '', details: '' }, + }) + + useEffect(() => { + if (!open) return + form.reset({ agencyUserId: claim?.agencyUserId ?? '', details: claim?.details ?? '' }) + usersApi.list({ pageSize: 200 }) + .then((res) => setUsersList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) + }, [open, claim]) + + async function onSubmit(values: FormValues) { + try { + if (isEdit) { + await claimsApi.update(claim!.id, values) + toast.add({ title: 'Claim updated', type: 'success' }) + } else { + await claimsApi.create(values) + toast.add({ title: 'Claim raised', type: 'success' }) + } + onSuccess() + onClose() + } catch (err) { + toast.add({ title: `Failed to ${isEdit ? 'update' : 'raise'} claim`, description: getApiError(err), type: 'error' }) + } + } + + return ( + { if (!v) onClose() }}> + + + {isEdit ? 'Edit' : 'Raise'} Agency Claim + + {isEdit ? 'Only claims that are still Open can be edited.' : 'Raise a claim against a regional agency user.'} + + + +
+ + ( + + )} + /> + + ( + + )} + /> + + + + + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-view-sheet.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-view-sheet.tsx new file mode 100644 index 0000000..c3c8e06 --- /dev/null +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-view-sheet.tsx @@ -0,0 +1,56 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import type { ClaimResponse } from '@/types/api' + +const fmtDateTime = (v: string) => + new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` + +interface ClaimViewSheetProps { + open: boolean + onClose: () => void + claim: ClaimResponse | null + agencyUserName?: string +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function ClaimViewSheet({ open, onClose, claim, agencyUserName }: ClaimViewSheetProps) { + if (!claim) return null + + return ( + !v && onClose()}> + + + Claim Details + + +
+ + + {claim.status}} /> + {claim.settlementAmount != null && } + {claim.settledAt && } + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx new file mode 100644 index 0000000..5ef80c8 --- /dev/null +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx @@ -0,0 +1,196 @@ +'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 { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { toast } from '@/components/ui/toast' +import { claims as claimsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { ClaimResponse } from '@/types/api' +import { ClaimSheet } from './claim-sheet' +import { ClaimSettleDialog } from './claim-settle-dialog' +import { ClaimViewSheet } from './claim-view-sheet' + +const fmtDate = (v: string) => + new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` + +export function ClaimsClient() { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + const [pageSize, setPageSize] = useState(20) + const [userNameById, setUserNameById] = useState>(new Map()) + const [createOpen, setCreateOpen] = useState(false) + const [settling, setSettling] = useState(null) + const [settleOpen, setSettleOpen] = useState(false) + const [viewing, setViewing] = useState(null) + const [viewOpen, setViewOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [editOpen, setEditOpen] = useState(false) + + function openSettle(row: ClaimResponse) { + setSettling(row) + setSettleOpen(true) + } + + function openView(row: ClaimResponse) { + setViewing(row) + setViewOpen(true) + } + + function openEdit(row: ClaimResponse) { + setEditing(row) + setEditOpen(true) + } + + const fetchData = useCallback(async () => { + setLoading(true) + try { + const [claimsList, usersRes] = await Promise.all([ + claimsApi.list(), + usersApi.list({ pageSize: 200 }), + ]) + setUserNameById(new Map(getPaginatedItems(usersRes).map((u) => [u.id, u.fullName]))) + setData(claimsList) + } catch (err) { + toast.add({ title: 'Failed to load claims', description: getApiError(err), type: 'error' }) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchData() + }, [fetchData]) + + const tableConfig = useMemo>(() => ({ + rowKey: 'id', + stickyHeader: true, + emptyMessage: 'No claims raised yet.', + pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, + columns: [ + { + key: 'agencyUserId', + title: 'Agency User', + fields: ['agencyUserId'], + minWidth: 180, + cellRender: (row) => ( + {userNameById.get(row.agencyUserId) ?? '—'} + ), + }, + { + key: 'details', + title: 'Details', + fields: ['details'], + minWidth: 240, + cellRender: (row) => ( + {row.details} + ), + }, + { + key: 'status', + title: 'Status', + fields: ['status'], + width: 120, + cellRender: (row) => ( + {row.status} + ), + }, + { + key: 'settlementAmount', + title: 'Settlement', + fields: ['settlementAmount'], + width: 140, + cellRender: (row) => ( + {row.settlementAmount != null ? fmtAmount(row.settlementAmount) : '—'} + ), + }, + { + key: 'createdAt', + title: 'Created', + fields: [{ key: 'createdAt', transform: (v) => fmtDate(v as string) }], + width: 140, + }, + ], + rowActions: { + actions: () => [ + { + action: 'view', + label: 'View claim', + onClick: openView, + }, + { + action: 'edit', + label: 'Edit claim', + onClick: openEdit, + visible: (target) => target.status === 'Open', + }, + { + action: 'confirm', + label: 'Settle claim', + onClick: openSettle, + visible: (target) => target.status === 'Open', + }, + ], + }, + }), [pageSize, userNameById]) + + return ( + <> +
+
+

Agency Claims

+

+ Claims raised against a regional agency and their settlement +

+
+ +
+ + { + setPageSize(p.pageSize) + }} + /> + + setCreateOpen(false)} + onSuccess={fetchData} + /> + + setEditOpen(false)} + onSuccess={fetchData} + claim={editing} + /> + + setSettleOpen(false)} + onSuccess={fetchData} + claim={settling} + /> + + setViewOpen(false)} + claim={viewing} + agencyUserName={viewing ? userNameById.get(viewing.agencyUserId) : undefined} + /> + + ) +} diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/page.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/page.tsx new file mode 100644 index 0000000..543f908 --- /dev/null +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/page.tsx @@ -0,0 +1,5 @@ +import { ClaimsClient } from './_components/claims-client' + +export default function ClaimsPage() { + return +}