import { useState } from 'react'; import { Controller, useForm, type DefaultValues } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Box, Typography, Card, CardContent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, TextField, Button, Skeleton, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Alert, Chip, MenuItem, } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined'; import { usePricingLibraryList, useCreatePricingLibraryItem, useUpdatePricingLibraryItem, useDeletePricingLibraryItem, } from '../../../domain/pricingLibrary/use-cases'; import type { PricingLibraryItem } from '../../../domain/pricingLibrary/types'; import { pricingLibraryItemFormSchema, toPricingLibraryItemRequest, type PricingLibraryItemFormValues, } from '../../../domain/pricingLibrary/schemas'; import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SERVICE_CATEGORIES } from '../../../constants'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; const COL_COUNT = 6; // Title, Category, Unit, Unit Price, Source, Actions /** Chip color mapping for service categories. */ const CATEGORY_CHIP_STYLES: Record = { HVAC: { bgcolor: '#E0F2FE', color: '#075985', border: '1px solid #7DD3FC' }, Plumbing: { bgcolor: '#DBEAFE', color: '#1E40AF', border: '1px solid #BFDBFE' }, Electrical: { bgcolor: '#FEF9C3', color: '#854D0E', border: '1px solid #FDE68A' }, General: { bgcolor: '#F3F4F6', color: '#374151', border: '1px solid #D1D5DB' }, Renovation: { bgcolor: '#F3E8FF', color: '#6B21A8', border: '1px solid #C084FC' }, Other: { bgcolor: '#F1F5F9', color: '#475569', border: '1px solid #CBD5E1' }, }; /** * serviceCategory stays union-typed (ServiceCategory) in the form schema, so the * create defaults leave it unset rather than using an out-of-union '' sentinel. */ const EMPTY_FORM_VALUES: DefaultValues = { title: '', unit: '', unitPrice: '', keywords: '', description: '', }; /** Format a number as $X.XX or return "—" for null/undefined. */ function formatPrice(value: number | null | undefined): string { if (value == null) return '—'; return `$${value.toFixed(2)}`; } export default function PricingLibraryPage() { useDocumentTitle('Pricing Library'); // --- List state --- const [page, setPage] = useState(DEFAULT_PAGE); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const { data, isLoading, isError, refetch } = usePricingLibraryList({ page, pageSize }); const rows = data?.items ?? []; const totalCount = data?.totalCount ?? 0; // --- Create/Edit dialog state --- const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const isEditing = editingItem !== null; const { control, handleSubmit, reset, formState: { errors }, } = useForm({ resolver: zodResolver(pricingLibraryItemFormSchema), defaultValues: EMPTY_FORM_VALUES, }); const openCreateDialog = () => { setEditingItem(null); reset(EMPTY_FORM_VALUES); setDialogOpen(true); }; const openEditDialog = (item: PricingLibraryItem) => { setEditingItem(item); reset({ title: item.title, serviceCategory: item.serviceCategory, unit: item.unit ?? '', unitPrice: item.unitPrice != null ? String(item.unitPrice) : '', keywords: item.keywords ?? '', description: item.description ?? '', }); setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingItem(null); reset(EMPTY_FORM_VALUES); }; // --- Delete confirm dialog state --- const [deleteTarget, setDeleteTarget] = useState(null); const openDeleteDialog = (item: PricingLibraryItem) => { setDeleteTarget(item); }; const closeDeleteDialog = () => { setDeleteTarget(null); }; // --- Mutations (domain hooks own invalidation + toasts; dialog close stays here) --- const createMutation = useCreatePricingLibraryItem(); const updateMutation = useUpdatePricingLibraryItem(); const deleteMutation = useDeletePricingLibraryItem(); const onSubmit = handleSubmit((values) => { const payload = toPricingLibraryItemRequest(values); if (editingItem) { updateMutation.mutate( { id: editingItem.id, data: payload }, { onSuccess: () => closeDialog() } ); } else { createMutation.mutate(payload, { onSuccess: () => closeDialog() }); } }); const handleDeleteConfirm = () => { if (deleteTarget) { deleteMutation.mutate(deleteTarget.id, { onSuccess: () => closeDeleteDialog() }); } }; const isSaving = createMutation.isPending || updateMutation.isPending; return ( Pricing Library {/* Error state */} {isError && ( } action={ } sx={{ mb: 2 }} > Failed to load pricing library. Please try again. )} {/* Table */} Title Category Unit Unit Price Source {isLoading ? Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: COL_COUNT }).map((_, j) => ( ))} )) : rows.map((item) => { const defaultChip = { bgcolor: '#F1F5F9', color: '#475569', border: '1px solid #CBD5E1' }; const chipStyle = CATEGORY_CHIP_STYLES[item.serviceCategory] ?? defaultChip; return ( {item.title} {item.unit ?? '—'} {formatPrice(item.unitPrice)} {item.source} openEditDialog(item)} > openDeleteDialog(item)} color="error" > ); })} {!isLoading && rows.length === 0 && !isError && ( No pricing items yet. Click "New Item" 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 Pricing Item' : 'New Pricing Item'}
( )} /> ( {SERVICE_CATEGORIES.map((cat) => ( {cat} ))} )} /> ( )} /> ( )} /> ( )} /> ( )} />
{/* Delete Confirm Dialog */} Delete Pricing Item Are you sure you want to delete {deleteTarget?.title}? This action cannot be undone.
); }