import { MainLayout } from '@/components/layout/MainLayout.tsx'; import PageHeader from '@/components/layout/PageHeader.tsx'; import MatchCard from '@/components/matches/MatchCard.tsx'; 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, ArrowRight, CheckCircle, Clock, DollarSign, FileText, MapPin, MessageSquare, TrendingUp, } from 'lucide-react'; import React, { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { formatCurrency } from '../lib/fin'; const MatchNegotiationPage = () => { const { id: matchId } = useParams<{ id: string }>(); const navigate = useNavigate(); 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 [showMessageModal, setShowMessageModal] = useState(false); const [messageText, setMessageText] = useState(''); // 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.new_value, })); }, [match?.History]); const getHistoryTitle = (action: string, value?: string) => { switch (action) { case 'status_changed': return t('matchNegotiation.statusChanged'); case 'comment': return t('matchNegotiation.commentAdded'); case 'update': return t('matchNegotiation.matchUpdated'); default: return value || action; } }; // Get allowed next statuses based on current status const allowedNextStatuses = useMemo(() => { if (!match) return []; const statusMap: Record = { suggested: ['negotiating', 'reserved', 'failed', 'cancelled'], negotiating: ['reserved', 'contracted', 'failed', 'cancelled'], reserved: ['contracted', 'negotiating', 'failed', 'cancelled'], contracted: ['live', 'failed', 'cancelled'], live: ['failed', 'cancelled'], failed: [], cancelled: [], }; return statusMap[match.Status] || []; }, [match]); const statusOptions = allowedNextStatuses.map((status) => ({ value: status, label: t(`matchStatus.${status}`, status), })); 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); } }; const handleSendMessage = async () => { // TODO: Implement message sending functionality // This would require a messaging API endpoint console.log('Sending message:', messageText); setShowMessageModal(false); setMessageText(''); }; // central fin.formatCurrency const formatScore = (score: number) => { return `${(score * 100).toFixed(1)}%`; }; const getStatusBadgeVariant = (status: string) => { switch (status) { case 'live': return 'default'; case 'contracted': return 'default'; case 'negotiating': return 'secondary'; case 'reserved': return 'outline'; case 'failed': return 'destructive'; case 'cancelled': return 'destructive'; default: return 'outline'; } }; const getNextActionText = (currentStatus: string) => { switch (currentStatus) { case 'suggested': return t('matchNegotiation.startNegotiation'); case 'negotiating': return t('matchNegotiation.proposeContract'); case 'reserved': return t('matchNegotiation.finalizeContract'); case 'contracted': return t('matchNegotiation.activateMatch'); default: return t('matchNegotiation.updateStatus'); } }; if (isLoading) { return (
); } if (error || !match) { return (

{error?.message || t('matchNegotiation.notFound')}

); } return ( {/* Current Status & Actions */}
{t('matchNegotiation.currentStatus')}
{t(`matchStatus.${match.Status}`, match.Status)}
{allowedNextStatuses.length > 0 && ( )}
{/* Match Overview */} {/* Key Negotiation 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('matchNegotiation.daysInNegotiation')} value={calculateNegotiationDays(match)} /> {/* Negotiation Progress */} {t('matchNegotiation.negotiationProgress')} {/* Economic Impact & Risk Assessment */} {/* Economic Impact */} {match.EconomicImpact && ( {t('matchNegotiation.potentialImpact')} {match.EconomicImpact.annual_savings && (
{t('matchDetail.annualSavings')} {formatCurrency(match.EconomicImpact.annual_savings)}
)} {match.EconomicImpact.co2_avoided_tonnes && (
{t('matchNegotiation.co2Avoided')} {match.EconomicImpact.co2_avoided_tonnes.toFixed(1)} t/year
)} {match.EconomicImpact.payback_years && (
{t('matchDetail.paybackPeriod')} {match.EconomicImpact.payback_years.toFixed(1)} years
)}
)} {/* Risk Assessment */} {match.RiskAssessment && ( {t('matchDetail.riskAssessment')}
{Object.entries(match.RiskAssessment).map(([key, value]) => { if (typeof value !== 'number') return null; return (
{key.replace('_', ' ')} Risk 0.7 ? 'destructive' : value > 0.4 ? 'secondary' : 'default' } > {formatScore(value)}
); })}
)}
{/* Negotiation History Timeline */} {t('matchNegotiation.negotiationHistory')}
{/* Status Update Modal */} {showStatusUpdate && (
{t('matchNegotiation.updateStatus')}