import React, { useState, useMemo } from 'react'; import ReactDOM from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { useTheme } from '../context/ThemeContext'; import { useAuth, ROLES } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; import { Plus, Search, FileText, Layers, Calendar, DollarSign, User, CheckCircle, Clock, Pencil, Trash2, Copy, ChevronRight, Tag, Package, ArrowDownUp, ArrowUp, ArrowDown, X as XIcon, Shield } from 'lucide-react'; import { toast } from 'sonner'; import TemplateEditorModal from '../components/estimates/TemplateEditorModal'; import EstimateDetailModal from '../components/estimates/EstimateDetailModal'; import TemplateAccessModal from '../components/estimates/TemplateAccessModal'; import { ALL_STATUS_CONFIG } from '../components/estimates/EstimateDetailModal'; import { computeGrandTotal } from '../utils/estimateExport'; // --------------------------------------------------------------------------- // Status badge — uses shared ALL_STATUS_CONFIG from EstimateDetailModal // --------------------------------------------------------------------------- function StatusBadge({ status }) { const cfg = ALL_STATUS_CONFIG[status] ?? ALL_STATUS_CONFIG['Draft']; const Icon = cfg.icon; return ( {status} ); } // --------------------------------------------------------------------------- // Category color // --------------------------------------------------------------------------- const CATEGORY_COLOR = { Roofing: { bg: 'bg-blue-100 dark:bg-blue-500/20', text: 'text-blue-700 dark:text-blue-400' }, Siding: { bg: 'bg-amber-100 dark:bg-amber-500/20', text: 'text-amber-700 dark:text-amber-400' }, Gutters: { bg: 'bg-cyan-100 dark:bg-cyan-500/20', text: 'text-cyan-700 dark:text-cyan-400' }, Windows: { bg: 'bg-purple-100 dark:bg-purple-500/20',text: 'text-purple-700 dark:text-purple-400' }, Insulation: { bg: 'bg-emerald-100 dark:bg-emerald-500/20',text: 'text-emerald-700 dark:text-emerald-400' }, Other: { bg: 'bg-zinc-100 dark:bg-zinc-700', text: 'text-zinc-600 dark:text-zinc-300' }, }; function CategoryBadge({ category }) { const cfg = CATEGORY_COLOR[category] ?? CATEGORY_COLOR.Other; return ( {category} ); } // --------------------------------------------------------------------------- // Estimate Card // --------------------------------------------------------------------------- function EstimateCard({ estimate, computedTotal, index, onClick }) { const displayTotal = computedTotal ?? estimate.total; return (

{estimate.clientName}

{estimate.address}

{estimate.date} {estimate.estimatedBy} {estimate.templateName && ( {estimate.templateName} )}
${displayTotal.toLocaleString('en-US', { minimumFractionDigits: 2 })} View
{estimate.notes && (

{estimate.notes}

)}
); } // --------------------------------------------------------------------------- // Template Card (Owner: editable; others: read-only) // --------------------------------------------------------------------------- function TemplateCard({ template, isOwner, onEdit, onDuplicate, onDelete, index }) { const catCfg = CATEGORY_COLOR[template.category] ?? CATEGORY_COLOR.Other; return ( {/* Header */}

{template.name}

{template.brand}
{/* Description preview */} {template.description?.length > 0 && (

{template.description[0]}

)} {/* Material count */}
{template.materials?.length || 0} material line items
{/* Actions — Owner only */} {isOwner && (
)}
); } // --------------------------------------------------------------------------- // Delete confirm modal // --------------------------------------------------------------------------- function DeleteConfirmModal({ template, onConfirm, onCancel }) { if (!template) return null; return ReactDOM.createPortal(
e.stopPropagation()} >

Delete Template

Are you sure you want to delete {template.name}? This cannot be undone.

