turash/bugulma/frontend/pages/MatchDetailPage.tsx
Damir Mukimov 18cdcb12fd
fix: continue linting fixes - remove unused variables, fix i18n strings
- Remove unused imports and variables from DashboardPage, HeritageBuildingPage, MatchesMapView
- Fix i18n literal strings in NetworkGraph component
- Continue systematic reduction of linting errors
2025-12-25 00:32:40 +01:00

394 lines
15 KiB
TypeScript

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.new_value,
}));
}, [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 (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<div className="animate-pulse">
<div className="h-8 bg-muted rounded w-1/3 mb-8"></div>
<div className="h-64 bg-muted rounded mb-8"></div>
<div className="h-48 bg-muted rounded"></div>
</div>
</Container>
</MainLayout>
);
}
if (error || !match) {
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('matchDetail.errorTitle')}
subtitle={t('matchDetail.errorSubtitle')}
onBack={handleBackNavigation}
/>
<Card>
<CardContent className="py-12">
<div className="text-center">
<p className="text-destructive mb-4">
{error?.message || t('matchDetail.notFound')}
</p>
<Button onClick={handleBackNavigation}>
<ArrowLeft className="h-4 mr-2 text-current w-4" />
{t('common.back')}
</Button>
</div>
</CardContent>
</Card>
</Container>
</MainLayout>
);
}
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 (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('matchDetail.title')}
subtitle={`${t('matchDetail.matchId')} ${match.ID}`}
onBack={handleBackNavigation}
/>
<Stack spacing="2xl">
{/* Match Overview Card */}
<MatchCard match={match} />
{/* Key Metrics */}
<Grid cols={{ md: 2, lg: 4 }} gap="md">
<MetricItem
icon={<TrendingUp className="h-4 h-5 text-current text-success w-4 w-5" />}
label={t('matchDetail.compatibilityScore')}
value={formatScore(match.CompatibilityScore)}
/>
<MetricItem
icon={<DollarSign className="h-4 h-5 text-current text-primary w-4 w-5" />}
label={t('matchDetail.economicValue')}
value={formatCurrency(match.EconomicValue)}
/>
<MetricItem
icon={<MapPin className="h-4 h-5 text-current text-warning w-4 w-5" />}
label={t('matchDetail.distance')}
value={`${match.DistanceKm.toFixed(1)} km`}
/>
<MetricItem
icon={<CheckCircle className="h-4 h-5 text-current text-muted-foreground w-4 w-5" />}
label={t('matchDetail.priority')}
value={match.Priority.toString()}
/>
</Grid>
{/* Economic Impact & Risk Assessment */}
<Grid cols={{ md: 2 }} gap="lg">
{/* Economic Impact */}
{match.EconomicImpact && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-4 h-5 text-current w-4 w-5" />
{t('matchDetail.economicImpact')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="sm">
{match.EconomicImpact.annual_savings && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">
{t('matchDetail.annualSavings')}
</span>
<span className="text-sm font-semibold text-success">
{formatCurrency(match.EconomicImpact.annual_savings)}
</span>
</div>
)}
{match.EconomicImpact.npv && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">{t('matchDetail.npv')}</span>
<span className="text-sm font-semibold">
{formatCurrency(match.EconomicImpact.npv)}
</span>
</div>
)}
{match.EconomicImpact.irr && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">{t('matchDetail.irr')}</span>
<span className="text-sm font-semibold">
{formatScore(match.EconomicImpact.irr)}
</span>
</div>
)}
{match.EconomicImpact.payback_years && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">
{t('matchDetail.paybackPeriod')}
</span>
<span className="text-sm font-semibold">
{t('matchDetail.paybackYears', {
years: match.EconomicImpact.payback_years.toFixed(1),
})}
</span>
</div>
)}
</Stack>
</CardContent>
</Card>
)}
{/* Risk Assessment */}
{match.RiskAssessment && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-4 h-5 text-current w-4 w-5" />
{t('matchDetail.riskAssessment')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="sm">
{match.RiskAssessment.technical_risk !== undefined && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">
{t('matchDetail.technicalRisk')}
</span>
<Badge
variant={
match.RiskAssessment.technical_risk > 0.7
? 'destructive'
: match.RiskAssessment.technical_risk > 0.4
? 'secondary'
: 'default'
}
>
{formatScore(match.RiskAssessment.technical_risk)}
</Badge>
</div>
)}
{match.RiskAssessment.regulatory_risk !== undefined && (
<div className="flex justify-between items-center py-2 border-b">
<span className="text-sm font-medium">
{t('matchDetail.regulatoryRisk')}
</span>
<Badge
variant={
match.RiskAssessment.regulatory_risk > 0.7
? 'destructive'
: match.RiskAssessment.regulatory_risk > 0.4
? 'secondary'
: 'default'
}
>
{formatScore(match.RiskAssessment.regulatory_risk)}
</Badge>
</div>
)}
{match.RiskAssessment.market_risk !== undefined && (
<div className="flex justify-between items-center py-2">
<span className="text-sm font-medium">{t('matchDetail.marketRisk')}</span>
<Badge
variant={
match.RiskAssessment.market_risk > 0.7
? 'destructive'
: match.RiskAssessment.market_risk > 0.4
? 'secondary'
: 'default'
}
>
{formatScore(match.RiskAssessment.market_risk)}
</Badge>
</div>
)}
</Stack>
</CardContent>
</Card>
)}
</Grid>
{/* Status Update Section */}
<Card>
<CardHeader>
<Flex align="center" justify="between">
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-4 h-5 text-current w-4 w-5" />
{t('matchDetail.updateStatus')}
</CardTitle>
<Button
variant="outline"
size="sm"
onClick={() => setShowStatusUpdate(!showStatusUpdate)}
>
{showStatusUpdate ? t('common.cancel') : t('matchDetail.changeStatus')}
</Button>
</Flex>
</CardHeader>
{showStatusUpdate && (
<CardContent>
<Stack spacing="md">
<FormField label={t('matchDetail.newStatus')}>
<Select
value={newStatus}
onValueChange={setNewStatus}
options={statusOptions}
placeholder={t('matchDetail.selectStatus')}
/>
</FormField>
<FormField label={`${t('matchDetail.notes')} (${t('common.optional')})`}>
<Textarea
value={statusNotes}
onChange={(e) => setStatusNotes(e.target.value)}
placeholder={t('matchDetail.notesPlaceholder')}
rows={3}
/>
</FormField>
<Flex gap="sm">
<Button
onClick={handleStatusUpdate}
disabled={!newStatus || updateStatusMutation.isPending}
>
{updateStatusMutation.isPending
? t('common.updating')
: t('matchDetail.updateStatus')}
</Button>
<Button
variant="outline"
onClick={() => {
setShowStatusUpdate(false);
setNewStatus('');
setStatusNotes('');
}}
>
{t('common.cancel')}
</Button>
</Flex>
</Stack>
</CardContent>
)}
</Card>
{/* Match History Timeline */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-4 h-5 text-current w-4 w-5" />
{t('matchDetail.matchHistory')}
</CardTitle>
</CardHeader>
<CardContent>
<Timeline entries={timelineEntries} />
</CardContent>
</Card>
</Stack>
</Container>
</MainLayout>
);
};
export default MatchDetailPage;