- StatusBadge/SimulationStatusBadge: swap bare i18n.t() helper for
useTranslation() hook so badges re-render reactively on i18n init
- EngagementDetailPage: formatDate() on start_date/end_date/created_at
(was: raw ISO string, formatDateTime with noisy 00:00:00)
- UsersAdminPage: formatDate() on created_at column (was: raw ISO string)
- Layout footer: t('nav.footer') with FR string in fr.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
4.5 KiB
TypeScript
110 lines
4.5 KiB
TypeScript
import { useParams, Link } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { extractApiError } from '@/api/client';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { useEngagement } from '@/hooks/useEngagements';
|
|
import { useHashTab } from '@/hooks/useHashTab';
|
|
import { useEngagementSimulations } from '@/hooks/useSimulations';
|
|
import { LoadingState } from '@/components/LoadingState';
|
|
import { ErrorState } from '@/components/ErrorState';
|
|
import { StatusBadge } from '@/components/StatusBadge';
|
|
import { SimulationList } from '@/components/SimulationList';
|
|
import { ExportEngagementButton } from '@/components/ExportEngagementButton';
|
|
import { BackLink } from '@/components/BackLink';
|
|
import { Tabs } from '@/components/Tabs';
|
|
import { formatDate } from '@/lib/format';
|
|
|
|
type TabId = 'description' | 'simulations';
|
|
|
|
export function EngagementDetailPage(): JSX.Element {
|
|
const { t } = useTranslation();
|
|
const { id } = useParams<{ id: string }>();
|
|
const numericId = id ? Number(id) : undefined;
|
|
const { canEditEngagements } = useAuth();
|
|
|
|
const detail = useEngagement(numericId);
|
|
const simsQuery = useEngagementSimulations(numericId);
|
|
|
|
const [activeTabRaw, setActiveTab] = useHashTab('description');
|
|
const activeTab = activeTabRaw as TabId;
|
|
|
|
if (detail.isLoading) return <LoadingState label={t('engagement.detail.loading')} />;
|
|
if (detail.isError) {
|
|
return (
|
|
<ErrorState
|
|
message={extractApiError(detail.error, t('engagement.detail.errorLoad'))}
|
|
onRetry={() => detail.refetch()}
|
|
/>
|
|
);
|
|
}
|
|
if (!detail.data) return <ErrorState message={t('engagement.detail.notFound')} />;
|
|
|
|
const eng = detail.data;
|
|
const simCount = simsQuery.data?.length;
|
|
|
|
const tabs = [
|
|
{ id: 'description', label: t('engagement.detail.tabs.description') },
|
|
{ id: 'simulations', label: t('engagement.detail.tabs.simulations'), count: simCount },
|
|
];
|
|
|
|
return (
|
|
<div className="flex flex-col gap-xl">
|
|
<header className="flex items-start justify-between gap-md">
|
|
<div className="flex flex-col gap-sm">
|
|
<BackLink to="/engagements">{t('engagement.detail.backTo')}</BackLink>
|
|
<h1 className="text-[32px] font-medium leading-none">{eng.name}</h1>
|
|
<div className="flex items-center gap-md">
|
|
<StatusBadge status={eng.status} />
|
|
<span className="text-[14px] text-graphite">
|
|
{t('engagement.detail.createdBy')}{' '}
|
|
<span className="text-ink">{eng.created_by.username}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{canEditEngagements ? (
|
|
<ExportEngagementButton engagementId={eng.id} />
|
|
) : null}
|
|
</header>
|
|
|
|
<Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} />
|
|
|
|
<div
|
|
role="tabpanel"
|
|
id={`tabpanel-${activeTab}`}
|
|
aria-labelledby={`tab-${activeTab}`}
|
|
>
|
|
{activeTab === 'description' && (
|
|
<div className="card-product flex flex-col gap-md">
|
|
<header className="flex items-start justify-between gap-md">
|
|
<dl className="grid grid-cols-2 gap-x-lg gap-y-xxs text-caption-md">
|
|
<dt className="text-graphite">{t('engagement.detail.schedule.startDate')}</dt>
|
|
<dd className="font-mono">{formatDate(eng.start_date)}</dd>
|
|
<dt className="text-graphite">{t('engagement.detail.schedule.endDate')}</dt>
|
|
<dd className="font-mono">{eng.end_date ? formatDate(eng.end_date) : '—'}</dd>
|
|
<dt className="text-graphite">{t('engagement.detail.schedule.status')}</dt>
|
|
<dd><StatusBadge status={eng.status} /></dd>
|
|
<dt className="text-graphite">{t('engagement.detail.schedule.createdAt')}</dt>
|
|
<dd className="font-mono">{formatDate(eng.created_at)}</dd>
|
|
</dl>
|
|
{canEditEngagements ? (
|
|
<Link to={`/engagements/${eng.id}/edit`} className="btn-outline shrink-0">
|
|
{t('engagement.detail.edit')}
|
|
</Link>
|
|
) : null}
|
|
</header>
|
|
<hr className="border-hairline" />
|
|
<h2 className="text-display-sm">{t('engagement.detail.tabs.description')}</h2>
|
|
<p className="text-[16px] text-charcoal whitespace-pre-line">
|
|
{eng.description?.trim() ? eng.description : t('engagement.detail.noDescription')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'simulations' && (
|
|
<SimulationList engagementId={eng.id} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|