, document.body ); } // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- export default function EstimatesPage() { const navigate = useNavigate(); const { theme } = useTheme(); const { user } = useAuth(); const { estimates, templates, addTemplate, updateTemplate, deleteTemplate, templateAccessRoles, templateAccessUsers, templateAccessExcluded, } = useMockStore(); const isOwner = user?.role === ROLES.OWNER; const basePath = user?.role === 'OWNER' ? '/owner' : user?.role === 'ADMIN' ? '/admin' : '/emp/fa'; // Effective template edit permission (Owner always; others via role/individual grant minus exclusions) const canEditTemplates = isOwner || (templateAccessRoles.includes(user?.role) && !templateAccessExcluded.includes(user?.id)) || templateAccessUsers.includes(user?.id); const [activeTab, setActiveTab] = useState('estimates'); // 'estimates' | 'templates' const [estimateSearch, setEstimateSearch] = useState(''); const [estimateStatusFilter, setEstimateStatusFilter] = useState('All'); const [sortBy, setSortBy] = useState('newest'); // 'newest' | 'oldest' | 'status' const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [templateSearch, setTemplateSearch] = useState(''); const [templateCategoryFilter, setTemplateCategoryFilter] = useState('All'); // Estimate detail modal const [detailEstimate, setDetailEstimate] = useState(null); // Template editor modal state const [editorOpen, setEditorOpen] = useState(false); const [editorMode, setEditorMode] = useState('create'); const [editorTemplate, setEditorTemplate] = useState(null); // Delete confirm const [deleteTarget, setDeleteTarget] = useState(null); // Access management modal (Owner only) const [accessModalOpen, setAccessModalOpen] = useState(false); // ---- filtered + sorted estimates ---- const STATUS_SORT_ORDER = ['Draft', 'Sent', 'Waiting Approval', 'Approved', 'Follow Up Required', 'Revision Required', 'Rejected', 'Expired']; const filteredEstimates = useMemo(() => { let result = estimates.filter(e => { const q = estimateSearch.toLowerCase(); const matchesSearch = e.clientName.toLowerCase().includes(q) || e.address.toLowerCase().includes(q) || (e.templateName || '').toLowerCase().includes(q); const matchesStatus = estimateStatusFilter === 'All' || e.status === estimateStatusFilter; const matchesFrom = !dateFrom || e.date >= dateFrom; const matchesTo = !dateTo || e.date <= dateTo; return matchesSearch && matchesStatus && matchesFrom && matchesTo; }); if (sortBy === 'newest') { result = [...result].sort((a, b) => b.date.localeCompare(a.date)); } else if (sortBy === 'oldest') { result = [...result].sort((a, b) => a.date.localeCompare(b.date)); } else if (sortBy === 'status') { result = [...result].sort((a, b) => STATUS_SORT_ORDER.indexOf(a.status) - STATUS_SORT_ORDER.indexOf(b.status) ); } return result; }, [estimates, estimateSearch, estimateStatusFilter, sortBy, dateFrom, dateTo]); // ---- filtered templates ---- const filteredTemplates = useMemo(() => { return templates.filter(t => { const matchesSearch = t.name.toLowerCase().includes(templateSearch.toLowerCase()) || (t.brand || '').toLowerCase().includes(templateSearch.toLowerCase()); const matchesCat = templateCategoryFilter === 'All' || t.category === templateCategoryFilter; return matchesSearch && matchesCat; }); }, [templates, templateSearch, templateCategoryFilter]); // ---- template editor actions ---- const openCreate = () => { setEditorMode('create'); setEditorTemplate(null); setEditorOpen(true); }; const openEdit = (tpl) => { setEditorMode('edit'); setEditorTemplate(tpl); setEditorOpen(true); }; const openDuplicate = (tpl) => { setEditorMode('duplicate'); setEditorTemplate(tpl); setEditorOpen(true); }; const handleEditorSave = (data, saveMode) => { if (editorMode === 'create') { addTemplate(data); toast.success('Template created successfully'); } else if (editorMode === 'edit') { updateTemplate(editorTemplate.id, data); toast.success('Template updated'); } else if (editorMode === 'duplicate') { if (saveMode === 'replace') { updateTemplate(editorTemplate.id, data); toast.success('Template replaced'); } else { addTemplate(data); toast.success('Template saved as new copy'); } } setEditorOpen(false); }; const handleDeleteConfirm = () => { if (deleteTarget) { deleteTemplate(deleteTarget.id); toast.success(`"${deleteTarget.name}" deleted`); setDeleteTarget(null); } }; // ---- computed totals map (same formula as detail modal) ---- const computedTotals = useMemo(() => { const map = {}; estimates.forEach(est => { const tpl = templates.find(t => t.id === est.templateId); map[est.id] = tpl && est.roofArea ? computeGrandTotal(tpl, est.roofArea) : est.total; }); return map; }, [estimates, templates]); // ---- stats ---- const stats = useMemo(() => { const total = estimates.length; const approved = estimates.filter(e => e.status === 'Approved').length; const totalValue = estimates .filter(e => e.status === 'Approved') .reduce((s, e) => s + (computedTotals[e.id] ?? e.total), 0); const pending = estimates.filter(e => e.status === 'Sent' || e.status === 'Draft').length; return { total, approved, totalValue, pending }; }, [estimates, computedTotals]); const TABS = [ { key: 'estimates', label: 'Estimates', count: estimates.length }, { key: 'templates', label: 'Templates', count: templates.length }, ]; const ESTIMATE_STATUSES = ['All', 'Draft', 'Sent', 'Waiting Approval', 'Approved', 'Follow Up Required', 'Revision Required', 'Rejected', 'Expired']; const TEMPLATE_CATEGORIES_FILTER = ['All', 'Roofing', 'Siding', 'Gutters', 'Windows', 'Insulation', 'Other']; return (
{/* Page header */}

