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:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
|
||||
<Status status={tender.status ?? "Unknown"}>
|
||||
{tender.status}
|
||||
</Status>
|
||||
{submissionUrl && (
|
||||
<Link
|
||||
href={submissionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<span>Open submission</span>
|
||||
<ExternalLinkIcon className="opacity-90" />
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex w-full min-w-0 shrink-0 flex-col gap-3 lg:max-w-2xl lg:items-end">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-dark-5 dark:text-dark-6 lg:w-full lg:text-end">
|
||||
Language & translation
|
||||
</p>
|
||||
<div className="flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
|
||||
<Select
|
||||
name="lang"
|
||||
label="Display language"
|
||||
items={Languages}
|
||||
className={cn(
|
||||
"w-full shrink-0 space-y-0 sm:w-[min(100%,11.5rem)]",
|
||||
"[&_label]:sr-only",
|
||||
)}
|
||||
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>
|
||||
}
|
||||
@@ -332,6 +397,20 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
</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>
|
||||
<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">
|
||||
@@ -340,6 +419,15 @@ const TenderDetails = ({ params }: IProps) => {
|
||||
</p>
|
||||
</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>
|
||||
</ShowcaseSection>
|
||||
</>
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
|
||||
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import IsVisible from "@/components/ui/IsVisible";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import ListWrapper from "@/components/ui/ListWrapper";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import Status from "@/components/ui/Status";
|
||||
import {
|
||||
Table,
|
||||
@@ -48,6 +50,9 @@ const AdminsTable = () => {
|
||||
filterRegister,
|
||||
isMutating,
|
||||
watch,
|
||||
metadata,
|
||||
setParams,
|
||||
shouldShowPagination,
|
||||
} = useAdminsPresenter();
|
||||
|
||||
return (
|
||||
@@ -77,6 +82,7 @@ const AdminsTable = () => {
|
||||
list={allUsers}
|
||||
columns={columns}
|
||||
emptyMessage="No admins were found"
|
||||
isLoading={isPending}
|
||||
>
|
||||
{allUsers.map((item, index) => (
|
||||
<TableRow
|
||||
@@ -85,7 +91,11 @@ const AdminsTable = () => {
|
||||
data-cy={`admins-table-row-${item?.id ?? index}`}
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-number">
|
||||
{getPaginatedRowNumber({ index })}
|
||||
{getPaginatedRowNumber({
|
||||
index,
|
||||
page: metadata?.page,
|
||||
limit: metadata?.limit,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-full-name">
|
||||
{item?.full_name}
|
||||
@@ -200,6 +210,19 @@ const AdminsTable = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,9 @@ export const useAdminsPresenter = () => {
|
||||
setParams(newParams);
|
||||
filterFormReset(newParams);
|
||||
}, [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(() => {
|
||||
setIsModalOpen(false);
|
||||
});
|
||||
@@ -121,5 +123,6 @@ export const useAdminsPresenter = () => {
|
||||
handleFilterSubmit,
|
||||
search,
|
||||
watch,
|
||||
shouldShowPagination,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Select } from "@/components/FormElements/select";
|
||||
import { Button } from "@/components/ui-elements/button";
|
||||
import { Countries } from "@/constants/countries";
|
||||
import { TenderStatus } from "@/constants/enums";
|
||||
import { Languages } from "@/constants/languages";
|
||||
import { getEnumAsArray } from "@/utils/shared";
|
||||
import {
|
||||
FieldValues,
|
||||
@@ -63,6 +64,16 @@ const TenderListFilters = ({
|
||||
value={watch("country_code")}
|
||||
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">
|
||||
<Button
|
||||
|
||||
@@ -32,6 +32,7 @@ const TendersTable = () => {
|
||||
router,
|
||||
pathName,
|
||||
setParams,
|
||||
params,
|
||||
columns,
|
||||
isFilterModalOpen,
|
||||
setIsFilterModalOpen,
|
||||
@@ -119,7 +120,10 @@ const TendersTable = () => {
|
||||
data-tooltip-content="Details"
|
||||
data-tooltip-place="top"
|
||||
onClick={() => {
|
||||
router.push(`${pathName}/${item.id}`);
|
||||
const queryString = params?.lang
|
||||
? `?lang=${encodeURIComponent(params.lang)}`
|
||||
: "";
|
||||
router.push(`${pathName}/${item.id}${queryString}`);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -135,7 +139,10 @@ const TendersTable = () => {
|
||||
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"
|
||||
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" })} />
|
||||
|
||||
@@ -34,6 +34,7 @@ const useTenderListPresenter = () => {
|
||||
...apiDefaultParams,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
lang: "en",
|
||||
};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
newParams[key] = value;
|
||||
@@ -73,6 +74,7 @@ const useTenderListPresenter = () => {
|
||||
error,
|
||||
isPending,
|
||||
setParams,
|
||||
params,
|
||||
data,
|
||||
pathName,
|
||||
isModalOpen,
|
||||
|
||||
@@ -23,8 +23,8 @@ const useCreateAdminPresenter = ({
|
||||
id,
|
||||
}: IProps) => {
|
||||
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
||||
"password" | "text"
|
||||
>("password");
|
||||
"password" | "text"
|
||||
>("password");
|
||||
const isMutating = useIsMutating();
|
||||
const [selectedFileName, setSelectedFileName] = useState<string>("");
|
||||
const [selectedFileSize, setSelectedFileSize] = useState<number | null>(null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const apiDefaultParams = {
|
||||
sort_by: "created_at",
|
||||
sort_order: "asc",
|
||||
sort_order: "dsc",
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { toast } from "react-toastify";
|
||||
|
||||
export const useTendersQuery = (params?: Record<string, any>) => {
|
||||
const queryKey = useMemo(
|
||||
@@ -14,10 +15,36 @@ export const useTendersQuery = (params?: Record<string, any>) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useTenderDetailQuery = (id: string) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.TENDERS.DETAILS(id)], [id]);
|
||||
export const useTenderDetailQuery = (id: string, params?: Record<string, any>) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.TENDERS.DETAILS(id), params],
|
||||
[id, params],
|
||||
);
|
||||
return useQuery({
|
||||
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.",
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@ export const API_ENDPOINTS = {
|
||||
UPDATE: (id: string) => `tenders/${id}`,
|
||||
DELETE: (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: {
|
||||
READ_ALL: "customers",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import { ApiResponse } from "../types";
|
||||
import { TTenderDetails, TTenderResponse } from "../types/Tenders";
|
||||
import {
|
||||
TTenderDetails,
|
||||
TTenderResponse,
|
||||
TTenderTranslationResponse,
|
||||
} from "../types/Tenders";
|
||||
|
||||
export const tendersService = {
|
||||
tendersList: async (
|
||||
@@ -14,9 +18,12 @@ export const tendersService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
tenderDetails: async (id: string): Promise<ApiResponse<TTenderDetails>> => {
|
||||
tenderDetails: async (
|
||||
id: string,
|
||||
params?: Record<string, any>,
|
||||
): Promise<ApiResponse<TTenderDetails>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id))).data;
|
||||
return (await api.get(API_ENDPOINTS.TENDERS.DETAILS(id), { params })).data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR caught in Tender Service => tender details: ",
|
||||
@@ -25,4 +32,22 @@ export const tendersService = {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,9 +23,21 @@ export const TenderSchema = z.object({
|
||||
created_at: z.number().optional(),
|
||||
tender_id: 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({
|
||||
tenders: z.array(TenderSchema).or(z.null()),
|
||||
});
|
||||
export type TTenderResponse = z.infer<typeof TendersResponse>;
|
||||
export type TTenderDetails = z.infer<typeof TenderSchema>;
|
||||
export type TTenderTranslationResponse = z.infer<
|
||||
typeof TenderTranslationResponseSchema
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user