'use client'; import { useEffect, useMemo, useState } from 'react'; import { Plus, RefreshCw, Search, Trash2, Wrench, Tag, Smartphone } from 'lucide-react'; import { toast } from 'sonner'; import { adminService } from '@/services/api/adminService'; import { formatCurrency } from '@/lib/utils'; import { SlideOver } from '@/components/ui/SlideOver'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; import { CustomSelect } from '@/components/ui/CustomSelect'; const CANONICAL_DEVICE_TYPES = [ { id: 'laptop', name: 'Laptop' }, { id: 'tablet', name: 'Tablet' }, { id: 'mobile', name: 'Mobile' }, ]; export interface VariantBatchRow { id: string; name: string; price: string; cost: string; duration: string; warranty: string; } function createDefaultBatchRows(): VariantBatchRow[] { return [ { id: '1', name: 'Original OLED Grade A+', price: '', cost: '', duration: '45', warranty: '180' }, { id: '2', name: 'Compatible Premium LCD', price: '', cost: '', duration: '30', warranty: '90' }, ]; } type CatalogTab = 'types' | 'mappings' | 'variants'; function parseApiError(err: any, fallback: string) { if (typeof err?.message === 'string' && err.message) return err.message; if (Array.isArray(err?.detail)) return err.detail.map((d: any) => d.msg).join(', '); return fallback; } export default function ServiceCatalogManagerPage() { const [activeTab, setActiveTab] = useState('types'); const [serviceTypes, setServiceTypes] = useState([]); const [brands, setBrands] = useState([]); const [deviceSeries, setDeviceSeries] = useState([]); const [deviceModels, setDeviceModels] = useState([]); const [repairServices, setRepairServices] = useState([]); const [repairVariants, setRepairVariants] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [showTypeModal, setShowTypeModal] = useState(false); const [showMappingModal, setShowMappingModal] = useState(false); const [showVariantModal, setShowVariantModal] = useState(false); const [typeName, setTypeName] = useState(''); const [typeDesc, setTypeDesc] = useState(''); const [selectedDeviceTypes, setSelectedDeviceTypes] = useState(['mobile']); const [step2CategoryId, setStep2CategoryId] = useState(''); const [step2BrandId, setStep2BrandId] = useState(''); const [step2SeriesId, setStep2SeriesId] = useState(''); const [step2ModelId, setStep2ModelId] = useState(''); const [step2Desc, setStep2Desc] = useState(''); const [step3CategoryId, setStep3CategoryId] = useState(''); const [step3BrandId, setStep3BrandId] = useState(''); const [step3ModelId, setStep3ModelId] = useState(''); const [batchRows, setBatchRows] = useState(createDefaultBatchRows); const [selectedViewBrandId, setSelectedViewBrandId] = useState(''); const [selectedViewModelId, setSelectedViewModelId] = useState(''); const [selectedViewCategoryId, setSelectedViewCategoryId] = useState('ALL'); useEffect(() => { loadAllCatalogData(); }, []); const loadAllCatalogData = async () => { setLoading(true); try { const [sTypes, bData, seriesData, modelsData, servicesData, variantsData] = await Promise.all([ adminService.fetchServiceTypes(), adminService.fetchBrands(), adminService.fetchDeviceSeries(), adminService.fetchDeviceModels(), adminService.fetchRepairServices(), adminService.fetchRepairVariants(), ]); setServiceTypes(sTypes || []); setBrands(bData || []); setDeviceSeries(seriesData || []); setDeviceModels(modelsData || []); setRepairServices(servicesData || []); setRepairVariants(variantsData || []); } catch (err: any) { toast.error(err.message || 'Failed to load service catalog data.'); } finally { setLoading(false); } }; const modelsMap = useMemo(() => { const map = new Map(); deviceModels.forEach((m) => map.set(m.model_id, m)); return map; }, [deviceModels]); const serviceTypesMap = useMemo(() => { const map = new Map(); serviceTypes.forEach((st) => map.set(st.service_type_id, st)); return map; }, [serviceTypes]); const repairServicesMap = useMemo(() => { const map = new Map(); repairServices.forEach((rs) => map.set(rs.repair_service_id, rs)); return map; }, [repairServices]); const viewModelsList = useMemo(() => { if (!selectedViewBrandId) return deviceModels; return deviceModels.filter((m) => m.brand_id === selectedViewBrandId); }, [deviceModels, selectedViewBrandId]); const brandsMap = useMemo(() => { const map = new Map(); brands.forEach((b) => map.set(b.brand_id, b)); return map; }, [brands]); const step2SeriesList = useMemo(() => { if (!step2BrandId) return []; return deviceSeries.filter((s) => s.brand_id === step2BrandId); }, [deviceSeries, step2BrandId]); const step2ModelsList = useMemo(() => { if (!step2BrandId) return []; let list = deviceModels.filter((m) => m.brand_id === step2BrandId); if (step2SeriesId) list = list.filter((m) => m.series_id === step2SeriesId); return list; }, [deviceModels, step2BrandId, step2SeriesId]); const step3MappedBrandsList = useMemo(() => { if (!step3CategoryId) return brands; const matchingServiceMappings = repairServices.filter((rs) => rs.service_type_id === step3CategoryId); const mappedModelIds = new Set(matchingServiceMappings.map((rs) => rs.model_id)); const mappedModels = deviceModels.filter((m) => mappedModelIds.has(m.model_id)); const mappedBrandIds = new Set(mappedModels.map((m) => m.brand_id)); if (mappedBrandIds.size > 0) return brands.filter((b) => mappedBrandIds.has(b.brand_id)); return brands; }, [repairServices, deviceModels, brands, step3CategoryId]); const step3MappedModelsList = useMemo(() => { if (!step3CategoryId || !step3BrandId) return []; const matchingServiceMappings = repairServices.filter((rs) => rs.service_type_id === step3CategoryId); const mappedModelIds = new Set(matchingServiceMappings.map((rs) => rs.model_id)); const mapped = deviceModels.filter((m) => m.brand_id === step3BrandId && mappedModelIds.has(m.model_id)); if (mapped.length > 0) return mapped; return deviceModels.filter((m) => m.brand_id === step3BrandId); }, [repairServices, deviceModels, step3CategoryId, step3BrandId]); const handleCreateServiceType = async (e: React.FormEvent) => { e.preventDefault(); if (!typeName.trim()) { toast.warning('Please enter a service category name.'); return; } setIsSubmitting(true); try { await adminService.createServiceType({ name: typeName.trim(), description: typeDesc.trim() || undefined, }); toast.success(`Service category "${typeName}" created`); setShowTypeModal(false); setTypeName(''); setTypeDesc(''); setSelectedDeviceTypes(['mobile']); await loadAllCatalogData(); } catch (err: any) { toast.error(parseApiError(err, 'Failed to create service category.')); } finally { setIsSubmitting(false); } }; const handleCreateMapping = async (e: React.FormEvent) => { e.preventDefault(); if (!step2CategoryId || !step2ModelId) { toast.warning('Please select both a service category and a device model.'); return; } setIsSubmitting(true); try { await adminService.createRepairService({ model_id: step2ModelId, service_type_id: step2CategoryId, description: step2Desc.trim() || undefined, }); toast.success('Device model mapped to service category'); setShowMappingModal(false); setStep2CategoryId(''); setStep2BrandId(''); setStep2SeriesId(''); setStep2ModelId(''); setStep2Desc(''); await loadAllCatalogData(); } catch (err: any) { toast.error(parseApiError(err, 'Failed to create repair service mapping.')); } finally { setIsSubmitting(false); } }; const handleAddBatchRow = () => { setBatchRows((prev) => [ ...prev, { id: Date.now().toString(), name: '', price: '', cost: '', duration: '45', warranty: '180' }, ]); }; const handleRemoveBatchRow = (id: string) => { if (batchRows.length <= 1) { toast.warning('At least one variant row is required.'); return; } setBatchRows((prev) => prev.filter((r) => r.id !== id)); }; const handleBatchRowChange = (id: string, field: keyof VariantBatchRow, value: string) => { setBatchRows((prev) => prev.map((r) => (r.id === id ? { ...r, [field]: value } : r))); }; const handleCreateBatchVariants = async (e: React.FormEvent) => { e.preventDefault(); if (!step3CategoryId || !step3ModelId) { toast.warning('Please select service category, brand, and device model.'); return; } const validRows = batchRows.filter((r) => r.name.trim() && r.price.trim()); if (validRows.length === 0) { toast.warning('Please enter variant name and price for at least one row.'); return; } setIsSubmitting(true); try { let targetService = repairServices.find( (rs) => rs.model_id === step3ModelId && rs.service_type_id === step3CategoryId ); if (!targetService) { toast.info('Creating model to category mapping automatically...'); targetService = await adminService.createRepairService({ model_id: step3ModelId, service_type_id: step3CategoryId, }); } let createdCount = 0; for (const row of validRows) { await adminService.createRepairVariant({ repair_service_id: targetService.repair_service_id, name: row.name.trim(), price: parseFloat(row.price), cost: row.cost ? parseFloat(row.cost) : undefined, duration_minutes: parseInt(row.duration, 10) || 45, warranty_days: parseInt(row.warranty, 10) || 180, }); createdCount++; } toast.success(`Created ${createdCount} repair variants`); setShowVariantModal(false); setStep3CategoryId(''); setStep3BrandId(''); setStep3ModelId(''); setBatchRows(createDefaultBatchRows()); await loadAllCatalogData(); } catch (err: any) { toast.error(parseApiError(err, 'Failed to batch create repair variants.')); } finally { setIsSubmitting(false); } }; const modelFilteredMappings = useMemo(() => { return repairServices.filter((rs) => { if (selectedViewModelId) { if (rs.model_id !== selectedViewModelId) return false; } else if (selectedViewBrandId) { const model = modelsMap.get(rs.model_id); if (model?.brand_id !== selectedViewBrandId) return false; } return true; }); }, [repairServices, selectedViewModelId, selectedViewBrandId, modelsMap]); const modelFilteredVariants = useMemo(() => { return repairVariants.filter((rv) => { const rs = repairServicesMap.get(rv.repair_service_id); if (!rs) return false; if (selectedViewModelId) { if (rs.model_id !== selectedViewModelId) return false; } else if (selectedViewBrandId) { const model = modelsMap.get(rs.model_id); if (model?.brand_id !== selectedViewBrandId) return false; } if (selectedViewCategoryId !== 'ALL') { if (rs.service_type_id !== selectedViewCategoryId) return false; } return true; }); }, [repairVariants, repairServicesMap, modelsMap, selectedViewModelId, selectedViewBrandId, selectedViewCategoryId]); const filteredServiceTypes = useMemo(() => { const q = searchQuery.trim().toLowerCase(); if (!q) return serviceTypes; return serviceTypes.filter( (st) => (st.name || '').toLowerCase().includes(q) || (st.slug || '').toLowerCase().includes(q) || (st.description || '').toLowerCase().includes(q) ); }, [serviceTypes, searchQuery]); const typesPager = useClientPagination(filteredServiceTypes); const mappingsPager = useClientPagination(modelFilteredMappings); const variantsPager = useClientPagination(modelFilteredVariants); const headerCount = activeTab === 'types' ? serviceTypes.length : activeTab === 'mappings' ? repairServices.length : repairVariants.length; const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const inputClass = 'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary disabled:opacity-50'; const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50'; const openAdd = () => { if (activeTab === 'types') { setTypeName(''); setTypeDesc(''); setSelectedDeviceTypes(['mobile']); setShowTypeModal(true); } else if (activeTab === 'mappings') { setStep2CategoryId(''); setStep2BrandId(''); setStep2SeriesId(''); setStep2ModelId(''); setStep2Desc(''); setShowMappingModal(true); } else { setStep3CategoryId(''); setStep3BrandId(''); setStep3ModelId(''); setBatchRows(createDefaultBatchRows()); setShowVariantModal(true); } }; const addButtonLabel = activeTab === 'types' ? 'Add category' : activeTab === 'mappings' ? 'Map model' : 'Add variants'; const renderActiveBadge = (label = 'Active') => ( {label} ); return (

Service Catalog Manager

{headerCount}
{activeTab === 'types' ? (
setSearchQuery(e.target.value)} className="w-full h-9 pl-9 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
) : (
{ setSelectedViewBrandId(value); setSelectedViewModelId(''); }} placeholder="All brands" options={[ { value: '', label: 'All brands' }, ...brands.map((b) => ({ value: b.brand_id, label: b.name })), ]} />
{ setSelectedViewModelId(value); if (value && !selectedViewBrandId) { const m = modelsMap.get(value); if (m?.brand_id) setSelectedViewBrandId(m.brand_id); } }} placeholder="All models" options={[ { value: '', label: 'All models' }, ...viewModelsList.map((m) => ({ value: m.model_id, label: m.name })), ]} />
{activeTab === 'variants' && (
({ value: st.service_type_id, label: st.name })), ]} />
)}
)}
{( [ { id: 'types', label: `Categories (${serviceTypes.length})` }, { id: 'mappings', label: `Mappings (${repairServices.length})` }, { id: 'variants', label: `Variants (${repairVariants.length})` }, ] as { id: CatalogTab; label: string }[] ).map((tab) => ( ))}
{loading ? (
Loading catalog...
) : activeTab === 'types' ? (
{filteredServiceTypes.length === 0 ? ( ) : ( typesPager.items.map((st, idx) => ( )) )}
Category Slug Description Status
{searchQuery ? 'No categories match your search.' : 'No service categories yet.'}
{st.name}
{st.slug || '—'} {st.description || '—'} {renderActiveBadge()}
) : activeTab === 'mappings' ? (
{modelFilteredMappings.length === 0 ? ( ) : ( mappingsPager.items.map((rs, idx) => { const sType = serviceTypesMap.get(rs.service_type_id); const model = modelsMap.get(rs.model_id); const brand = model ? brandsMap.get(model.brand_id) : null; return ( ); }) )}
Service category Device model Brand Path Mapping ID
No service category mappings found for the selected filter.
{sType?.name || rs.service_type_id} {model?.name || rs.model_id || '—'} {brand?.name || '—'} {rs.full_path || '—'} {rs.repair_service_id}
) : (
{modelFilteredVariants.length === 0 ? ( ) : ( variantsPager.items.map((rv) => { const rs = repairServicesMap.get(rv.repair_service_id); const sType = rs ? serviceTypesMap.get(rs.service_type_id) : null; const model = rs ? modelsMap.get(rs.model_id) : null; return ( ); }) )}
Variant Device model Category Selling price Cost Duration Warranty Status
No repair variants found for the selected filter.
{rv.name} {model?.name || '—'} {sType?.name || '—'} {formatCurrency(rv.price)} {rv.cost ? formatCurrency(rv.cost) : '—'} {rv.duration_minutes} min {rv.warranty_days} days {renderActiveBadge(rv.status || 'Active')}
)} {activeTab === 'types' && } {activeTab === 'mappings' && } {activeTab === 'variants' && }
{ if (isSubmitting) return; setShowTypeModal(false); }} title="Add service category" icon={} >
setTypeName(e.target.value)} className={inputClass} />