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.
This commit is contained in:
AmirReza Jamali
2026-05-10 08:44:03 +03:30
parent f48cb7e249
commit f01632e38d
14 changed files with 378 additions and 33 deletions
@@ -0,0 +1,145 @@
/// <reference types="cypress" />
const tenderId = "507f1f77bcf86cd799439055";
const makeTenderDetailsResponse = (
overrides: Partial<Record<string, any>> = {},
) => ({
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");
});
});
+2 -2
View File
@@ -1,10 +1,10 @@
"use client"; "use client";
import Loading from "@/components/loading";
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import CreateAdminForm from "@/components/forms/admins/CreateAdmin"; import CreateAdminForm from "@/components/forms/admins/CreateAdmin";
import Loading from "@/components/loading";
import { useAdminDetails } from "@/hooks/queries"; import { useAdminDetails } from "@/hooks/queries";
import React, { use } from "react"; import { use } from "react";
const AdminEditPage = ({ params }: { params: Promise<{ id: string }> }) => { const AdminEditPage = ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = use(params); const { id } = use(params);
+105 -17
View File
@@ -1,10 +1,16 @@
"use client"; "use client";
import { LocationIcon } from "@/assets/icons"; import { LocationIcon } from "@/assets/icons";
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import { Select } from "@/components/FormElements/select";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import Loading from "@/components/loading"; import Loading from "@/components/loading";
import Status from "@/components/ui/Status"; 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 { useGetFlagQuery } from "@/hooks/queries/useFlagsQueries";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
@@ -14,6 +20,7 @@ import {
} from "@/utils/shared"; } from "@/utils/shared";
import getSymbolFromCurrency from "currency-symbol-map"; import getSymbolFromCurrency from "currency-symbol-map";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { use } from "react"; import { use } from "react";
interface IProps { interface IProps {
@@ -64,7 +71,15 @@ const SHOWCASE_CENTERED_BODY = {
const TenderDetails = ({ params }: IProps) => { const TenderDetails = ({ params }: IProps) => {
const { details } = use(params); 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 = const countryCodeNormalized =
data?.data.country_code?.trim().toUpperCase() ?? ""; data?.data.country_code?.trim().toUpperCase() ?? "";
const hasValidCountryCodeForDisplay = const hasValidCountryCodeForDisplay =
@@ -130,21 +145,71 @@ const TenderDetails = ({ params }: IProps) => {
{tender.title} {tender.title}
</h1> </h1>
</div> </div>
<div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-end"> <div className="flex w-full min-w-0 shrink-0 flex-col gap-3 lg:max-w-2xl lg:items-end">
<Status status={tender.status ?? "Unknown"}> <p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-dark-5 dark:text-dark-6 lg:w-full lg:text-end">
{tender.status} Language & translation
</Status> </p>
{submissionUrl && ( <div className="flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
<Link <Select
href={submissionUrl} name="lang"
target="_blank" label="Display language"
rel="noopener noreferrer" items={Languages}
className="shadow-theme-xs inline-flex items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:bg-primary/90 hover:shadow-md" className={cn(
> "w-full shrink-0 space-y-0 sm:w-[min(100%,11.5rem)]",
<span>Open submission</span> "[&_label]:sr-only",
<ExternalLinkIcon className="opacity-90" /> )}
</Link> value={selectedLanguage}
)} onChange={async (event) => {
const nextParams = new URLSearchParams(
searchParams.toString(),
);
const nextLanguage = event.target.value;
if (nextLanguage) {
nextParams.set("lang", nextLanguage);
} else {
nextParams.delete("lang");
}
const queryString = nextParams.toString();
router.replace(
queryString ? `${pathname}?${queryString}` : pathname,
);
}}
/>
<Button
type="button"
size="small"
shape="rounded"
className="w-full shrink-0 whitespace-nowrap sm:w-auto sm:min-h-[3.375rem]"
label={isTranslating ? "Translating..." : "Translate metadata"}
disabled={isTranslating}
onClick={() => {
translateTender(
{ id: details, language: selectedLanguage },
{
onSuccess: async () => {
await refetch();
},
},
);
}}
/>
<div className="flex shrink-0 items-center sm:min-h-[3.375rem]">
<Status status={tender.status ?? "Unknown"}>
{tender.status}
</Status>
</div>
{submissionUrl && (
<Link
href={submissionUrl}
target="_blank"
rel="noopener noreferrer"
className="shadow-theme-xs inline-flex min-h-[3.375rem] w-full shrink-0 items-center justify-center gap-2 rounded-xl bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:bg-primary/90 hover:shadow-md sm:w-auto"
>
<span>Open submission</span>
<ExternalLinkIcon className="opacity-90" />
</Link>
)}
</div>
</div> </div>
</div> </div>
} }
@@ -332,6 +397,20 @@ const TenderDetails = ({ params }: IProps) => {
</div> </div>
)} )}
<div>
<SectionHeading>Translation</SectionHeading>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-stroke/60 bg-gray-2/40 p-6 shadow-inner dark:border-dark-3 dark:bg-dark-3/30">
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-dark-5 dark:text-dark-6">
Resolved language
</p>
<p className="text-sm text-dark dark:text-white">
{tender.language || "original"}
</p>
</div>
</div>
</div>
<div> <div>
<SectionHeading>Description</SectionHeading> <SectionHeading>Description</SectionHeading>
<div className="rounded-2xl border border-stroke/60 bg-gray-2/40 p-6 shadow-inner dark:border-dark-3 dark:bg-dark-3/30"> <div className="rounded-2xl border border-stroke/60 bg-gray-2/40 p-6 shadow-inner dark:border-dark-3 dark:bg-dark-3/30">
@@ -340,6 +419,15 @@ const TenderDetails = ({ params }: IProps) => {
</p> </p>
</div> </div>
</div> </div>
<div>
<SectionHeading>AI summary</SectionHeading>
<div className="rounded-2xl border border-stroke/60 bg-gray-2/40 p-6 shadow-inner dark:border-dark-3 dark:bg-dark-3/30">
<p className="whitespace-pre-wrap text-sm leading-relaxed text-dark dark:text-white/90">
{tender.overall_summary || "Summary not ready yet"}
</p>
</div>
</div>
</div> </div>
</ShowcaseSection> </ShowcaseSection>
</> </>
+24 -1
View File
@@ -3,9 +3,11 @@
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
import ListHeader from "@/components/ui/ListHeader"; import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper"; import ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import { import {
Table, Table,
@@ -48,6 +50,9 @@ const AdminsTable = () => {
filterRegister, filterRegister,
isMutating, isMutating,
watch, watch,
metadata,
setParams,
shouldShowPagination,
} = useAdminsPresenter(); } = useAdminsPresenter();
return ( return (
@@ -77,6 +82,7 @@ const AdminsTable = () => {
list={allUsers} list={allUsers}
columns={columns} columns={columns}
emptyMessage="No admins were found" emptyMessage="No admins were found"
isLoading={isPending}
> >
{allUsers.map((item, index) => ( {allUsers.map((item, index) => (
<TableRow <TableRow
@@ -85,7 +91,11 @@ const AdminsTable = () => {
data-cy={`admins-table-row-${item?.id ?? index}`} data-cy={`admins-table-row-${item?.id ?? index}`}
> >
<TableCell className="text-start" colSpan={100} data-cy="admins-row-number"> <TableCell className="text-start" colSpan={100} data-cy="admins-row-number">
{getPaginatedRowNumber({ index })} {getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell> </TableCell>
<TableCell className="text-start" colSpan={100} data-cy="admins-row-full-name"> <TableCell className="text-start" colSpan={100} data-cy="admins-row-full-name">
{item?.full_name} {item?.full_name}
@@ -200,6 +210,19 @@ const AdminsTable = () => {
onCancel={() => setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)}
/> />
)} )}
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={metadata?.page ? metadata.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
</IsVisible>
</ListWrapper> </ListWrapper>
); );
}; };
@@ -58,7 +58,9 @@ export const useAdminsPresenter = () => {
setParams(newParams); setParams(newParams);
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useGetAdminListQuery(params); const { data, isPending } = useGetAdminListQuery({ ...params, ...apiDefaultParams });
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const { mutate: deleteAdmin } = useDeleteAdmin(() => { const { mutate: deleteAdmin } = useDeleteAdmin(() => {
setIsModalOpen(false); setIsModalOpen(false);
}); });
@@ -121,5 +123,6 @@ export const useAdminsPresenter = () => {
handleFilterSubmit, handleFilterSubmit,
search, search,
watch, watch,
shouldShowPagination,
}; };
}; };
@@ -3,6 +3,7 @@ import { Select } from "@/components/FormElements/select";
import { Button } from "@/components/ui-elements/button"; import { Button } from "@/components/ui-elements/button";
import { Countries } from "@/constants/countries"; import { Countries } from "@/constants/countries";
import { TenderStatus } from "@/constants/enums"; import { TenderStatus } from "@/constants/enums";
import { Languages } from "@/constants/languages";
import { getEnumAsArray } from "@/utils/shared"; import { getEnumAsArray } from "@/utils/shared";
import { import {
FieldValues, FieldValues,
@@ -63,6 +64,16 @@ const TenderListFilters = ({
value={watch("country_code")} value={watch("country_code")}
onClear={() => setValue("country_code", undefined)} onClear={() => setValue("country_code", undefined)}
/> />
<Select
{...filterRegister("lang")}
items={Languages}
label="language"
name="lang"
placeholder="Language"
clearable
value={watch("lang")}
onClear={() => setValue("lang", undefined)}
/>
<div className="col-span-2 flex gap-4"> <div className="col-span-2 flex gap-4">
<Button <Button
+9 -2
View File
@@ -32,6 +32,7 @@ const TendersTable = () => {
router, router,
pathName, pathName,
setParams, setParams,
params,
columns, columns,
isFilterModalOpen, isFilterModalOpen,
setIsFilterModalOpen, setIsFilterModalOpen,
@@ -119,7 +120,10 @@ const TendersTable = () => {
data-tooltip-content="Details" data-tooltip-content="Details"
data-tooltip-place="top" data-tooltip-place="top"
onClick={() => { onClick={() => {
router.push(`${pathName}/${item.id}`); const queryString = params?.lang
? `?lang=${encodeURIComponent(params.lang)}`
: "";
router.push(`${pathName}/${item.id}${queryString}`);
}} }}
> >
<Tooltip <Tooltip
@@ -135,7 +139,10 @@ const TendersTable = () => {
data-tooltip-place="top" data-tooltip-place="top"
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5" className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
onClick={() => { onClick={() => {
router.push(`${pathName}/feedback/${item.id}`); const queryString = params?.lang
? `?lang=${encodeURIComponent(params.lang)}`
: "";
router.push(`${pathName}/feedback/${item.id}${queryString}`);
}} }}
> >
<Tooltip {..._TooltipDefaultParams({ id: "feedback" })} /> <Tooltip {..._TooltipDefaultParams({ id: "feedback" })} />
@@ -34,6 +34,7 @@ const useTenderListPresenter = () => {
...apiDefaultParams, ...apiDefaultParams,
offset: 0, offset: 0,
limit: 10, limit: 10,
lang: "en",
}; };
for (const [key, value] of searchParams.entries()) { for (const [key, value] of searchParams.entries()) {
newParams[key] = value; newParams[key] = value;
@@ -73,6 +74,7 @@ const useTenderListPresenter = () => {
error, error,
isPending, isPending,
setParams, setParams,
params,
data, data,
pathName, pathName,
isModalOpen, isModalOpen,
@@ -23,8 +23,8 @@ const useCreateAdminPresenter = ({
id, id,
}: IProps) => { }: IProps) => {
const [passwordFieldInputType, setPasswordFieldInputType] = useState< const [passwordFieldInputType, setPasswordFieldInputType] = useState<
"password" | "text" "password" | "text"
>("password"); >("password");
const isMutating = useIsMutating(); const isMutating = useIsMutating();
const [selectedFileName, setSelectedFileName] = useState<string>(""); const [selectedFileName, setSelectedFileName] = useState<string>("");
const [selectedFileSize, setSelectedFileSize] = useState<number | null>(null); const [selectedFileSize, setSelectedFileSize] = useState<number | null>(null);
+1 -1
View File
@@ -1,6 +1,6 @@
export const apiDefaultParams = { export const apiDefaultParams = {
sort_by: "created_at", sort_by: "created_at",
sort_order: "asc", sort_order: "dsc",
offset: 0, offset: 0,
limit: 10, limit: 10,
}; };
+31 -4
View File
@@ -1,6 +1,7 @@
import { API_ENDPOINTS, tendersService } from "@/lib/api"; import { API_ENDPOINTS, tendersService } from "@/lib/api";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo } from "react"; import { useMemo } from "react";
import { toast } from "react-toastify";
export const useTendersQuery = (params?: Record<string, any>) => { export const useTendersQuery = (params?: Record<string, any>) => {
const queryKey = useMemo( const queryKey = useMemo(
@@ -14,10 +15,36 @@ export const useTendersQuery = (params?: Record<string, any>) => {
}); });
}; };
export const useTenderDetailQuery = (id: string) => { export const useTenderDetailQuery = (id: string, params?: Record<string, any>) => {
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DETAILS(id)], [id]); const queryKey = useMemo(
() => [API_ENDPOINTS.TENDERS.DETAILS(id), params],
[id, params],
);
return useQuery({ return useQuery({
queryKey, queryKey,
queryFn: () => tendersService.tenderDetails(id), queryFn: () => tendersService.tenderDetails(id, params),
});
};
export const useTranslateTenderMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: [API_ENDPOINTS.TENDERS.AI_TRANSLATE("translate")],
mutationFn: tendersService.translateTender,
onSuccess: (_, variables) => {
toast.success(`Translation for "${variables.language}" is ready`);
queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.TENDERS.READ_ALL],
});
queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.TENDERS.DETAILS(variables.id)],
});
},
onError: () => {
toast.error(
"Translation temporarily unavailable. Showing original content.",
);
},
}); });
}; };
+2
View File
@@ -43,6 +43,8 @@ export const API_ENDPOINTS = {
UPDATE: (id: string) => `tenders/${id}`, UPDATE: (id: string) => `tenders/${id}`,
DELETE: (id: string) => `tenders/${id}`, DELETE: (id: string) => `tenders/${id}`,
DETAILS: (id: string) => `tenders/${id}`, DETAILS: (id: string) => `tenders/${id}`,
AI_TRANSLATE: (id: string) => `tenders/${id}/ai-translate`,
AI_SUMMARY: (id: string) => `tenders/${id}/ai-summary`,
}, },
CUSTOMERS: { CUSTOMERS: {
READ_ALL: "customers", READ_ALL: "customers",
+28 -3
View File
@@ -1,7 +1,11 @@
import api from "../axios"; import api from "../axios";
import { API_ENDPOINTS } from "../endpoints"; import { API_ENDPOINTS } from "../endpoints";
import { ApiResponse } from "../types"; import { ApiResponse } from "../types";
import { TTenderDetails, TTenderResponse } from "../types/Tenders"; import {
TTenderDetails,
TTenderResponse,
TTenderTranslationResponse,
} from "../types/Tenders";
export const tendersService = { export const tendersService = {
tendersList: async ( tendersList: async (
@@ -14,9 +18,12 @@ export const tendersService = {
throw error; throw error;
} }
}, },
tenderDetails: async (id: string): Promise<ApiResponse<TTenderDetails>> => { tenderDetails: async (
id: string,
params?: Record<string, any>,
): Promise<ApiResponse<TTenderDetails>> => {
try { try {
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id))).data; return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params })).data;
} catch (error) { } catch (error) {
console.error( console.error(
"ERROR caught in Tender Service => tender details: ", "ERROR caught in Tender Service => tender details: ",
@@ -25,4 +32,22 @@ export const tendersService = {
throw error; throw error;
} }
}, },
translateTender: async ({
id,
language,
}: {
id: string;
language: string;
}): Promise<ApiResponse<TTenderTranslationResponse>> => {
try {
return (
await api.post(API_ENDPOINTS.TENDERS.AI_TRANSLATE(id), {
language,
})
).data;
} catch (error) {
console.error("ERROR caught in Tender Service => translate tender: ", error);
throw error;
}
},
}; };
+12
View File
@@ -23,9 +23,21 @@ export const TenderSchema = z.object({
created_at: z.number().optional(), created_at: z.number().optional(),
tender_id: z.string(), tender_id: z.string(),
title: z.string(), title: z.string(),
language: z.string().optional(),
overall_summary: z.string().optional(),
});
export const TenderTranslationResponseSchema = z.object({
notice_id: z.string(),
language: z.string(),
translated_title: z.string(),
translated_description: z.string(),
}); });
export const TendersResponse = z.object({ export const TendersResponse = z.object({
tenders: z.array(TenderSchema).or(z.null()), tenders: z.array(TenderSchema).or(z.null()),
}); });
export type TTenderResponse = z.infer<typeof TendersResponse>; export type TTenderResponse = z.infer<typeof TendersResponse>;
export type TTenderDetails = z.infer<typeof TenderSchema>; export type TTenderDetails = z.infer<typeof TenderSchema>;
export type TTenderTranslationResponse = z.infer<
typeof TenderTranslationResponseSchema
>;