import { MainLayout } from '@/components/layout/MainLayout.tsx'; import PageHeader from '@/components/layout/PageHeader.tsx'; import MatchCard from '@/components/matches/MatchCard.tsx'; import { FormField } from '@/components/ui'; import Badge from '@/components/ui/Badge.tsx'; import Button from '@/components/ui/Button.tsx'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx'; import MetricItem from '@/components/ui/MetricItem.tsx'; import Select from '@/components/ui/Select.tsx'; import Textarea from '@/components/ui/Textarea.tsx'; import type { TimelineEntry } from '@/components/ui/Timeline.tsx'; import Timeline from '@/components/ui/Timeline.tsx'; import { Container, Flex, Grid, Stack } from '@/components/ui/layout'; import { useAuth } from '@/contexts/AuthContext.tsx'; import { useMatch, useUpdateMatchStatus } from '@/hooks/api/useMatchingAPI.ts'; import { useTranslation } from '@/hooks/useI18n.tsx'; import { useNavigation } from '@/hooks/useNavigation.tsx'; import { AlertTriangle, ArrowLeft, CheckCircle, Clock, DollarSign, MapPin, TrendingUp, } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { formatCurrency } from '../lib/fin'; const MatchDetailPage = () => { const { id: matchId } = useParams<{ id: string }>(); const { t } = useTranslation(); const { handleBackNavigation, handleFooterNavigate } = useNavigation(); const { user } = useAuth(); const { data: match, isLoading, error } = useMatch(matchId); const updateStatusMutation = useUpdateMatchStatus(); const [showStatusUpdate, setShowStatusUpdate] = useState(false); const [newStatus, setNewStatus] = useState(''); const [statusNotes, setStatusNotes] = useState(''); const getHistoryTitle = useCallback( (action: string, value?: string) => { switch (action) { case 'status_change': return t('matchDetail.statusChanged'); case 'comment': return t('matchDetail.commentAdded'); case 'update': return t('matchDetail.matchUpdated'); default: return value || action; } }, [t] ); // Transform match history to timeline format const timelineEntries: TimelineEntry[] = useMemo(() => { if (!match?.History) return []; return match.History.map((entry, index) => ({ id: `history-${index}`, timestamp: entry.timestamp, title: getHistoryTitle(entry.action, entry.new_value || entry.old_value), description: entry.notes, actor: entry.actor, action: entry.action, oldValue: entry.old_value, newValue: entry.newValue, })); }, [match.History, getHistoryTitle]); const handleStatusUpdate = async () => { if (!match || !newStatus || !user) return; try { await updateStatusMutation.mutateAsync({ matchId: match.ID, status: newStatus, actor: user.email || user.name || 'Unknown', notes: statusNotes, }); setShowStatusUpdate(false); setNewStatus(''); setStatusNotes(''); } catch (error) { console.error('Failed to update match status:', error); } }; // central fin.formatCurrency used throughout the app const formatScore = (score: number) => { return `${(score * 100).toFixed(1)}%`; }; if (isLoading) { return ( ); } if (error || !match) { return ( {error?.message || t('matchDetail.notFound')} {t('common.back')} ); } const statusOptions = [ { value: 'suggested', label: t('matchStatus.suggested') }, { value: 'negotiating', label: t('matchStatus.negotiating') }, { value: 'reserved', label: t('matchStatus.reserved') }, { value: 'contracted', label: t('matchStatus.contracted') }, { value: 'live', label: t('matchStatus.live') }, { value: 'failed', label: t('matchStatus.failed') }, { value: 'cancelled', label: t('matchStatus.cancelled') }, ]; return ( {/* Match Overview Card */} {/* Key Metrics */} } label={t('matchDetail.compatibilityScore')} value={formatScore(match.CompatibilityScore)} /> } label={t('matchDetail.economicValue')} value={formatCurrency(match.EconomicValue)} /> } label={t('matchDetail.distance')} value={`${match.DistanceKm.toFixed(1)} km`} /> } label={t('matchDetail.priority')} value={match.Priority.toString()} /> {/* Economic Impact & Risk Assessment */} {/* Economic Impact */} {match.EconomicImpact && ( {t('matchDetail.economicImpact')} {match.EconomicImpact.annual_savings && ( {t('matchDetail.annualSavings')} {formatCurrency(match.EconomicImpact.annual_savings)} )} {match.EconomicImpact.npv && ( {t('matchDetail.npv')} {formatCurrency(match.EconomicImpact.npv)} )} {match.EconomicImpact.irr && ( {t('matchDetail.irr')} {formatScore(match.EconomicImpact.irr)} )} {match.EconomicImpact.payback_years && ( {t('matchDetail.paybackPeriod')} {t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1), })} )} )} {/* Risk Assessment */} {match.RiskAssessment && ( {t('matchDetail.riskAssessment')} {match.RiskAssessment.technical_risk !== undefined && ( {t('matchDetail.technicalRisk')} 0.7 ? 'destructive' : match.RiskAssessment.technical_risk > 0.4 ? 'secondary' : 'default' } > {formatScore(match.RiskAssessment.technical_risk)} )} {match.RiskAssessment.regulatory_risk !== undefined && ( {t('matchDetail.regulatoryRisk')} 0.7 ? 'destructive' : match.RiskAssessment.regulatory_risk > 0.4 ? 'secondary' : 'default' } > {formatScore(match.RiskAssessment.regulatory_risk)} )} {match.RiskAssessment.market_risk !== undefined && ( {t('matchDetail.marketRisk')} 0.7 ? 'destructive' : match.RiskAssessment.market_risk > 0.4 ? 'secondary' : 'default' } > {formatScore(match.RiskAssessment.market_risk)} )} )} {/* Status Update Section */} {t('matchDetail.updateStatus')} setShowStatusUpdate(!showStatusUpdate)} > {showStatusUpdate ? t('common.cancel') : t('matchDetail.changeStatus')} {showStatusUpdate && ( setStatusNotes(e.target.value)} placeholder={t('matchDetail.notesPlaceholder')} rows={3} /> {updateStatusMutation.isPending ? t('common.updating') : t('matchDetail.updateStatus')} { setShowStatusUpdate(false); setNewStatus(''); setStatusNotes(''); }} > {t('common.cancel')} )} {/* Match History Timeline */} {t('matchDetail.matchHistory')} ); }; export default MatchDetailPage;
{error?.message || t('matchDetail.notFound')}