Files
tessera-ctl/apps/web/src/app/(portal)/modules/tender-radar/page.tsx
T
schalli 653c63b072 feat(11-04): TenderDetail component with ?tender=<id> wiring
Adds getTender() to the api client and a self-fetching TenderDetail
overlay (pattern: SourceConfigForm) rendering all Tender fields plus a
safe (rel=noopener noreferrer, target=_blank) sourceUrl link. No local
mirroring of Vergabeunterlagen — rawPayload is 100% NULL in the live DB
(research finding), so only the source link exists; NULL value/deadline
render graceful German placeholders (D-05 applies to the detail view too).

page.tsx reads ?tender=<id> via useSearchParams and renders TenderDetail
as an overlay; closing removes the param. ResultsList row clicks set the
param (deviation: ResultsList.tsx was not listed in the plan's
files_modified but is required by the plan's own done-criteria/key_link
"ResultsList-Zeile setzt ?tender=<id>" — Rule 3 auto-fix, blocking).

3/3 TenderDetail tests pass; full web suite (117 tests) and tsc --noEmit
both clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:21:54 +02:00

76 lines
2.5 KiB
TypeScript

'use client';
import { Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { CoverageBanner } from './components/CoverageBanner';
import { FilterPanel } from './components/FilterPanel';
import { ResultsList } from './components/ResultsList';
import { TenderDetail } from './components/TenderDetail';
/**
* Ausschreibungs-Radar module page within the portal.
*
* Replaces the Phase 10 placeholder (CONFIG-01 MVP stub) with the real
* Trefferliste (Plan 11-01): CoverageBanner explains data coverage,
* FilterPanel writes filter/sort state into the URL, ResultsList reads it
* and renders the ~1671 existing DÖE tenders.
*
* Plan 11-04 (UI-02, D-07): the `?tender=<id>` URL param drives an
* in-component detail view (TenderDetail) rendered as an overlay on top
* of the list — deliberately NOT a separate Next.js sub-route, since this
* page is mounted via `dynamic(..., { ssr: false })` under
* `[category]/[moduleSlug]` (Anti-Pattern/Assumption A5). A row click in
* ResultsList sets the param; closing the detail removes it.
*
* `useSearchParams()` (used by FilterPanel/ResultsList/this component)
* requires a Suspense boundary per the Next.js App Router contract —
* wrapped here even though this component is loaded via
* `dynamic(..., { ssr: false })` (module-loader.ts), per the plan's
* explicit Next 16 guidance.
*
* Intentional hardcoded German strings: full i18n (next-intl message
* keys) is CONFIG-03, planned for Phase 14 (Research Open Question 3,
* RESOLVED).
*/
function TenderRadarContent() {
const router = useRouter();
const searchParams = useSearchParams();
const tenderId = searchParams.get('tender');
const closeDetail = () => {
const next = new URLSearchParams(searchParams.toString());
next.delete('tender');
router.replace(`?${next.toString()}`);
};
return (
<div className="mx-auto max-w-5xl space-y-6 p-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
Ausschreibungs-Radar
</h1>
</div>
<CoverageBanner />
<FilterPanel />
<ResultsList />
{tenderId && <TenderDetail tenderId={tenderId} onClose={closeDetail} />}
</div>
);
}
export default function TenderRadarPage() {
return (
<Suspense
fallback={
<div className="mx-auto max-w-5xl space-y-6 p-6">
<p className="text-sm text-muted-foreground">Lädt…</p>
</div>
}
>
<TenderRadarContent />
</Suspense>
);
}