Estimates

Manage estimates and master templates

{/* KPI strip */}
{[ { label: 'Total Estimates', value: stats.total, icon: FileText, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-500/10' }, { label: 'Approved', value: stats.approved, icon: CheckCircle, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-500/10' }, { label: 'Pending', value: stats.pending, icon: Clock, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-500/10' }, { label: 'Approved Value', value: `$${(stats.totalValue / 1000).toFixed(0)}k`, icon: DollarSign, color: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10' }, ].map(s => { const Icon = s.icon; return (

{s.label}

{s.value}

); })}
{/* Tabs */}
{TABS.map(tab => ( ))}
{/* ===== ESTIMATES TAB ===== */} {activeTab === 'estimates' && ( {/* Filters row 1: Search + Sort */}
setEstimateSearch(e.target.value)} placeholder="Search by client, address, or template..." className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 text-sm outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" />
{/* Sort control */}
{[ { key: 'newest', icon: ArrowDown, label: 'Newest' }, { key: 'oldest', icon: ArrowUp, label: 'Oldest' }, { key: 'status', icon: ArrowDownUp, label: 'Status' }, ].map(({ key, icon: Icon, label }) => ( ))}
{/* Filters row 2: Status chips + Date range */}
{ESTIMATE_STATUSES.map(s => ( ))}
{/* Date range */}
setDateFrom(e.target.value)} title="From date" className="px-3 py-1.5 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 text-xs outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" /> setDateTo(e.target.value)} title="To date" className="px-3 py-1.5 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 text-xs outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" /> {(dateFrom || dateTo) && ( )}
{filteredEstimates.length === 0 ? (

No estimates found

Try adjusting your search or filters

) : (
{filteredEstimates.map((est, i) => ( setDetailEstimate(est)} /> ))}
)}
)} {/* ===== TEMPLATES TAB ===== */} {activeTab === 'templates' && ( {/* Filters + create button */}
setTemplateSearch(e.target.value)} placeholder="Search templates by name or brand..." className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 text-sm outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" />
{TEMPLATE_CATEGORIES_FILTER.map(c => ( ))} {canEditTemplates && ( )} {isOwner && ( )}
{!canEditTemplates && (
Templates are managed by the Owner. You can apply these when creating a new estimate.
)} {filteredTemplates.length === 0 ? (

No templates found

{canEditTemplates && ( )}
) : (
{filteredTemplates.map((tpl, i) => ( ))}
)}
)}
{/* Estimate detail drawer */} setDetailEstimate(null)} estimate={detailEstimate} /> {/* Template editor modal */} setEditorOpen(false)} onSave={handleEditorSave} mode={editorMode} template={editorTemplate} /> {/* Delete confirm */} {deleteTarget && ( setDeleteTarget(null)} /> )} {/* Template access modal — Owner only */} setAccessModalOpen(false)} />
); }