import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Box, Typography, Card, CardContent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, TextField, InputAdornment, Button, Skeleton, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Alert, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined'; import { useCustomerList, useCreateCustomer, useUpdateCustomer, } from '../../../domain/customers/use-cases'; import type { Customer } from '../../../domain/customers/types'; import { customerFormSchema, toCustomerRequest, type CustomerFormValues, } from '../../../domain/customers/schemas'; import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEBOUNCE_SEARCH } from '../../../constants'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; import { useDebouncedValue } from '../../../hooks/useDebouncedValue'; const COL_COUNT = 4; // Name, Address, Contact Email, Actions const EMPTY_FORM: CustomerFormValues = { name: '', address: '', contactEmail: '' }; export default function CustomerManagementPage() { useDocumentTitle('Customer Management'); // --- List state --- const [search, setSearch] = useState(''); const [page, setPage] = useState(DEFAULT_PAGE); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); // Debounced search resets to page 1 (shared hook — the previous inline // timer leaked on unmount). const debouncedSearch = useDebouncedValue(search, DEBOUNCE_SEARCH, () => setPage(1)); const handleSearchChange = (value: string) => setSearch(value); const { data, isLoading, isError, refetch } = useCustomerList({ search: debouncedSearch || undefined, page, pageSize, }); const rows = data?.items ?? []; const totalCount = data?.totalCount ?? 0; // --- Dialog state --- const [dialogOpen, setDialogOpen] = useState(false); const [editingCustomer, setEditingCustomer] = useState(null); const isEditing = editingCustomer !== null; const { register, handleSubmit, reset, formState: { errors }, } = useForm({ resolver: zodResolver(customerFormSchema), defaultValues: EMPTY_FORM, }); const openCreateDialog = () => { setEditingCustomer(null); reset(EMPTY_FORM); setDialogOpen(true); }; const openEditDialog = (customer: Customer) => { setEditingCustomer(customer); reset({ name: customer.name, address: customer.addresses?.[0] ?? '', contactEmail: customer.contactEmail ?? '', }); setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingCustomer(null); reset(EMPTY_FORM); }; // --- Mutations (domain hooks: invalidation + toasts live in the hooks) --- const createMutation = useCreateCustomer(); const updateMutation = useUpdateCustomer(); const onSubmit = (values: CustomerFormValues) => { // Fix: WEB-M5 — the API contract is addresses: string[] (CustomerDtos.cs); // the old singular `address` field was silently dropped by model binding. // The form edits the primary address; any additional addresses are preserved // (toCustomerRequest maps to addresses: [primary, ...rest]). const payload = toCustomerRequest(values, editingCustomer?.addresses ?? []); if (isEditing) { updateMutation.mutate( { id: editingCustomer.id, data: payload }, { onSuccess: closeDialog } ); } else { createMutation.mutate(payload, { onSuccess: closeDialog }); } }; const isSaving = createMutation.isPending || updateMutation.isPending; return ( Customer Management {/* Search bar */} handleSearchChange(e.target.value)} sx={{ width: 300 }} slotProps={{ input: { startAdornment: ( ), }, }} /> {/* Error state */} {isError && ( } action={ } sx={{ mb: 2 }} > Failed to load customers. Please try again. )} {/* Table */} Name Address Contact Email {isLoading ? Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: COL_COUNT }).map((_, j) => ( ))} )) : rows.map((customer) => ( {customer.name} {customer.addresses?.[0] ?? '-'} {customer.contactEmail ? ( customer.contactEmail ) : ( No email )} openEditDialog(customer)} > ))} {!isLoading && rows.length === 0 && !isError && ( {debouncedSearch ? 'No customers match your search.' : 'No customers yet. Click "New Customer" to add one.'} )}
setPage(newPage + 1)} rowsPerPage={pageSize} onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))} rowsPerPageOptions={[12, 24, 48]} />
{/* Create / Edit Dialog */}
{isEditing ? 'Edit Customer' : 'New Customer'}
); }