From f01632e38d817ec72ee9c92655c19f9d8452ad91 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 10 May 2026 08:44:03 +0330 Subject: [PATCH] refactor(tender-details): enhance translation and summary features - Integrated language selection and translation functionality in TenderDetails, allowing users to choose display language and translate tender metadata. - Added AI summary section to display overall summary of the tender. - Updated API service to support translation requests and handle responses effectively. - Improved pagination handling in AdminsTable for better user experience. - Refactored various components to ensure consistent data flow and state management. --- .../e2e/tenders/tender-translation-page.cy.ts | 145 ++++++++++++++++++ src/app/admins/edit/[id]/page.tsx | 4 +- src/app/tenders/[details]/page.tsx | 122 +++++++++++++-- src/components/Tables/admins/index.tsx | 25 ++- .../Tables/admins/useAdminsPresenter.ts | 5 +- .../Tables/tenders/TenderListFilters.tsx | 11 ++ src/components/Tables/tenders/index.tsx | 11 +- .../Tables/tenders/useTenderListPresenter.ts | 2 + .../forms/admins/useCreateAdminPresenter.ts | 4 +- src/constants/Api_Params.ts | 2 +- src/hooks/queries/useTenders.ts | 35 ++++- src/lib/api/endpoints.ts | 2 + src/lib/api/services/tenders-service.ts | 31 +++- src/lib/api/types/Tenders.ts | 12 ++ 14 files changed, 378 insertions(+), 33 deletions(-) create mode 100644 cypress/e2e/tenders/tender-translation-page.cy.ts diff --git a/cypress/e2e/tenders/tender-translation-page.cy.ts b/cypress/e2e/tenders/tender-translation-page.cy.ts new file mode 100644 index 0000000..ec6bcf5 --- /dev/null +++ b/cypress/e2e/tenders/tender-translation-page.cy.ts @@ -0,0 +1,145 @@ +/// + +const tenderId = "507f1f77bcf86cd799439055"; + +const makeTenderDetailsResponse = ( + overrides: Partial> = {}, +) => ({ + success: true, + message: "ok", + data: { + id: tenderId, + buyer_organization: { name: "ACME Procurement" }, + application_deadline: 1710001000, + country_code: "IR", + currency: "USD", + description: "Original description", + duration: "12", + duration_unit: "month", + estimated_value: 100000, + notice_publication_id: "NP-123", + procedure_code: "OPEN", + procurement_type_code: "GOODS", + publication_date: 1710000000, + status: "open", + submission_deadline: 1710002000, + submission_url: "https://example.com/submit", + tender_deadline: 1710003000, + created_at: 1710000000, + tender_id: "TDR-9001", + title: "Original title", + ...overrides, + }, +}); + +describe("tender details - translation and summary", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("renders details with summary fallback and translated metadata", () => { + cy.intercept("GET", `**/api/proxy/tenders/${tenderId}**`, (req) => { + expect(req.query.lang).to.eq("en"); + req.reply({ + statusCode: 200, + body: makeTenderDetailsResponse({ + title: "Translated title EN", + description: "Translated description EN", + language: "en", + overall_summary: undefined, + }), + }); + }).as("getTenderDetailsEn"); + + cy.visit(`/tenders/${tenderId}?lang=en`); + cy.wait("@getTenderDetailsEn"); + + cy.contains("h1", "Translated title EN").should("be.visible"); + cy.contains("Resolved language").should("be.visible"); + cy.contains("en").should("be.visible"); + cy.contains("AI summary").should("be.visible"); + cy.contains("Summary not ready yet").should("be.visible"); + cy.contains("Translated description EN").should("be.visible"); + }); + + it("switches language from selector and reloads details with new lang query", () => { + cy.intercept("GET", `**/api/proxy/tenders/${tenderId}**`, (req) => { + const lang = String(req.query.lang ?? "en"); + req.alias = lang === "fr" ? "getTenderDetailsFr" : "getTenderDetailsEn"; + req.reply({ + statusCode: 200, + body: makeTenderDetailsResponse({ + title: lang === "fr" ? "Titre FR" : "Title EN", + description: lang === "fr" ? "Description FR" : "Description EN", + language: lang, + overall_summary: lang === "fr" ? "Resume FR" : "Summary EN", + }), + }); + }); + + cy.visit(`/tenders/${tenderId}?lang=en`); + cy.wait("@getTenderDetailsEn"); + + cy.get('select[name="lang"]').select("fr"); + cy.location("search").should("include", "lang=fr"); + cy.wait("@getTenderDetailsFr"); + cy.contains("h1", "Titre FR").should("be.visible"); + cy.contains("Resume FR").should("be.visible"); + }); + + it("triggers translate endpoint with selected language", () => { + cy.intercept("GET", `**/api/proxy/tenders/${tenderId}**`, { + statusCode: 200, + body: makeTenderDetailsResponse({ + title: "Before translate", + description: "Before translate description", + language: "de", + }), + }).as("getTenderDetails"); + + cy.intercept( + "POST", + `**/api/proxy/tenders/${tenderId}/ai-translate`, + (req) => { + expect(req.body).to.deep.equal({ language: "de" }); + req.reply({ + statusCode: 200, + body: { + success: true, + message: "ok", + data: { + notice_id: "NP-123", + language: "de", + translated_title: "Ubersetzter Titel", + translated_description: "Ubersetzte Beschreibung", + }, + }, + }); + }, + ).as("translateTender"); + + cy.visit(`/tenders/${tenderId}?lang=de`); + cy.wait("@getTenderDetails"); + cy.contains("button", "Translate metadata").click(); + cy.wait("@translateTender"); + }); + + it("redirects legacy /translation URL to tender details with lang preserved", () => { + cy.intercept("GET", `**/api/proxy/tenders/${tenderId}**`, { + statusCode: 200, + body: makeTenderDetailsResponse({ + title: "Redirect check", + language: "fa", + }), + }).as("getTenderAfterRedirect"); + + cy.visit(`/tenders/${tenderId}/translation?lang=fa`); + cy.location("pathname", { timeout: 15000 }).should( + "eq", + `/tenders/${tenderId}`, + ); + cy.location("search").should("include", "lang=fa"); + cy.wait("@getTenderAfterRedirect"); + cy.contains("h1", "Redirect check").should("be.visible"); + }); +}); diff --git a/src/app/admins/edit/[id]/page.tsx b/src/app/admins/edit/[id]/page.tsx index 6864d6f..d967b2d 100644 --- a/src/app/admins/edit/[id]/page.tsx +++ b/src/app/admins/edit/[id]/page.tsx @@ -1,10 +1,10 @@ "use client"; -import Loading from "@/components/loading"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; import CreateAdminForm from "@/components/forms/admins/CreateAdmin"; +import Loading from "@/components/loading"; import { useAdminDetails } from "@/hooks/queries"; -import React, { use } from "react"; +import { use } from "react"; const AdminEditPage = ({ params }: { params: Promise<{ id: string }> }) => { const { id } = use(params); diff --git a/src/app/tenders/[details]/page.tsx b/src/app/tenders/[details]/page.tsx index 1715592..d50b759 100644 --- a/src/app/tenders/[details]/page.tsx +++ b/src/app/tenders/[details]/page.tsx @@ -1,10 +1,16 @@ "use client"; import { LocationIcon } from "@/assets/icons"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; +import { Select } from "@/components/FormElements/select"; import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import Loading from "@/components/loading"; import Status from "@/components/ui/Status"; -import { useTenderDetailQuery } from "@/hooks/queries"; +import { Button } from "@/components/ui-elements/button"; +import { Languages } from "@/constants/languages"; +import { + useTenderDetailQuery, + useTranslateTenderMutation, +} from "@/hooks/queries"; import { useGetFlagQuery } from "@/hooks/queries/useFlagsQueries"; import { cn } from "@/lib/utils"; import { @@ -14,6 +20,7 @@ import { } from "@/utils/shared"; import getSymbolFromCurrency from "currency-symbol-map"; import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { use } from "react"; interface IProps { @@ -64,7 +71,15 @@ const SHOWCASE_CENTERED_BODY = { const TenderDetails = ({ params }: IProps) => { const { details } = use(params); - const { data, isLoading, isError } = useTenderDetailQuery(details); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const selectedLanguage = searchParams.get("lang") || "en"; + const { mutate: translateTender, isPending: isTranslating } = + useTranslateTenderMutation(); + const { data, isLoading, isError, refetch } = useTenderDetailQuery(details, { + lang: selectedLanguage, + }); const countryCodeNormalized = data?.data.country_code?.trim().toUpperCase() ?? ""; const hasValidCountryCodeForDisplay = @@ -130,21 +145,71 @@ const TenderDetails = ({ params }: IProps) => { {tender.title} -
- - {tender.status} - - {submissionUrl && ( - - Open submission - - - )} +
+

+ Language & translation +

+
+ setValue("lang", undefined)} + />