import { useState } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; 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 { toast } from 'react-toastify'; import { customersApi, type Customer, type PagedCustomerResponse, type CreateCustomerRequest, type UpdateCustomerRequest, } from '../../../lib/api/customers'; import { queryClient } from '../../../lib/queryClient'; import { QUERY_KEYS } from '../../../constants/queryKeys'; import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEBOUNCE_SEARCH } from '../../../constants'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; const COL_COUNT = 4; // Name, Address, Contact Email, Actions /** Simple email format check (client-side only). */ function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } interface CustomerFormState { name: string; address: string; contactEmail: string; } const EMPTY_FORM: CustomerFormState = { name: '', address: '', contactEmail: '' }; export default function CustomerManagementPage() { useDocumentTitle('Customer Management'); // --- List state --- const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [page, setPage] = useState(DEFAULT_PAGE); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); // Debounce search input const [debounceTimer, setDebounceTimer] = useState | null>(null); const handleSearchChange = (value: string) => { setSearch(value); if (debounceTimer) clearTimeout(debounceTimer); const timer = setTimeout(() => { setDebouncedSearch(value); setPage(1); }, DEBOUNCE_SEARCH); setDebounceTimer(timer); }; const { data, isLoading, isError, refetch, } = useQuery({ queryKey: [QUERY_KEYS.customerList, debouncedSearch, page, pageSize], queryFn: () => customersApi.list({ 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 [form, setForm] = useState(EMPTY_FORM); const [formErrors, setFormErrors] = useState>>({}); const isEditing = editingCustomer !== null; const openCreateDialog = () => { setEditingCustomer(null); setForm(EMPTY_FORM); setFormErrors({}); setDialogOpen(true); }; const openEditDialog = (customer: Customer) => { setEditingCustomer(customer); setForm({ name: customer.name, address: customer.addresses?.[0] ?? '', contactEmail: customer.contactEmail ?? '', }); setFormErrors({}); setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingCustomer(null); setForm(EMPTY_FORM); setFormErrors({}); }; const validateForm = (): boolean => { const errors: Partial> = {}; if (!form.name.trim()) errors.name = 'Name is required'; if (!form.address.trim()) errors.address = 'Address is required'; if (form.contactEmail.trim() && !isValidEmail(form.contactEmail.trim())) { errors.contactEmail = 'Enter a valid email address'; } setFormErrors(errors); return Object.keys(errors).length === 0; }; const handleFieldChange = (field: keyof CustomerFormState, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); // Clear field error on change if (formErrors[field]) { setFormErrors((prev) => { const next = { ...prev }; delete next[field]; return next; }); } }; // --- Mutations --- const createMutation = useMutation({ mutationFn: (data: CreateCustomerRequest) => customersApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.customerList] }); closeDialog(); toast.success('Customer created'); }, onError: (error: Error) => { toast.error(`Failed to create customer: ${error.message}`); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateCustomerRequest }) => customersApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.customerList] }); closeDialog(); toast.success('Customer updated'); }, onError: (error: Error) => { toast.error(`Failed to update customer: ${error.message}`); }, }); const handleSubmit = () => { if (!validateForm()) return; const payload = { name: form.name.trim(), address: form.address.trim(), ...(form.contactEmail.trim() ? { contactEmail: form.contactEmail.trim() } : {}), }; if (isEditing) { updateMutation.mutate({ id: editingCustomer.id, data: payload }); } else { createMutation.mutate(payload); } }; 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'} handleFieldChange('name', e.target.value)} error={!!formErrors.name} helperText={formErrors.name} size="small" required fullWidth autoFocus /> handleFieldChange('address', e.target.value)} error={!!formErrors.address} helperText={formErrors.address} size="small" required fullWidth /> handleFieldChange('contactEmail', e.target.value)} error={!!formErrors.contactEmail} helperText={formErrors.contactEmail || 'Optional. Used for proposal PDF delivery.'} size="small" fullWidth />
); }