/** * @ai-summary Admin Vehicle Catalog page with search-first UI * @ai-context Uses server-side search and pagination instead of loading all data */ import React, { useState, useCallback, useRef } from 'react'; import { Navigate } from 'react-router-dom'; import { Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, InputAdornment, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Tooltip, Typography, Alert, Collapse, } from '@mui/material'; import { Search, Delete, FileDownload, FileUpload, Clear, ExpandMore, ExpandLess, } from '@mui/icons-material'; import toast from 'react-hot-toast'; import { useAdminAccess } from '../../core/auth/useAdminAccess'; import { useCatalogSearch, useExportCatalog, useImportPreview, useImportApply, } from '../../features/admin/hooks/useCatalog'; import { adminApi } from '../../features/admin/api/admin.api'; import { AdminSectionHeader, } from '../../features/admin/components'; import { CatalogSearchResult, ImportPreviewResult, ImportApplyResult, } from '../../features/admin/types/admin.types'; const PAGE_SIZE_OPTIONS = [25, 50, 100]; export const AdminCatalogPage: React.FC = () => { const { loading: authLoading, isAdmin } = useAdminAccess(); // Search state const [searchInput, setSearchInput] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50); // Selection state const [selectedIds, setSelectedIds] = useState>(new Set()); // Dialog state const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); // Import state const [importDialogOpen, setImportDialogOpen] = useState(false); const [importPreview, setImportPreview] = useState(null); const [importResult, setImportResult] = useState(null); const [errorsExpanded, setErrorsExpanded] = useState(false); const fileInputRef = useRef(null); // Hooks const { data: searchData, isLoading: searchLoading, refetch } = useCatalogSearch( searchQuery, page + 1, // API uses 1-based pages pageSize ); const exportMutation = useExportCatalog(); const importPreviewMutation = useImportPreview(); const importApplyMutation = useImportApply(); // Search handlers const handleSearch = useCallback(() => { setPage(0); setSelectedIds(new Set()); setSearchQuery(searchInput); }, [searchInput]); const handleSearchKeyPress = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter') { handleSearch(); } }, [handleSearch] ); const handleClearSearch = useCallback(() => { setSearchInput(''); setSearchQuery(''); setPage(0); setSelectedIds(new Set()); }, []); // Pagination handlers const handlePageChange = useCallback((_: unknown, newPage: number) => { setPage(newPage); setSelectedIds(new Set()); }, []); const handlePageSizeChange = useCallback( (event: React.ChangeEvent) => { setPageSize(parseInt(event.target.value, 10)); setPage(0); setSelectedIds(new Set()); }, [] ); // Selection handlers const handleSelectAll = useCallback( (event: React.ChangeEvent) => { if (event.target.checked && searchData?.items) { setSelectedIds(new Set(searchData.items.map((item) => item.id))); } else { setSelectedIds(new Set()); } }, [searchData] ); const handleSelectRow = useCallback((id: number) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); // Delete handlers const handleDeleteClick = useCallback((row: CatalogSearchResult) => { setDeleteTarget(row); setDeleteDialogOpen(true); }, []); const handleBulkDeleteClick = useCallback(() => { setDeleteTarget(null); setDeleteDialogOpen(true); }, []); const handleDeleteConfirm = useCallback(async () => { setDeleting(true); try { if (deleteTarget) { // Single delete - delete from vehicle_options await adminApi.deleteEngine(deleteTarget.id.toString()); toast.success('Configuration deleted'); } else { // Bulk delete const idsToDelete = Array.from(selectedIds); await Promise.all( idsToDelete.map((id) => adminApi.deleteEngine(id.toString())) ); toast.success(`${idsToDelete.length} configurations deleted`); setSelectedIds(new Set()); } setDeleteDialogOpen(false); setDeleteTarget(null); refetch(); } catch (error) { toast.error('Failed to delete configuration(s)'); } finally { setDeleting(false); } }, [deleteTarget, selectedIds, refetch]); // Import handlers const handleImportClick = useCallback(() => { fileInputRef.current?.click(); }, []); const handleFileSelect = useCallback( async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { const result = await importPreviewMutation.mutateAsync(file); setImportPreview(result); setImportDialogOpen(true); } catch (error) { // Error is handled by mutation } // Reset file input if (fileInputRef.current) { fileInputRef.current.value = ''; } }, [importPreviewMutation] ); const handleImportConfirm = useCallback(async () => { if (!importPreview?.previewId) return; try { const result = await importApplyMutation.mutateAsync(importPreview.previewId); setImportResult(result); if (result.errors.length > 0) { toast.error( `Import completed with ${result.errors.length} error(s): ${result.created} created, ${result.updated} updated` ); // Keep dialog open for error review } else { toast.success( `Import completed successfully: ${result.created} created, ${result.updated} updated` ); // Auto-close on complete success setImportDialogOpen(false); setImportPreview(null); setImportResult(null); } refetch(); } catch (error) { // Error is handled by mutation's onError } }, [importPreview, importApplyMutation, refetch]); const handleImportDialogClose = useCallback(() => { if (importApplyMutation.isPending) return; setImportDialogOpen(false); setImportPreview(null); setImportResult(null); setErrorsExpanded(false); }, [importApplyMutation.isPending]); // Export handler const handleExport = useCallback(() => { exportMutation.mutate(); }, [exportMutation]); // Auth loading if (authLoading) { return ( ); } // Not admin if (!isAdmin) { return ; } const items = searchData?.items || []; const total = searchData?.total || 0; const hasResults = searchQuery.length > 0 && items.length > 0; const noResults = searchQuery.length > 0 && items.length === 0 && !searchLoading; return ( {/* Search and Actions Bar */} {/* Search Input */} setSearchInput(e.target.value)} onKeyPress={handleSearchKeyPress} fullWidth sx={{ flex: 1 }} InputProps={{ startAdornment: ( ), endAdornment: searchInput && ( ), }} /> {/* Search Button */} {/* Action Buttons */} {/* Hidden file input for import */} {/* Instructions when no search */} {!searchQuery && ( Search for Vehicles Enter a search term like "2024 Toyota Camry" or "Honda Civic" to find vehicles in the catalog. Use Import/Export buttons to manage catalog data in bulk via CSV files. )} {/* No Results */} {noResults && ( No Results Found No vehicles match "{searchQuery}". Try a different search term. )} {/* Results Table */} {hasResults && ( {/* Bulk Actions */} {selectedIds.size > 0 && ( )} 0 && selectedIds.size < items.length} checked={selectedIds.size === items.length && items.length > 0} onChange={handleSelectAll} size="small" /> Year Make Model Trim Engine Transmission Actions {items.map((row) => ( handleSelectRow(row.id)} size="small" /> {row.year} {row.make} {row.model} {row.trim} {row.engineName || '-'} {row.transmissionType || '-'} handleDeleteClick(row)} > ))}
)} {/* Delete Confirmation Dialog */} !deleting && setDeleteDialogOpen(false)}> {deleteTarget ? 'Delete Configuration' : `Delete ${selectedIds.size} Configuration${selectedIds.size > 1 ? 's' : ''}`} {deleteTarget ? ( Are you sure you want to delete the configuration for{' '} {deleteTarget.year} {deleteTarget.make} {deleteTarget.model} {deleteTarget.trim} ? ) : ( Are you sure you want to delete {selectedIds.size} selected configuration {selectedIds.size > 1 ? 's' : ''}? )} {/* Import Preview/Results Dialog */} {importResult ? 'Import Results' : 'Import Preview'} {/* Preview Mode */} {importPreview && !importResult && ( To Create: {importPreview.toCreate.length} To Update: {importPreview.toUpdate.length} {importPreview.errors.length > 0 && ( {importPreview.errors.length} Error(s) Found: {importPreview.errors.slice(0, 10).map((err, idx) => (
  • Row {err.row}: {err.error}
  • ))} {importPreview.errors.length > 10 && (
  • ...and {importPreview.errors.length - 10} more errors
  • )}
    )} {importPreview.valid ? ( The import file is valid and ready to be applied. ) : ( Please fix the errors above before importing. )}
    )} {/* Results Mode */} {importResult && ( Created: {importResult.created} Updated: {importResult.updated} {importResult.errors.length > 0 && ( setErrorsExpanded(!errorsExpanded)} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 2, bgcolor: 'error.light', cursor: 'pointer', '&:hover': { bgcolor: 'error.main', color: 'white' }, }} > {importResult.errors.length} Error(s) Occurred {errorsExpanded ? : } {importResult.errors.map((err, idx) => ( Row {err.row}: {err.error} ))} )} {importResult.errors.length === 0 && ( Import completed successfully with no errors. )} )}
    {!importResult && ( )}
    ); };