import { useState } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; 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 { toast } from 'react-toastify'; import { pricingLibraryApi, type PricingLibraryItem, type PagedPricingLibraryResponse, type CreatePricingLibraryItemRequest, type UpdatePricingLibraryItemRequest, } from '../../../lib/api/pricingLibrary'; import { queryClient } from '../../../lib/queryClient'; import { QUERY_KEYS } from '../../../constants/queryKeys'; 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' }, }; interface ItemFormState { title: string; serviceCategory: string; unit: string; unitPrice: string; keywords: string; description: string; } const EMPTY_FORM: ItemFormState = { title: '', serviceCategory: '', 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, } = useQuery({ queryKey: [QUERY_KEYS.pricingLibraryList, page, pageSize], queryFn: () => pricingLibraryApi.list({ 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 [form, setForm] = useState(EMPTY_FORM); const [formErrors, setFormErrors] = useState>>({}); const isEditing = editingItem !== null; const openCreateDialog = () => { setEditingItem(null); setForm(EMPTY_FORM); setFormErrors({}); setDialogOpen(true); }; const openEditDialog = (item: PricingLibraryItem) => { setEditingItem(item); setForm({ title: item.title, serviceCategory: item.serviceCategory, unit: item.unit ?? '', unitPrice: item.unitPrice != null ? String(item.unitPrice) : '', keywords: item.keywords ?? '', description: item.description ?? '', }); setFormErrors({}); setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingItem(null); setForm(EMPTY_FORM); setFormErrors({}); }; const validateForm = (): boolean => { const errors: Partial> = {}; if (!form.title.trim()) errors.title = 'Title is required'; if (!form.serviceCategory) errors.serviceCategory = 'Service category is required'; if (form.unitPrice.trim() !== '') { const parsed = parseFloat(form.unitPrice); if (isNaN(parsed) || parsed < 0) { errors.unitPrice = 'Must be a number >= 0'; } } setFormErrors(errors); return Object.keys(errors).length === 0; }; const handleFieldChange = (field: keyof ItemFormState, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); if (formErrors[field]) { setFormErrors((prev) => { const next = { ...prev }; delete next[field]; return next; }); } }; // --- Delete confirm dialog state --- const [deleteTarget, setDeleteTarget] = useState(null); const openDeleteDialog = (item: PricingLibraryItem) => { setDeleteTarget(item); }; const closeDeleteDialog = () => { setDeleteTarget(null); }; // --- Mutations --- const createMutation = useMutation({ mutationFn: (data: CreatePricingLibraryItemRequest) => pricingLibraryApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.pricingLibraryList] }); closeDialog(); toast.success('Pricing item created'); }, onError: (error: Error) => { toast.error(`Failed to create item: ${error.message}`); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdatePricingLibraryItemRequest }) => pricingLibraryApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.pricingLibraryList] }); closeDialog(); toast.success('Pricing item updated'); }, onError: (error: Error) => { toast.error(`Failed to update item: ${error.message}`); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => pricingLibraryApi.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.pricingLibraryList] }); closeDeleteDialog(); toast.success('Pricing item deleted'); }, onError: (error: Error) => { toast.error(`Failed to delete item: ${error.message}`); }, }); const handleSubmit = () => { if (!validateForm()) return; const payload = { title: form.title.trim(), serviceCategory: form.serviceCategory, ...(form.unit.trim() ? { unit: form.unit.trim() } : {}), unitPrice: form.unitPrice.trim() !== '' ? parseFloat(form.unitPrice) : null, ...(form.keywords.trim() ? { keywords: form.keywords.trim() } : {}), ...(form.description.trim() ? { description: form.description.trim() } : {}), }; if (isEditing) { updateMutation.mutate({ id: editingItem.id, data: payload }); } else { createMutation.mutate(payload); } }; const handleDeleteConfirm = () => { if (deleteTarget) { deleteMutation.mutate(deleteTarget.id); } }; 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'} handleFieldChange('title', e.target.value)} error={!!formErrors.title} helperText={formErrors.title} size="small" required fullWidth autoFocus /> handleFieldChange('serviceCategory', e.target.value)} error={!!formErrors.serviceCategory} helperText={formErrors.serviceCategory} size="small" required fullWidth select > {SERVICE_CATEGORIES.map((cat) => ( {cat} ))} handleFieldChange('unit', e.target.value)} helperText="e.g., each, hour, sq ft" size="small" fullWidth /> handleFieldChange('unitPrice', e.target.value)} error={!!formErrors.unitPrice} helperText={formErrors.unitPrice || 'Optional. Leave blank if price varies.'} size="small" fullWidth slotProps={{ htmlInput: { min: 0, step: '0.01' }, }} /> handleFieldChange('keywords', e.target.value)} helperText="Comma-separated keywords for AI matching." size="small" fullWidth multiline minRows={2} /> handleFieldChange('description', e.target.value)} size="small" fullWidth multiline minRows={2} /> {/* Delete Confirm Dialog */} Delete Pricing Item Are you sure you want to delete {deleteTarget?.title}? This action cannot be undone.
); }