import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import { estimatesApi, type Estimate, type EstimateStage, type StageHistoryEntry } from '../../api/estimates';
import {
ArrowLeft,
ChevronRight,
Clock,
MapPin,
Phone,
Mail,
User,
Calendar,
Tag,
DollarSign,
FileText,
AlertTriangle,
CheckCircle2,
XCircle,
AlertCircle,
FileCheck,
ClipboardList,
Trash2,
History,
Building2,
Package,
} from 'lucide-react';
const STAGE_CONFIG: Record<EstimateStage, { label: string; bg: string; text: string; icon: any }> = {
draft: { label: 'Draft', bg: 'bg-slate-100', text: 'text-slate-700', icon: FileText },
delivered: { label: 'Delivered', bg: 'bg-blue-100', text: 'text-blue-700', icon: ClipboardList },
accepted: { label: 'Accepted', bg: 'bg-emerald-100', text: 'text-emerald-700', icon: CheckCircle2 },
scheduled: { label: 'Scheduled', bg: 'bg-amber-100', text: 'text-amber-700', icon: Calendar },
closed: { label: 'Closed', bg: 'bg-green-100', text: 'text-green-700', icon: CheckCircle2 },
rejected: { label: 'Rejected', bg: 'bg-red-100', text: 'text-red-700', icon: XCircle },
expired: { label: 'Expired', bg: 'bg-gray-100', text: 'text-gray-700', icon: AlertCircle },
};
const VALID_TRANSITIONS: Record<EstimateStage, EstimateStage[]> = {
draft: ['delivered'],
delivered: ['accepted', 'rejected', 'expired'],
accepted: ['scheduled', 'rejected'],
scheduled: ['closed', 'rejected'],
rejected: [],
expired: [],
closed: [],
};
function formatCurrency(value: number): string {
if (Math.abs(value) >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
return `$${value.toLocaleString()}`;
}
function formatDate(dateStr: string | null): string {
if (!dateStr) return '—';
try {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
} catch {
return dateStr;
}
}
function normalizeStageLabel(entry: StageHistoryEntry): string {
if (entry.to_stage) return entry.to_stage;
if (entry.stage) return entry.stage;
return 'unknown';
}
function normalizeFromStage(entry: StageHistoryEntry): EstimateStage | null {
return entry.from_stage || null;
}
export function EstimatesDetailPage() {
const { estimateId } = useParams<{ estimateId: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const [transitionStage, setTransitionStage] = useState<EstimateStage | null>(null);
const [transitionNotes, setTransitionNotes] = useState('');
const queryClient = useQueryClient();
const companyId = user?.tenantId || '';
if (!estimateId || !companyId) {
return (
<AppLayout>
<div className="flex items-center justify-center h-full">
<p className="text-surface-500">Invalid estimate ID</p>
</div>
</AppLayout>
);
}
const { data, isLoading, error } = useQuery({
queryKey: ['estimate-detail', companyId, estimateId],
queryFn: () => estimatesApi.getEstimate(companyId, estimateId),
enabled: !!companyId && !!estimateId,
});
const { data: historyData } = useQuery({
queryKey: ['estimate-history', companyId, estimateId],
queryFn: () => estimatesApi.getStageHistory(companyId, estimateId),
enabled: !!companyId && !!estimateId,
});
const transitionMutation = useMutation({
mutationFn: (payload: { stage: EstimateStage; notes?: string }) =>
estimatesApi.transitionEstimate(companyId, estimateId, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['estimate-detail', companyId, estimateId] });
queryClient.invalidateQueries({ queryKey: ['estimate-history', companyId, estimateId] });
setTransitionStage(null);
setTransitionNotes('');
},
});
const handleTransition = (stage: EstimateStage) => {
setTransitionStage(stage);
};
const confirmTransition = () => {
if (!transitionStage) return;
transitionMutation.mutate({ stage: transitionStage, notes: transitionNotes || undefined });
};
if (isLoading) {
return (
<AppLayout>
<div className="flex items-center justify-center h-full">
<LoadingSpinner />
</div>
</AppLayout>
);
}
if (error || !data) {
return (
<AppLayout>
<div className="flex flex-col items-center justify-center h-full gap-4">
<AlertTriangle className="h-12 w-12 text-red-400" />
<p className="text-surface-700 font-medium">Failed to load estimate</p>
<button
onClick={() => navigate('/app/estimates')}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700"
>
<ArrowLeft className="h-4 w-4" />
Back to Estimates
</button>
</div>
</AppLayout>
);
}
const estimate = data.estimate;
const stage = estimate.stage;
const stageConfig = STAGE_CONFIG[stage] || STAGE_CONFIG.draft;
const StageIcon = stageConfig.icon;
const validTransitions = VALID_TRANSITIONS[stage] || [];
const stageHistory = estimate.stage_history || [];
const mergedHistory = historyData?.stage_history || stageHistory;
return (
<AppLayout>
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<div>
<button
onClick={() => navigate('/app/estimates')}
className="flex items-center gap-2 text-sm text-surface-500 hover:text-surface-700 mb-4"
>
<ArrowLeft className="h-4 w-4" />
Back to Estimates
</button>
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-2xl font-bold text-surface-900">
{estimate.estimate_number}
</h1>
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold ${stageConfig.bg} ${stageConfig.text}`}>
<StageIcon className="h-4 w-4" />
{stageConfig.label}
</span>
</div>
<p className="text-lg text-surface-600">
{estimate.customer_name} — {estimate.project_type}
</p>
</div>
<div className="text-right">
<p className="text-3xl font-bold text-surface-900">
{formatCurrency(estimate.total_value)}
</p>
<p className="text-sm text-surface-500 mt-1">Total Estimate Value</p>
</div>
</div>
</div>
{/* Stage Transition */}
{validTransitions.length > 0 && !transitionStage && (
<div className="bg-white rounded-xl border border-surface-200 p-4">
<h3 className="text-sm font-semibold text-surface-700 mb-3 flex items-center gap-2">
<ChevronRight className="h-4 w-4" />
Transition Estimate
</h3>
<div className="flex flex-wrap gap-2">
{validTransitions.map((next) => {
const config = STAGE_CONFIG[next];
const Icon = config.icon;
return (
<button
key={next}
onClick={() => handleTransition(next)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold border border-surface-200 hover:border-surface-300 transition-colors ${config.bg} ${config.text}`}
>
<Icon className="h-4 w-4" />
→ {config.label}
</button>
);
})}
</div>
</div>
)}
{/* Transition Confirmation Modal */}
{transitionStage && (
<div className="bg-white rounded-xl border-2 border-brand-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-3">
Confirm: {stageConfig.label} → {STAGE_CONFIG[transitionStage].label}
</h3>
<textarea
value={transitionNotes}
onChange={(e) => setTransitionNotes(e.target.value)}
placeholder="Add notes (optional)..."
className="w-full border border-surface-200 rounded-lg px-3 py-2 text-sm mb-3 focus:outline-none focus:ring-2 focus:ring-brand-500"
rows={2}
/>
<div className="flex gap-2">
<button
onClick={confirmTransition}
disabled={transitionMutation.isPending}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm font-semibold hover:bg-brand-700 disabled:opacity-50"
>
{transitionMutation.isPending ? 'Transitioning...' : 'Confirm'}
</button>
<button
onClick={() => {
setTransitionStage(null);
setTransitionNotes('');
}}
className="px-4 py-2 border border-surface-200 rounded-lg text-sm font-medium text-surface-600 hover:bg-surface-50"
>
Cancel
</button>
</div>
</div>
)}
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Details */}
<div className="lg:col-span-2 space-y-6">
{/* Customer & Project Info */}
<div className="bg-white rounded-xl border border-surface-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-4 flex items-center gap-2">
<User className="h-4 w-4" />
Customer & Project
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Customer</p>
<p className="text-sm font-medium text-surface-900">{estimate.customer_name}</p>
</div>
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Project Type</p>
<p className="text-sm font-medium text-surface-900">{estimate.project_type}</p>
</div>
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Email</p>
<a href={`mailto:${estimate.customer_email}`} className="text-sm text-brand-600 hover:underline flex items-center gap-1">
<Mail className="h-3 w-3" />
{estimate.customer_email}
</a>
</div>
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Phone</p>
<a href={`tel:${estimate.customer_phone}`} className="text-sm text-brand-600 hover:underline flex items-center gap-1">
<Phone className="h-3 w-3" />
{estimate.customer_phone}
</a>
</div>
<div className="sm:col-span-2">
<p className="text-xs text-surface-400 uppercase tracking-wide">Address</p>
<p className="text-sm font-medium text-surface-900 flex items-start gap-1">
<MapPin className="h-3 w-3 mt-0.5 shrink-0" />
{estimate.address}, {estimate.city}, {estimate.state} {estimate.zip_code}
</p>
</div>
<div className="sm:col-span-2">
<p className="text-xs text-surface-400 uppercase tracking-wide">Description</p>
<p className="text-sm text-surface-700">{estimate.description || '—'}</p>
</div>
</div>
</div>
{/* Financial Summary */}
<div className="bg-white rounded-xl border border-surface-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-4 flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Financial Summary
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b border-surface-100">
<span className="text-sm text-surface-600">Subtotal</span>
<span className="text-sm font-medium">{formatCurrency(estimate.total_value - estimate.tax_amount - estimate.discount_amount)}</span>
</div>
{estimate.discount_amount > 0 && (
<div className="flex justify-between items-center py-2 border-b border-surface-100">
<span className="text-sm text-surface-600">Discount</span>
<span className="text-sm font-medium text-red-600">-{formatCurrency(estimate.discount_amount)}</span>
</div>
)}
{estimate.tax_amount > 0 && (
<div className="flex justify-between items-center py-2 border-b border-surface-100">
<span className="text-sm text-surface-600">Tax</span>
<span className="text-sm font-medium">+{formatCurrency(estimate.tax_amount)}</span>
</div>
)}
<div className="flex justify-between items-center py-2">
<span className="text-sm font-semibold text-surface-900">Total</span>
<span className="text-lg font-bold text-surface-900">{formatCurrency(estimate.total_value)}</span>
</div>
{estimate.deposit > 0 && (
<div className="mt-2 p-3 bg-emerald-50 rounded-lg border border-emerald-200">
<div className="flex justify-between items-center">
<span className="text-sm text-emerald-700 font-medium">Deposit</span>
<span className="text-sm font-bold text-emerald-700">{formatCurrency(estimate.deposit)}</span>
</div>
</div>
)}
</div>
{/* Line Items */}
{estimate.line_items && estimate.line_items.length > 0 && (
<div className="mt-4">
<h4 className="text-xs font-semibold text-surface-500 uppercase tracking-wide mb-2">
Line Items ({estimate.line_items.length})
</h4>
<div className="max-h-48 overflow-y-auto rounded-lg border border-surface-200">
<table className="w-full text-sm">
<thead className="bg-surface-50 sticky top-0">
<tr>
<th className="text-left px-3 py-2 text-xs font-semibold text-surface-500">Description</th>
<th className="text-right px-3 py-2 text-xs font-semibold text-surface-500">Qty</th>
<th className="text-right px-3 py-2 text-xs font-semibold text-surface-500">Price</th>
<th className="text-right px-3 py-2 text-xs font-semibold text-surface-500">Total</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-100">
{estimate.line_items.map((item, i) => (
<tr key={i} className="bg-white">
<td className="px-3 py-2 text-surface-700 max-w-[200px] truncate">{item.description}</td>
<td className="px-3 py-2 text-right text-surface-600">{item.quantity}</td>
<td className="px-3 py-2 text-right text-surface-600">{formatCurrency(item.unit_price)}</td>
<td className="px-3 py-2 text-right font-medium text-surface-900">{formatCurrency(item.total)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* Rejection Details */}
{(estimate.rejection_reason || estimate.competitor_name) && (
<div className="bg-white rounded-xl border border-red-200 p-5">
<h3 className="text-sm font-semibold text-red-700 mb-4 flex items-center gap-2">
<AlertTriangle className="h-4 w-4" />
Loss Details
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{estimate.rejection_reason && (
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Rejection Reason</p>
<p className="text-sm text-surface-700">{estimate.rejection_reason}</p>
</div>
)}
{estimate.competitor_name && (
<div>
<p className="text-xs text-surface-400 uppercase tracking-wide">Competitor</p>
<p className="text-sm text-surface-700">{estimate.competitor_name}</p>
{estimate.competitor_price !== null && (
<p className="text-xs text-surface-500">
at {formatCurrency(estimate.competitor_price || 0)}
</p>
)}
</div>
)}
</div>
</div>
)}
{/* Notes */}
{estimate.notes && (
<div className="bg-white rounded-xl border border-surface-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-3 flex items-center gap-2">
<FileText className="h-4 w-4" />
Notes
</h3>
<p className="text-sm text-surface-700 whitespace-pre-wrap">{estimate.notes}</p>
</div>
)}
</div>
{/* Right Column */}
<div className="space-y-6">
{/* Stage History Timeline */}
<div className="bg-white rounded-xl border border-surface-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-4 flex items-center gap-2">
<History className="h-4 w-4" />
Stage History
</h3>
{mergedHistory.length === 0 ? (
<p className="text-sm text-surface-400">No history recorded</p>
) : (
<div className="relative space-y-0">
{/* Timeline line */}
<div className="absolute left-3.5 top-2 bottom-2 w-0.5 bg-surface-200" />
{mergedHistory.map((entry, index) => {
const stageLabel = normalizeStageLabel(entry);
const fromStage = normalizeFromStage(entry);
const config = STAGE_CONFIG[stageLabel] || STAGE_CONFIG.draft;
const Icon = config.icon;
const isInitial = !fromStage;
return (
<div key={index} className="relative flex gap-3 pb-4 last:pb-0">
{/* Dot */}
<div className={`relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 ${config.bg} ${config.text} border-white`}>
<Icon className="h-3 w-3" />
</div>
{/* Content */}
<div className="flex-1 min-w-0 pt-0.5">
<div className="flex items-center gap-1.5 flex-wrap">
{isInitial ? (
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${config.bg} ${config.text}`}>
{config.label}
</span>
) : (
<>
<span className="text-xs text-surface-400 line-through">
{STAGE_CONFIG[fromStage]?.label || fromStage}
</span>
<ChevronRight className="h-3 w-3 text-surface-300" />
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${config.bg} ${config.text}`}>
{config.label}
</span>
</>
)}
</div>
<p className="text-xs text-surface-400 mt-0.5">{formatDate(entry.timestamp)}</p>
{entry.notes && (
<p className="text-xs text-surface-500 mt-1 bg-surface-50 rounded px-2 py-1">
{entry.notes}
</p>
)}
</div>
</div>
);
})}
</div>
)}
</div>
{/* Meta Info */}
<div className="bg-white rounded-xl border border-surface-200 p-5">
<h3 className="text-sm font-semibold text-surface-700 mb-4">Details</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<Tag className="h-3 w-3" />
Source
</span>
<span className="text-sm font-medium capitalize">{estimate.source}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<Building2 className="h-3 w-3" />
Days in Stage
</span>
<span className="text-sm font-medium">{estimate.days_in_stage ?? '—'}</span>
</div>
{estimate.assigned_to && (
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<User className="h-3 w-3" />
Assigned To
</span>
<span className="text-sm font-medium">{estimate.assigned_to}</span>
</div>
)}
{estimate.expires_at && (
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<AlertCircle className="h-3 w-3" />
Expires
</span>
<span className="text-sm font-medium">{formatDate(estimate.expires_at)}</span>
</div>
)}
{estimate.scheduled_date && (
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400 flex items-center gap-1.5">
<Calendar className="h-3 w-3" />
Scheduled
</span>
<span className="text-sm font-medium">{formatDate(estimate.scheduled_date)}</span>
</div>
)}
<div className="border-t border-surface-100 pt-3 mt-3">
<div className="flex items-center justify-between">
<span className="text-xs text-surface-400">Created</span>
<span className="text-xs text-surface-500">{formatDate(estimate.created_at)}</span>
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-xs text-surface-400">Updated</span>
<span className="text-xs text-surface-500">{formatDate(estimate.updated_at)}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
}
export default EstimatesDetailPage;