diff --git a/src/app/tenders/[details]/components/tender-lots-section.tsx b/src/app/tenders/[details]/components/tender-lots-section.tsx
new file mode 100644
index 0000000..8ef15ae
--- /dev/null
+++ b/src/app/tenders/[details]/components/tender-lots-section.tsx
@@ -0,0 +1,112 @@
+import { ShowcaseSection } from "@/components/Layouts/showcase-section";
+import type { TTenderDetails, TTenderLot } from "@/lib/api/types/Tenders";
+import { cn } from "@/lib/utils";
+import { isNonEmptyTrimmed } from "../tender-display-utils";
+import {
+ HEADER_SCROLL_MARGIN,
+ SHOWCASE_CENTERED_BODY,
+ TENDER_SECTION_IDS,
+} from "../constants";
+import { SectionHeading } from "./section-heading";
+
+type TenderLotsSectionProps = {
+ tender: TTenderDetails;
+};
+
+function lotHasVisibleContent(lot: TTenderLot): boolean {
+ return (
+ isNonEmptyTrimmed(lot.lot_id) ||
+ isNonEmptyTrimmed(lot.title) ||
+ isNonEmptyTrimmed(lot.description) ||
+ isNonEmptyTrimmed(lot.main_nature_of_contract) ||
+ isNonEmptyTrimmed(lot.main_classification) ||
+ isNonEmptyTrimmed(lot.main_classification_display) ||
+ (typeof lot.estimated_value === "number" &&
+ Number.isFinite(lot.estimated_value) &&
+ lot.estimated_value > 0) ||
+ isNonEmptyTrimmed(lot.currency)
+ );
+}
+
+export function TenderLotsSection({ tender }: TenderLotsSectionProps) {
+ const lots = (tender.lots ?? []).filter(lotHasVisibleContent);
+ if (lots.length === 0) return null;
+
+ return (
+
+
Lots
+
+ {lots.map((lot, idx) => {
+ const key = lot.lot_id?.trim() || `lot-${idx}`;
+ const cpv =
+ lot.main_classification_display?.trim() ||
+ lot.main_classification?.trim();
+ const hasValue =
+ typeof lot.estimated_value === "number" &&
+ Number.isFinite(lot.estimated_value) &&
+ lot.estimated_value > 0;
+ const currency = lot.currency?.trim().toUpperCase() ?? "";
+
+ return (
+
+
+ {isNonEmptyTrimmed(lot.lot_id) ? lot.lot_id : `Lot ${idx + 1}`}
+
+
+ {isNonEmptyTrimmed(lot.title) ? (
+
+
+ {lot.title}
+
+
+ ) : null}
+ {isNonEmptyTrimmed(lot.description) ? (
+
+
+ {lot.description}
+
+
+ ) : null}
+ {isNonEmptyTrimmed(lot.main_nature_of_contract) ? (
+
+
+ {lot.main_nature_of_contract}
+
+
+ ) : null}
+ {cpv ? (
+
+ {cpv}
+
+ ) : null}
+ {hasValue ? (
+
+
+ {new Intl.NumberFormat().format(lot.estimated_value!)}
+ {currency ? (
+
+ {currency}
+
+ ) : null}
+
+
+ ) : null}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/tenders/[details]/components/tender-procurement-section.tsx b/src/app/tenders/[details]/components/tender-procurement-section.tsx
index b7f6eb4..25eb88b 100644
--- a/src/app/tenders/[details]/components/tender-procurement-section.tsx
+++ b/src/app/tenders/[details]/components/tender-procurement-section.tsx
@@ -1,12 +1,17 @@
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
+import type { TTenderAddress, TTenderDetails } from "@/lib/api/types/Tenders";
import { cn } from "@/lib/utils";
-import type { TTenderDetails } from "@/lib/api/types/Tenders";
import Link from "next/link";
import {
HEADER_SCROLL_MARGIN,
SHOWCASE_CENTERED_BODY,
TENDER_SECTION_IDS,
} from "../constants";
+import {
+ hasBuyerExtendedDetails,
+ isNonEmptyTrimmed,
+ isValidHttpUrl,
+} from "../tender-display-utils";
import { SectionHeading } from "./section-heading";
type TenderProcurementSectionProps = {
@@ -28,11 +33,38 @@ function uniqueTrimmedNoticeIds(ids: string[] | undefined): string[] {
return out;
}
+function formatAddressLines(addr: TTenderAddress | undefined): string[] {
+ if (!addr) return [];
+ const lines: string[] = [];
+ if (isNonEmptyTrimmed(addr.street_name)) lines.push(addr.street_name!.trim());
+ const cityLine = [addr.postal_zone, addr.city_name]
+ .map((s) => s?.trim())
+ .filter(Boolean)
+ .join(" ");
+ if (cityLine) lines.push(cityLine);
+ const regionCountry = [addr.country_subentity_code, addr.country_code]
+ .map((s) => s?.trim())
+ .filter(Boolean)
+ .join(" · ");
+ if (regionCountry) lines.push(regionCountry);
+ return lines;
+}
+
export function TenderProcurementSection({ tender }: TenderProcurementSectionProps) {
const relatedNoticeIds = uniqueTrimmedNoticeIds(tender.related_notice_publication_ids);
-
const noticeTypeCode = tender.notice_type_code?.trim();
- const mainClassification = tender.main_classification?.trim();
+ const mainCpv =
+ tender.main_classification_display?.trim() || tender.main_classification?.trim();
+ const noticePubId = tender.notice_publication_id?.trim();
+ const durationParts = [tender.duration?.trim(), tender.duration_unit?.trim()].filter(
+ Boolean,
+ );
+ const durationLabel = durationParts.join(" ");
+
+ const buyer = tender.buyer_organization;
+ const buyerAddressLines = formatAddressLines(buyer.address);
+ const showBuyerOrgName = isNonEmptyTrimmed(buyer.name);
+ const showBuyerExtended = hasBuyerExtendedDetails(tender);
return (
Procurement
-
-
- {tender.tender_id}
-
-
-
-
- {tender.buyer_organization.name}
-
-
-
-
-
- {tender.notice_publication_id}
-
-
-
-
-
- {tender.procedure_code}
-
-
-
-
- {tender.procurement_type_code}
-
-
+ {isNonEmptyTrimmed(tender.tender_id) ? (
+
+ {tender.tender_id}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.project_name) ? (
+
+
+ {tender.project_name}
+
+
+ ) : null}
+
+ {showBuyerOrgName ? (
+
+ {buyer.name}
+
+ ) : null}
+
+ {noticePubId ? (
+
+
+
+ {noticePubId}
+
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.procurement_project_id) ? (
+
+
+ {tender.procurement_project_id}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.contract_folder_id) ? (
+
+
+ {tender.contract_folder_id}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.contract_notice_id) ? (
+
+
+ {tender.contract_notice_id}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.notice_identifier) ? (
+
+
+ {tender.notice_identifier}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.notice_version) ? (
+
+
+ {tender.notice_version}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.gazette_id) ? (
+
+ {tender.gazette_id}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.form_type) ? (
+
+ {tender.form_type}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.notice_sub_type_code) ? (
+
+
+ {tender.notice_sub_type_code}
+
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.source) ? (
+
+ {tender.source}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.procedure_code) ? (
+
+ {tender.procedure_code}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.procurement_type_code) ? (
+
+
+ {tender.procurement_type_code}
+
+
+ ) : null}
+
{noticeTypeCode ? (
+ {noticeTypeCode}
+
+ ) : null}
+
+ {mainCpv ? (
+
+ {mainCpv}
+
+ ) : null}
+
+ {isNonEmptyTrimmed(tender.estimated_value_vat_basis) ? (
+
- {noticeTypeCode}
+ {tender.estimated_value_vat_basis}
) : null}
- {mainClassification ? (
-
-
- {mainClassification}
-
+
+ {durationLabel ? (
+
+ {durationLabel}
) : null}
+
{relatedNoticeIds.length > 0 ? (
) : null}
+
+ {showBuyerExtended ? (
+
+
+ Buyer contact & address
+
+
+ {isNonEmptyTrimmed(buyer.company_id) ? (
+
+ {buyer.company_id}
+
+ ) : null}
+ {isValidHttpUrl(buyer.website_uri) ? (
+
+
+ {buyer.website_uri!.trim()}
+
+
+ ) : null}
+ {isNonEmptyTrimmed(buyer.contact_name) ? (
+
+
+ {buyer.contact_name}
+
+
+ ) : null}
+ {isNonEmptyTrimmed(buyer.contact_telephone) ? (
+
+
+ {buyer.contact_telephone}
+
+
+ ) : null}
+ {isNonEmptyTrimmed(buyer.contact_email) ? (
+
+
+ {buyer.contact_email!.trim()}
+
+
+ ) : null}
+ {buyerAddressLines.length > 0 ? (
+
+
+ {buyerAddressLines.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+ ) : null}
+
+
+ ) : null}
);
}
diff --git a/src/app/tenders/[details]/components/tender-timeline-section.tsx b/src/app/tenders/[details]/components/tender-timeline-section.tsx
index 3e33640..0469393 100644
--- a/src/app/tenders/[details]/components/tender-timeline-section.tsx
+++ b/src/app/tenders/[details]/components/tender-timeline-section.tsx
@@ -1,12 +1,21 @@
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import type { TTenderDetails } from "@/lib/api/types/Tenders";
-import { truncateString, unixToDate } from "@/utils/shared";
+import { isoStringToDate, truncateString, unixToDate } from "@/utils/shared";
import Link from "next/link";
import {
HEADER_SCROLL_MARGIN,
SHOWCASE_CENTERED_BODY,
TENDER_SECTION_IDS,
} from "../constants";
+import {
+ hasAddressContent,
+ hasWinningTendererContent,
+ isNonEmptyTrimmed,
+ isValidHttpUrl,
+ isValidIsoDateTimeString,
+ isValidPositiveUnix,
+ shouldShowExternalLinksBlock,
+} from "../tender-display-utils";
import { ExternalLinkIcon } from "./external-link-icon";
import { SectionHeading } from "./section-heading";
@@ -14,58 +23,212 @@ type TenderTimelineSectionProps = {
tender: TTenderDetails;
};
+function formatWinnerAddress(tender: TTenderDetails): string[] {
+ const w = tender.winning_tenderer;
+ if (!w?.address || !hasAddressContent(w.address)) return [];
+ const a = w.address;
+ const lines: string[] = [];
+ if (isNonEmptyTrimmed(a.street_name)) lines.push(a.street_name!.trim());
+ const cityLine = [a.postal_zone, a.city_name]
+ .map((s) => s?.trim())
+ .filter(Boolean)
+ .join(" ");
+ if (cityLine) lines.push(cityLine);
+ const tail = [a.country_subentity_code, a.country_code]
+ .map((s) => s?.trim())
+ .filter(Boolean)
+ .join(" · ");
+ if (tail) lines.push(tail);
+ return lines;
+}
+
export function TenderTimelineSection({ tender }: TenderTimelineSectionProps) {
const submissionUrl = tender.submission_url?.trim();
+ const dispatchIso = tender.notice_dispatch_date_with_timezone?.trim();
+ const dispatchLabel = isValidIsoDateTimeString(dispatchIso)
+ ? isoStringToDate({ isoString: dispatchIso!, hasTime: true })
+ : isValidPositiveUnix(tender.notice_dispatch_at)
+ ? unixToDate({
+ unix: tender.notice_dispatch_at!,
+ hasTime: true,
+ })
+ : null;
+
+ const winnerName = tender.winning_tenderer?.name?.trim();
+ const winnerLines = formatWinnerAddress(tender);
+ const showWinnerBlock =
+ hasWinningTendererContent(tender) &&
+ (Boolean(winnerName) || winnerLines.length > 0);
+
+ const showSubmissionUrl = Boolean(submissionUrl) && isValidHttpUrl(submissionUrl);
+ const showExternal = shouldShowExternalLinksBlock(tender);
+
+ const deadlineCards = [
+ {
+ key: "application",
+ title: "Application deadline",
+ unix: tender.application_deadline,
+ },
+ {
+ key: "publication",
+ title: "Publication date",
+ unix: tender.publication_date,
+ },
+ {
+ key: "submission",
+ title: "Submission deadline",
+ unix: tender.submission_deadline,
+ },
+ {
+ key: "tender",
+ title: "Tender deadline",
+ unix: tender.tender_deadline,
+ },
+ ].filter((d) => isValidPositiveUnix(d.unix));
return (
Timeline
-
-
-
-
-
-
-
-
-
-
-
-
- {submissionUrl && (
+ {isValidPositiveUnix(tender.award_date) ? (
+
+
+
+ ) : null}
+
+ {showWinnerBlock ? (
+
+ {winnerName ? (
+ {winnerName}
+ ) : null}
+ {winnerLines.length > 0 ? (
+
+ {winnerLines.map((line, i) => (
+
+ {line}
+
+ ))}
+
+ ) : null}
+
+ ) : null}
+
+ {dispatchLabel ? (
+
+
+
+ ) : null}
+
+ {isValidPositiveUnix(tender.issue_date) ? (
+
+
+
+ ) : null}
+
+ {isValidPositiveUnix(tender.issue_time) ? (
+
+
+
+ ) : null}
+
+ {deadlineCards.map((d) => (
+
+
+
+ ))}
+
+ {showSubmissionUrl ? (
- {truncateString(submissionUrl, 42)}
+ {truncateString(submissionUrl!, 42)}
- )}
+ ) : null}
+
+ {showExternal ? (
+
+
+ {isValidHttpUrl(tender.document_url) ? (
+ -
+
+
+ Document (TED)
+
+
+ ) : null}
+ {isValidHttpUrl(tender.tender_url) ? (
+ -
+
+
+ {truncateString(tender.tender_url!.trim(), 56)}
+
+
+ ) : null}
+ {isValidHttpUrl(tender.buyer_profile_url) ? (
+ -
+
+
+ Buyer profile
+
+
+ ) : null}
+
+
+ ) : null}
);
diff --git a/src/app/tenders/[details]/components/tender-translation-section.tsx b/src/app/tenders/[details]/components/tender-translation-section.tsx
index ad9fd92..bb3b34f 100644
--- a/src/app/tenders/[details]/components/tender-translation-section.tsx
+++ b/src/app/tenders/[details]/components/tender-translation-section.tsx
@@ -1,5 +1,9 @@
import type { TTenderDetails } from "@/lib/api/types/Tenders";
import { HEADER_SCROLL_MARGIN, TENDER_SECTION_IDS } from "../constants";
+import {
+ isNonEmptyTrimmed,
+ shouldShowTranslationSection,
+} from "../tender-display-utils";
import { SectionHeading } from "./section-heading";
type TenderTranslationSectionProps = {
@@ -9,18 +13,33 @@ type TenderTranslationSectionProps = {
export function TenderTranslationSection({
tender,
}: TenderTranslationSectionProps) {
+ if (!shouldShowTranslationSection(tender)) return null;
+
+ const showNoticeLang = isNonEmptyTrimmed(tender.notice_language_code);
+ const showResolved = isNonEmptyTrimmed(tender.language);
+
return (
Translation
-
-
- Resolved language
-
-
- {tender.language || "original"}
-
-
+ {showNoticeLang ? (
+
+
+ Notice language
+
+
+ {tender.notice_language_code}
+
+
+ ) : null}
+ {showResolved ? (
+
+
+ Resolved language
+
+
{tender.language}
+
+ ) : null}
);
diff --git a/src/app/tenders/[details]/constants.ts b/src/app/tenders/[details]/constants.ts
index b82709f..83dc12e 100644
--- a/src/app/tenders/[details]/constants.ts
+++ b/src/app/tenders/[details]/constants.ts
@@ -7,6 +7,7 @@ export const TENDER_HEADER_ACTIONS_RESERVE_PX = 360;
export const TENDER_SECTION_IDS = {
procurement: "tender-section-procurement",
+ lots: "tender-section-lots",
timeline: "tender-section-timeline",
location: "tender-section-location",
translation: "tender-section-translation",
diff --git a/src/app/tenders/[details]/page.tsx b/src/app/tenders/[details]/page.tsx
index dfe4e7f..31e4fc5 100644
--- a/src/app/tenders/[details]/page.tsx
+++ b/src/app/tenders/[details]/page.tsx
@@ -8,6 +8,7 @@ import {
useTenderDetailQuery,
useTranslateTenderMutation,
} from "@/hooks/queries";
+import type { TTenderDetails } from "@/lib/api/types/Tenders";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { use, useCallback, useMemo } from "react";
import { TenderDetailsContent } from "./components/tender-details-content";
@@ -93,7 +94,7 @@ const TenderDetails = ({ params }: IProps) => {
return
;
}
- const tender = data!.data;
+ const tender: TTenderDetails = data!.data;
return (
<>
diff --git a/src/app/tenders/[details]/tender-display-utils.ts b/src/app/tenders/[details]/tender-display-utils.ts
new file mode 100644
index 0000000..c808834
--- /dev/null
+++ b/src/app/tenders/[details]/tender-display-utils.ts
@@ -0,0 +1,105 @@
+import type { TTenderAddress, TTenderDetails } from "@/lib/api/types/Tenders";
+
+export function isNonEmptyTrimmed(value: string | undefined | null): boolean {
+ return Boolean(value?.trim());
+}
+
+export function isValidPositiveUnix(value: number | undefined | null): boolean {
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
+}
+
+export function isValidHttpUrl(value: string | undefined | null): boolean {
+ const t = value?.trim();
+ if (!t) return false;
+ try {
+ const u = new URL(t);
+ return u.protocol === "http:" || u.protocol === "https:";
+ } catch {
+ return false;
+ }
+}
+
+export function isValidIsoDateTimeString(
+ value: string | undefined | null,
+): boolean {
+ const t = value?.trim();
+ if (!t) return false;
+ const ms = Date.parse(t);
+ return Number.isFinite(ms);
+}
+
+export function hasAddressContent(addr: TTenderAddress | undefined): boolean {
+ if (!addr) return false;
+ return (
+ isNonEmptyTrimmed(addr.street_name) ||
+ isNonEmptyTrimmed(addr.city_name) ||
+ isNonEmptyTrimmed(addr.postal_zone) ||
+ isNonEmptyTrimmed(addr.country_subentity_code) ||
+ isNonEmptyTrimmed(addr.country_code)
+ );
+}
+
+export function hasWinningTendererContent(tender: TTenderDetails): boolean {
+ const w = tender.winning_tenderer;
+ if (!w) return false;
+ if (isNonEmptyTrimmed(w.name)) return true;
+ return hasAddressContent(w.address);
+}
+
+export function shouldShowLotsSection(tender: TTenderDetails): boolean {
+ const lots = tender.lots ?? [];
+ return lots.some(
+ (lot) =>
+ isNonEmptyTrimmed(lot.lot_id) ||
+ isNonEmptyTrimmed(lot.title) ||
+ isNonEmptyTrimmed(lot.description) ||
+ isNonEmptyTrimmed(lot.main_nature_of_contract) ||
+ isNonEmptyTrimmed(lot.main_classification) ||
+ isNonEmptyTrimmed(lot.main_classification_display) ||
+ (typeof lot.estimated_value === "number" &&
+ Number.isFinite(lot.estimated_value) &&
+ lot.estimated_value > 0) ||
+ isNonEmptyTrimmed(lot.currency),
+ );
+}
+
+export function hasBuyerExtendedDetails(tender: TTenderDetails): boolean {
+ const b = tender.buyer_organization;
+ return (
+ isNonEmptyTrimmed(b.company_id) ||
+ isValidHttpUrl(b.website_uri) ||
+ isNonEmptyTrimmed(b.contact_name) ||
+ isNonEmptyTrimmed(b.contact_telephone) ||
+ isNonEmptyTrimmed(b.contact_email) ||
+ hasAddressContent(b.address)
+ );
+}
+
+export function shouldShowDescriptionSection(tender: TTenderDetails): boolean {
+ return isNonEmptyTrimmed(tender.description);
+}
+
+export function shouldShowTranslationSection(tender: TTenderDetails): boolean {
+ return (
+ isNonEmptyTrimmed(tender.language) ||
+ isNonEmptyTrimmed(tender.notice_language_code)
+ );
+}
+
+export function shouldShowAiSummarySection(tender: TTenderDetails): boolean {
+ return isNonEmptyTrimmed(tender.overall_summary);
+}
+
+export function shouldShowExternalLinksBlock(tender: TTenderDetails): boolean {
+ return (
+ isValidHttpUrl(tender.document_url) ||
+ isValidHttpUrl(tender.tender_url) ||
+ isValidHttpUrl(tender.buyer_profile_url)
+ );
+}
+
+export function isValidRegionCode(code: string | undefined | null): boolean {
+ const t = code?.trim() ?? "";
+ if (t.length < 2) return false;
+ return /^[A-Z0-9]{2,10}$/i.test(t);
+}
diff --git a/src/app/tenders/[details]/tender-nav-utils.ts b/src/app/tenders/[details]/tender-nav-utils.ts
index 97a92ad..85d623d 100644
--- a/src/app/tenders/[details]/tender-nav-utils.ts
+++ b/src/app/tenders/[details]/tender-nav-utils.ts
@@ -1,6 +1,13 @@
import type { TTenderDetails } from "@/lib/api/types/Tenders";
import { TENDER_SECTION_IDS } from "./constants";
import type { TenderNavItem } from "./types";
+import {
+ isValidRegionCode,
+ shouldShowAiSummarySection,
+ shouldShowDescriptionSection,
+ shouldShowLotsSection,
+ shouldShowTranslationSection,
+} from "./tender-display-utils";
export const isValidDisplayCountryCode = (code: string) =>
/^[A-Za-z]{2,3}$/.test(code);
@@ -14,7 +21,8 @@ function analyzeLocationVisibility(tender: TTenderDetails) {
typeof tender.estimated_value === "number" &&
Number.isFinite(tender.estimated_value) &&
tender.estimated_value > 0;
- const showLocation = hasCountry || hasCurrency || hasEst;
+ const hasRegion = isValidRegionCode(tender.region_code);
+ const showLocation = hasCountry || hasCurrency || hasEst || hasRegion;
return { showLocation };
}
@@ -27,22 +35,29 @@ export function getTenderNavItems(
const items: TenderNavItem[] = [
{ id: TENDER_SECTION_IDS.procurement, label: "Procurement" },
- { id: TENDER_SECTION_IDS.timeline, label: "Timeline" },
];
+ if (shouldShowLotsSection(tender)) {
+ items.push({ id: TENDER_SECTION_IDS.lots, label: "Lots" });
+ }
+ items.push({ id: TENDER_SECTION_IDS.timeline, label: "Timeline" });
if (showLocation) {
items.push({
id: TENDER_SECTION_IDS.location,
label: "Location & currency",
});
}
- items.push({ id: TENDER_SECTION_IDS.translation, label: "Translation" });
+ if (shouldShowTranslationSection(tender)) {
+ items.push({ id: TENDER_SECTION_IDS.translation, label: "Translation" });
+ }
if (showDocuments) {
items.push({ id: TENDER_SECTION_IDS.documents, label: "Documents" });
}
- items.push(
- { id: TENDER_SECTION_IDS.description, label: "Description" },
- { id: TENDER_SECTION_IDS.aiSummary, label: "AI summary" },
- );
+ if (shouldShowDescriptionSection(tender)) {
+ items.push({ id: TENDER_SECTION_IDS.description, label: "Description" });
+ }
+ if (shouldShowAiSummarySection(tender)) {
+ items.push({ id: TENDER_SECTION_IDS.aiSummary, label: "AI summary" });
+ }
return items;
}
diff --git a/src/lib/api/types/Tenders.ts b/src/lib/api/types/Tenders.ts
index f7742d2..5203058 100644
--- a/src/lib/api/types/Tenders.ts
+++ b/src/lib/api/types/Tenders.ts
@@ -1,37 +1,111 @@
import { z } from "zod";
-export const TenderSchema = z.object({
- id: z.string(),
- buyer_organization: z.object({
- name: z.string(),
- }),
- application_deadline: z.number(),
- country_code: z.string(),
- currency: z.string(),
- description: z.string(),
- duration: z.string(),
- duration_unit: z.string(),
- estimated_value: z.number(),
- notice_publication_id: z.string(),
- /** Related TED notice publication IDs (e.g. modifications, same procedure). */
- related_notice_publication_ids: z.array(z.string()).optional(),
- /** TED form / notice type code (e.g. can-modif). */
- notice_type_code: z.string().optional(),
- /** Primary CPV classification code. */
- main_classification: z.string().optional(),
- procedure_code: z.string(),
- procurement_type_code: z.string(),
- publication_date: z.number(),
- status: z.string(),
- submission_deadline: z.number(),
- submission_url: z.string(),
- tender_deadline: z.number(),
- created_at: z.number().optional(),
- tender_id: z.string(),
- title: z.string(),
- language: z.string().optional(),
- overall_summary: z.string().optional(),
-});
+/** TED / eForms notice address block */
+export const TenderAddressSchema = z
+ .object({
+ street_name: z.string().optional(),
+ city_name: z.string().optional(),
+ postal_zone: z.string().optional(),
+ country_subentity_code: z.string().optional(),
+ country_code: z.string().optional(),
+ })
+ .passthrough();
+
+export type TTenderAddress = z.infer
;
+
+export const BuyerOrganizationSchema = z
+ .object({
+ name: z.string().default(""),
+ company_id: z.string().optional(),
+ website_uri: z.string().optional(),
+ contact_name: z.string().optional(),
+ contact_telephone: z.string().optional(),
+ contact_email: z.string().optional(),
+ address: TenderAddressSchema.optional(),
+ })
+ .passthrough();
+
+export type TBuyerOrganization = z.infer;
+
+export const WinningTendererSchema = z
+ .object({
+ name: z.string().optional(),
+ address: TenderAddressSchema.optional(),
+ })
+ .passthrough();
+
+export type TWinningTenderer = z.infer;
+
+export const TenderLotSchema = z
+ .object({
+ lot_id: z.string().optional(),
+ title: z.string().optional(),
+ description: z.string().optional(),
+ main_nature_of_contract: z.string().optional(),
+ main_classification: z.string().optional(),
+ main_classification_display: z.string().optional(),
+ estimated_value: z.number().optional(),
+ currency: z.string().optional(),
+ })
+ .passthrough();
+
+export type TTenderLot = z.infer;
+
+export const TenderSchema = z
+ .object({
+ id: z.string(),
+ notice_publication_id: z.string(),
+ /** Related TED notice publication IDs (e.g. modifications, same procedure). */
+ related_notice_publication_ids: z.array(z.string()).optional(),
+ contract_folder_id: z.string().optional(),
+ procurement_project_id: z.string().optional(),
+ contract_notice_id: z.string().optional(),
+ notice_type_code: z.string().optional(),
+ form_type: z.string().optional(),
+ notice_sub_type_code: z.string().optional(),
+ notice_identifier: z.string().optional(),
+ notice_version: z.string().optional(),
+ notice_language_code: z.string().optional(),
+ gazette_id: z.string().optional(),
+ notice_dispatch_at: z.number().optional(),
+ notice_dispatch_date_with_timezone: z.string().optional(),
+ issue_date: z.number().optional(),
+ issue_time: z.number().optional(),
+ title: z.string(),
+ description: z.string(),
+ procurement_type_code: z.string(),
+ procedure_code: z.string(),
+ main_classification: z.string().optional(),
+ main_classification_display: z.string().optional(),
+ estimated_value: z.number(),
+ currency: z.string(),
+ estimated_value_vat_basis: z.string().optional(),
+ duration: z.string().optional(),
+ duration_unit: z.string().optional(),
+ publication_date: z.number(),
+ tender_deadline: z.number(),
+ submission_deadline: z.number(),
+ application_deadline: z.number(),
+ submission_url: z.string(),
+ country_code: z.string(),
+ region_code: z.string().optional(),
+ document_url: z.string().optional(),
+ tender_url: z.string().optional(),
+ buyer_profile_url: z.string().optional(),
+ buyer_organization: BuyerOrganizationSchema,
+ winning_tenderer: WinningTendererSchema.optional(),
+ lots: z.array(TenderLotSchema).optional(),
+ award_date: z.number().optional(),
+ status: z.string(),
+ source: z.string().optional(),
+ tender_id: z.string(),
+ project_name: z.string().optional(),
+ created_at: z.number().optional(),
+ /** Resolved / UI language (may come from translation pipeline). */
+ language: z.string().optional(),
+ overall_summary: z.string().optional(),
+ })
+ .passthrough();
export const TenderDocumentsResponseSchema = z.object({
filename: z.string(),