From f48cb7e2495a6a9d569a69e15d69b9690d931d26 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 5 May 2026 15:20:55 +0330 Subject: [PATCH] chore: update app version and enhance UI components - Bumped NEXT_PUBLIC_APP_VERSION to 2.0.3 for the latest release. - Improved Cypress test for customer feedback page to handle delayed route transitions. - Refactored CompanyDetailsPage to enhance layout and added new components for better structure. - Updated TenderDetails to conditionally render location and currency information based on validity checks. - Added data-cy attributes to various form elements and tables for improved testing capabilities. --- .env.production | 2 +- .../companies/company-categories-forms.cy.ts | 149 +++++++++++ .../companies/company-categories-list.cy.ts | 156 ++++++++++++ .../e2e/customers/customers-management.cy.ts | 6 +- cypress/support/company-category-fixtures.ts | 45 ++++ cypress/support/company-category-helpers.ts | 14 ++ .../support/company-category-intercepts.ts | 53 ++++ .../companies/details/[id]/page.tsx | 236 +++++++++++++----- src/app/tenders/[details]/page.tsx | 119 +++++---- .../FormElements/InputGroup/text-area.tsx | 3 + src/components/FormElements/switch.tsx | 3 + .../CompanyCategoryListFilters.tsx | 5 + .../companies/company-categories/index.tsx | 18 +- src/components/Tables/tenders/index.tsx | 6 + .../Tables/tenders/useTenderListPresenter.ts | 1 + .../CreateCompanyCategory.tsx | 10 +- 16 files changed, 713 insertions(+), 113 deletions(-) create mode 100644 cypress/e2e/companies/company-categories-forms.cy.ts create mode 100644 cypress/e2e/companies/company-categories-list.cy.ts create mode 100644 cypress/support/company-category-fixtures.ts create mode 100644 cypress/support/company-category-helpers.ts create mode 100644 cypress/support/company-category-intercepts.ts diff --git a/.env.production b/.env.production index 614dc75..7ba8c83 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -NEXT_PUBLIC_APP_VERSION=2.0.2 +NEXT_PUBLIC_APP_VERSION=2.0.3 NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500 NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0 NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com diff --git a/cypress/e2e/companies/company-categories-forms.cy.ts b/cypress/e2e/companies/company-categories-forms.cy.ts new file mode 100644 index 0000000..7db6351 --- /dev/null +++ b/cypress/e2e/companies/company-categories-forms.cy.ts @@ -0,0 +1,149 @@ +/// +import { makeCompanyCategory } from "../../support/company-category-fixtures"; +import { + categoryMutationSuccessBody, + companyCategoriesListUrl, + companyCategoryCreateUrl, + companyCategoryEditUrl, +} from "../../support/company-category-helpers"; +import { stubCompanyCategoriesReads } from "../../support/company-category-intercepts"; + +describe("company categories create and edit forms", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("create: shows required error when name is empty", () => { + stubCompanyCategoriesReads(() => []); + cy.visit(companyCategoryCreateUrl()); + cy.getByCy("company-category-form").should("be.visible"); + cy.contains("h2", "Create Company Category").should("be.visible"); + cy.getByCy("admin-form-submit-button").click(); + cy.contains("This field is required").should("be.visible"); + }); + + it("create: shows min length for one-character name", () => { + stubCompanyCategoriesReads(() => []); + cy.visit(companyCategoryCreateUrl()); + cy.getByCy("company-category-form-name-input").type("A"); + cy.getByCy("admin-form-submit-button").click(); + cy.contains("This field must be at least 2 characters long").should("be.visible"); + }); + + it("create: shows min length when description is too short", () => { + stubCompanyCategoriesReads(() => []); + cy.visit(companyCategoryCreateUrl()); + cy.getByCy("company-category-form-name-input").type("Valid Name"); + cy.getByCy("company-category-form-description-input").type("short"); + cy.getByCy("admin-form-submit-button").click(); + cy.contains("This field must be at least 10 characters long").should("be.visible"); + }); + + it("create: submits and redirects to list with success toast", () => { + let categories: ReturnType[] = []; + stubCompanyCategoriesReads(() => categories); + + cy.intercept("POST", "**/api/proxy/company-categories", (req) => { + categories = [ + makeCompanyCategory( + { + id: "507f1f77bcf86cd799439099", + name: "New Category", + description: "A description with enough characters.", + }, + 99, + ), + ]; + req.reply({ + statusCode: 201, + body: categoryMutationSuccessBody("Category created"), + }); + }).as("createCategory"); + + cy.visit(companyCategoryCreateUrl()); + cy.getByCy("company-category-form-name-input").type("New Category"); + cy.getByCy("company-category-form-description-input").type( + "A description with enough characters.", + ); + cy.getByCy("admin-form-submit-button").click(); + cy.wait("@createCategory"); + // Next.js navigation can be slightly delayed in CI/headless runs. + cy.location("pathname", { timeout: 15000 }).should("eq", "/company-categories"); + cy.contains(/success|created|Category created/i).should("be.visible"); + cy.contains("New Category").should("be.visible"); + }); + + it("create: disables submit while request is in flight", () => { + stubCompanyCategoriesReads(() => []); + cy.intercept("POST", "**/api/proxy/company-categories", (req) => { + req.reply({ + delay: 1400, + statusCode: 201, + body: categoryMutationSuccessBody("Category created"), + }); + }).as("slowCreate"); + + cy.visit(companyCategoryCreateUrl()); + cy.getByCy("company-category-form-name-input").type("Slow Category"); + cy.getByCy("company-category-form-description-input").type( + "Description long enough for validation rules here.", + ); + cy.getByCy("admin-form-submit-button").click(); + cy.getByCy("admin-form-submit-button").should("be.disabled"); + cy.getByCy("admin-form-submit-button").find("div.animate-spin").should("exist"); + cy.wait("@slowCreate"); + cy.get("@slowCreate.all").should("have.length", 1); + }); + + it("edit: prefills fields and updates then returns to list", () => { + const cat = makeCompanyCategory({ name: "Before Name", id: "507f1f77bcf86cd799439055" }, 5); + let categories = [cat]; + + stubCompanyCategoriesReads(() => categories); + cy.intercept("PUT", `**/api/proxy/company-categories/${cat.id}`, (req) => { + categories = [ + makeCompanyCategory( + { + ...cat, + name: "After Name", + }, + 5, + ), + ]; + req.reply({ + statusCode: 200, + body: categoryMutationSuccessBody("Category updated"), + }); + }).as("updateCategory"); + + cy.visit(companyCategoryEditUrl(cat.id)); + cy.contains("h2", "Edit Company Category").should("be.visible"); + cy.getByCy("company-category-form-name-input").should("have.value", "Before Name"); + cy.getByCy("company-category-form-name-input").clear().type("After Name"); + cy.getByCy("admin-form-submit-button").click(); + cy.wait("@updateCategory"); + cy.location("pathname").should("eq", "/company-categories"); + cy.contains(/success|updated|Category updated/i).should("be.visible"); + cy.contains("After Name").should("be.visible"); + }); + + it("edit: cancel uses back navigation", () => { + const cat = makeCompanyCategory({}, 1); + stubCompanyCategoriesReads(() => [cat]); + + cy.visit(companyCategoriesListUrl()); + cy.getByCy(`company-categories-row-edit-${cat.id}`).click(); + cy.location("pathname").should("eq", `/company-categories/edit/${cat.id}`); + cy.getByCy("admin-form-cancel-button").click(); + cy.location("pathname").should("eq", "/company-categories"); + }); + + it("create: cancel navigates back when opened from list", () => { + stubCompanyCategoriesReads(() => [makeCompanyCategory({}, 1)]); + cy.visit(companyCategoriesListUrl()); + cy.getByCy("company-categories-create-button").click(); + cy.location("pathname").should("eq", "/company-categories/create"); + cy.getByCy("admin-form-cancel-button").click(); + cy.location("pathname").should("eq", "/company-categories"); + }); +}); diff --git a/cypress/e2e/companies/company-categories-list.cy.ts b/cypress/e2e/companies/company-categories-list.cy.ts new file mode 100644 index 0000000..9fb8d63 --- /dev/null +++ b/cypress/e2e/companies/company-categories-list.cy.ts @@ -0,0 +1,156 @@ +/// +import { makeCompanyCategory } from "../../support/company-category-fixtures"; +import { companyCategoriesListUrl } from "../../support/company-category-helpers"; +import { stubCompanyCategoriesReads } from "../../support/company-category-intercepts"; + +describe("company categories list", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("toggles published optimistically, then keeps state after PATCH succeeds", () => { + const cat = makeCompanyCategory({ published: false }, 1); + let categories: ReturnType[] = [cat]; + + stubCompanyCategoriesReads(() => categories); + cy.intercept("PATCH", "**/api/proxy/company-categories/**/publish", (req) => { + const m = req.url.match(/company-categories\/([^/]+)\/publish/); + const id = m?.[1]; + if (id) { + categories = categories.map((c) => + c.id === id ? { ...c, published: !c.published } : c, + ); + } + req.reply({ + statusCode: 200, + body: { success: true, message: "Publish status updated", data: {} }, + }); + }).as("togglePublished"); + + cy.visit(companyCategoriesListUrl()); + const toggle = () => cy.getByCy("company-categories-published-toggle-" + cat.id); + toggle().should("not.be.checked"); + toggle().click({ force: true }); + // Optimistic update (useOptimistic) before the network call finishes. + toggle().should("be.checked"); + cy.wait("@togglePublished"); + toggle().should("be.checked"); + }); + + it("reverts published toggle when PATCH fails", () => { + const cat = makeCompanyCategory({ published: false }, 1); + stubCompanyCategoriesReads(() => [cat]); + cy.intercept("PATCH", "**/api/proxy/company-categories/**/publish", { + statusCode: 500, + body: { success: false, message: "Server error" }, + delay: 200, + }).as("togglePublishedFail"); + + cy.visit(companyCategoriesListUrl()); + const toggle = () => cy.getByCy("company-categories-published-toggle-" + cat.id); + toggle().should("not.be.checked"); + toggle().click({ force: true }); + toggle().should("be.checked"); + cy.wait("@togglePublishedFail"); + toggle().should("not.be.checked"); + }); + + it("opens filter modal, searches by q, and updates the list", () => { + const alpha = makeCompanyCategory({ name: "Alpha Row", id: "507f1f77bcf86cd799439011" }, 1); + const beta = makeCompanyCategory({ name: "Beta Row", id: "507f1f77bcf86cd799439012" }, 2); + let categories = [alpha, beta]; + + stubCompanyCategoriesReads(() => categories); + + cy.visit(companyCategoriesListUrl()); + cy.contains("Alpha Row").should("be.visible"); + cy.getByCy("company-categories-filter-button").click(); + cy.getByCy("company-categories-filter-form").should("be.visible"); + cy.getByCy("company-categories-filter-search-input").clear().type("Beta"); + cy.then(() => { categories = [beta]; }); + cy.getByCy("company-categories-filter-submit-button").click(); + cy.location("search").should("include", "q=Beta"); + cy.contains("Beta Row").should("be.visible"); + cy.contains("Alpha Row").should("not.exist"); + }); + + it("filters by published select and clears published filter", () => { + const pub = makeCompanyCategory({ name: "Pub Cat", published: true }, 1); + const draft = makeCompanyCategory( + { name: "Draft Cat", published: false, id: "507f1f77bcf86cd799439013" }, + 2, + ); + let categories = [pub, draft]; + + stubCompanyCategoriesReads(() => categories); + + cy.visit(companyCategoriesListUrl()); + cy.getByCy("company-categories-filter-button").click(); + cy.getByCy("company-categories-filter-published-select").select("false"); + cy.then(() => { categories = [draft]; }); + cy.getByCy("company-categories-filter-submit-button").click(); + cy.location("search").should("include", "published=false"); + cy.contains("Draft Cat").should("be.visible"); + cy.contains("Pub Cat").should("not.exist"); + + cy.getByCy("company-categories-filter-button").click(); + cy.getByCy("company-categories-filter-published-select-clear").click(); + cy.then(() => { categories = [pub, draft]; }); + cy.getByCy("company-categories-filter-submit-button").click(); + cy.location("search").should("not.include", "published=false"); + cy.contains("Pub Cat").should("be.visible"); + }); + + it("closes filter modal without applying", () => { + const cat = makeCompanyCategory({}, 1); + stubCompanyCategoriesReads(() => [cat]); + + cy.visit(companyCategoriesListUrl()); + cy.getByCy("company-categories-filter-button").click(); + cy.getByCy("company-categories-filter-form").should("be.visible"); + cy.getByCy("company-categories-filter-close-button").click(); + cy.getByCy("company-categories-filter-form").should("not.exist"); + }); + + it("navigates to create from header button", () => { + stubCompanyCategoriesReads(() => [makeCompanyCategory({}, 1)]); + cy.visit(companyCategoriesListUrl()); + cy.getByCy("company-categories-create-button").click(); + cy.location("pathname").should("eq", "/company-categories/create"); + cy.getByCy("company-category-form").should("be.visible"); + cy.contains("h2", "Create Company Category").should("be.visible"); + }); + + it("opens edit route for a row", () => { + const cat = makeCompanyCategory({}, 1); + stubCompanyCategoriesReads(() => [cat]); + + cy.visit(companyCategoriesListUrl()); + cy.getByCy(`company-categories-row-edit-${cat.id}`).click(); + cy.location("pathname").should("eq", `/company-categories/edit/${cat.id}`); + cy.contains("h2", "Edit Company Category").should("be.visible"); + }); + + it("deletes after confirm and shows empty state", () => { + const cat = makeCompanyCategory({}, 1); + let categories = [cat]; + + stubCompanyCategoriesReads(() => categories); + cy.intercept("DELETE", `**/api/proxy/company-categories/${cat.id}`, { + statusCode: 200, + body: { success: true, message: "Deleted", data: {} }, + }).as("deleteCategory"); + + cy.visit(companyCategoriesListUrl()); + cy.getByCy(`company-categories-row-delete-${cat.id}`).click(); + cy.getByCy("confirmation-modal").should("be.visible"); + cy.getByCy("confirmation-modal-cancel").click(); + cy.getByCy("confirmation-modal").should("not.exist"); + + cy.getByCy(`company-categories-row-delete-${cat.id}`).click(); + cy.then(() => { categories = []; }); + cy.getByCy("confirmation-modal-confirm").click(); + cy.wait("@deleteCategory"); + cy.getByCy("empty-list-message").should("contain.text", "No categories were found"); + }); +}); diff --git a/cypress/e2e/customers/customers-management.cy.ts b/cypress/e2e/customers/customers-management.cy.ts index 38bc9fe..c8bdb3f 100644 --- a/cypress/e2e/customers/customers-management.cy.ts +++ b/cypress/e2e/customers/customers-management.cy.ts @@ -366,7 +366,11 @@ describe("customers management - list, actions, filters, feedback", () => { cy.visit("/customers"); cy.wait("@getCustomers"); cy.get('button[data-tooltip-id="feedback"]').first().click(); - cy.location("pathname").should("eq", `/customers/feedback/${customer.id}`); + // Route transition can be delayed in headless runs. + cy.location("pathname", { timeout: 15000 }).should( + "eq", + `/customers/feedback/${customer.id}`, + ); cy.wait("@getCustomerFeedback"); cy.contains("feedback type").should("be.visible"); }); diff --git a/cypress/support/company-category-fixtures.ts b/cypress/support/company-category-fixtures.ts new file mode 100644 index 0000000..03ef181 --- /dev/null +++ b/cypress/support/company-category-fixtures.ts @@ -0,0 +1,45 @@ +export type CompanyCategoryRow = { + id: string; + name: string; + description: string; + published: boolean; + created_at: number; + updated_at: number; + published_at: number; + thumbnail: string; +}; + +export const makeCompanyCategory = ( + overrides: Partial = {}, + index = 1, +): CompanyCategoryRow => ({ + id: `507f1f77bcf86cd7994390${(10 + index).toString().padStart(2, "0")}`, + name: `E2E Category ${index}`, + description: "This is a sufficiently long description for category rows and forms.", + published: false, + created_at: 1_700_000_000 + index, + updated_at: 1_700_000_000 + index, + published_at: 0, + thumbnail: "", + ...overrides, +}); + +export const categoriesListResponse = (categories: CompanyCategoryRow[]) => ({ + success: true, + message: "ok", + data: { categories }, +}); + +export const categoryDetailsResponse = (data: { + name: string; + description: string; + thumbnail?: string; +}) => ({ + success: true, + message: "ok", + data: { + name: data.name, + description: data.description, + thumbnail: data.thumbnail ?? "", + }, +}); diff --git a/cypress/support/company-category-helpers.ts b/cypress/support/company-category-helpers.ts new file mode 100644 index 0000000..d5580c2 --- /dev/null +++ b/cypress/support/company-category-helpers.ts @@ -0,0 +1,14 @@ +/** Matches `useCreateCompanyCategory` / `useUpdateCompanyCategories` onSuccess (axios `response.data` shape). */ +export const categoryMutationSuccessBody = (message: string) => ({ + success: true, + data: { message }, +}); + +export const companyCategoriesListUrl = (path = "/company-categories") => + `${path}?_cy=${Date.now()}`; + +export const companyCategoryCreateUrl = () => + `/company-categories/create?_cy=${Date.now()}`; + +export const companyCategoryEditUrl = (id: string) => + `/company-categories/edit/${id}?_cy=${Date.now()}`; diff --git a/cypress/support/company-category-intercepts.ts b/cypress/support/company-category-intercepts.ts new file mode 100644 index 0000000..e88e1d8 --- /dev/null +++ b/cypress/support/company-category-intercepts.ts @@ -0,0 +1,53 @@ +import { + categoriesListResponse, + categoryDetailsResponse, + type CompanyCategoryRow, +} from "./company-category-fixtures"; + +/** Subset of Cypress `cy.intercept` request used by this stub (avoids non-exported / deep Cypress types). */ +type InterceptRequest = { + url: string; + reply(staticResponse: { statusCode: number; body: Record }): void; + continue(): void; +}; + +const CATEGORY_ID_IN_URL = /\/company-categories\/([a-f0-9]{24})(?:\?|\/|$)/i; + +/** + * Stubs GET list and GET single category. Uses `req.url` string matching so all + * request shapes (query strings, rewritten hosts) are handled consistently. + */ +export function stubCompanyCategoriesReads( + getCategories: () => CompanyCategoryRow[], +): void { + const reply = (req: InterceptRequest) => { + const raw = req.url; + const detailMatch = raw.match(CATEGORY_ID_IN_URL); + if (detailMatch) { + const id = detailMatch[1]; + const cat = getCategories().find((c) => c.id === id); + req.reply({ + statusCode: 200, + body: categoryDetailsResponse({ + name: cat?.name ?? "Category", + description: cat?.description ?? "Default description long enough.", + thumbnail: cat?.thumbnail ?? "", + }), + }); + return; + } + + if (raw.includes("/company-categories")) { + req.reply({ + statusCode: 200, + body: categoriesListResponse(getCategories()), + }); + return; + } + + req.continue(); + }; + + cy.intercept("GET", /\/api\/proxy\/company-categories/, reply); + cy.intercept("GET", /\/admin\/v1\/company-categories/, reply); +} diff --git a/src/app/(companies)/companies/details/[id]/page.tsx b/src/app/(companies)/companies/details/[id]/page.tsx index 67a77f8..3b39f1a 100644 --- a/src/app/(companies)/companies/details/[id]/page.tsx +++ b/src/app/(companies)/companies/details/[id]/page.tsx @@ -2,20 +2,56 @@ import Loading from "@/components/loading"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; import { ShowcaseSection } from "@/components/Layouts/showcase-section"; -import IsVisible from "@/components/ui/IsVisible"; import Status from "@/components/ui/Status"; import { useCompanyDetails } from "@/hooks/queries"; +import { cn } from "@/lib/utils"; import { BreadcrumbItem } from "@/types/shared"; import { formatPhoneNumber, unixToDate } from "@/utils/shared"; import getSymbolFromCurrency from "currency-symbol-map"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { use } from "react"; interface IProps { params: Promise<{ id: string }>; } +function SectionHeading({ children }: { children: React.ReactNode }) { + return ( +
+ +

+ {children} +

+ +
+ ); +} + +function ExternalLinkIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +const SHOWCASE_CENTERED_BODY = { + rootClassName: "flex h-full min-h-0 flex-col", + className: + "flex min-h-0 flex-1 flex-col items-center justify-center text-center", +} as const; + const CompanyDetailsPage = ({ params }: IProps) => { const { id } = use(params); const breadcrumbItems: BreadcrumbItem[] = [ @@ -33,8 +69,9 @@ const CompanyDetailsPage = ({ params }: IProps) => { }, ]; const { data, isLoading } = useCompanyDetails(id); - const router = useRouter(); if (isLoading) return ; + const company = data.data; + const website = company.website?.trim(); return ( <> @@ -42,71 +79,142 @@ const CompanyDetailsPage = ({ params }: IProps) => { - {data.data.name} - {data.data.status} +
+
+

+ Company +

+

+ {company.name} +

+
+
+ {company.status} + {website && ( + + Visit website + + + )} +
} + className="!p-0" > -
- - - {`${data?.data?.currency} ${getSymbolFromCurrency(data?.data.currency ?? "USD")}`} - - - - {data?.data?.language} - - - {unixToDate({ unix: data?.data?.created_at })} - - - {data?.data?.annual_revenue} - - - {data?.data?.email} - - - {data?.data?.employee_count} - - - {data?.data?.founded_year} - - - {data?.data?.industry} - - - {data?.data?.is_compliant ? "Yes" : "No"} - - - {data?.data?.is_verified ? "Yes" : "No"} - - - {formatPhoneNumber(data?.data?.phone ?? 0)} - - - {data?.data?.registration_number} - - - {data?.data?.tax_id} - - - {data?.data?.type} - - - {unixToDate({ unix: data?.data?.updated_at ?? 0 })} - - - - +
+ Profile +
+ + + {company.industry || "—"} + + + + + {company.type || "—"} + + + + + {company.language || "—"} + + + + + {company.founded_year || "—"} + + +
+
+ +
+ Business +
+ + + {`${company.currency || "—"} ${getSymbolFromCurrency(company.currency ?? "USD")}`} + + + + + {company.annual_revenue || "—"} + + + + + {company.employee_count || "—"} + + + - {data.data.website} - - - + + {company.registration_number || "—"} + + + + + {company.tax_id || "—"} + + + + + {company.is_compliant ? "Yes" : "No"} + + + + + {company.is_verified ? "Yes" : "No"} + + +
+
+ +
+ Contact & timeline +
+ + + {company.email || "—"} + + + + + {formatPhoneNumber(company.phone ?? 0)} + + + + + + + + + {website && ( + + + + {website} + + + )} +
+
diff --git a/src/app/tenders/[details]/page.tsx b/src/app/tenders/[details]/page.tsx index 7250468..1715592 100644 --- a/src/app/tenders/[details]/page.tsx +++ b/src/app/tenders/[details]/page.tsx @@ -22,6 +22,8 @@ interface IProps { }>; } +const isValidDisplayCountryCode = (code: string) => /^[A-Za-z]{2,3}$/.test(code); + function SectionHeading({ children }: { children: React.ReactNode }) { return (
@@ -65,9 +67,11 @@ const TenderDetails = ({ params }: IProps) => { const { data, isLoading, isError } = useTenderDetailQuery(details); const countryCodeNormalized = data?.data.country_code?.trim().toUpperCase() ?? ""; - const countryCodeValid = isValidAlpha2CountryCode(countryCodeNormalized); + const hasValidCountryCodeForDisplay = + isValidDisplayCountryCode(countryCodeNormalized); + const canLoadCountryFlag = isValidAlpha2CountryCode(countryCodeNormalized); const { data: flagData } = useGetFlagQuery(countryCodeNormalized, { - enabled: countryCodeValid, + enabled: canLoadCountryFlag, }); const breadcrumbItems = [ { name: "Dashboard", href: "/" }, @@ -103,6 +107,14 @@ const TenderDetails = ({ params }: IProps) => { const tender = data!.data; const submissionUrl = tender.submission_url?.trim(); + const currencyCode = tender.currency?.trim().toUpperCase() ?? ""; + const hasValidCurrency = Boolean(currencyCode); + const hasValidEstimatedValue = + typeof tender.estimated_value === "number" && + Number.isFinite(tender.estimated_value) && + tender.estimated_value > 0; + const hasValidLocationCurrencyOrValue = + hasValidCountryCodeForDisplay || hasValidCurrency || hasValidEstimatedValue; return ( <> @@ -252,50 +264,73 @@ const TenderDetails = ({ params }: IProps) => {
-
- Location & currency -
-
-
-

- Country -

-
- - - -
- - {tender.country_code} - - {flagData && ( -
- )} + {hasValidLocationCurrencyOrValue && ( +
+ Location & currency +
+ {hasValidCountryCodeForDisplay && ( +
+
+

+ Country +

+
+ + + +
+ + {countryCodeNormalized} + + {flagData && ( +
+ )} +
+
-
-
+ )} - {tender.currency && ( -
-
-

- Currency -

-
- - {tender.currency} - - - {getSymbolFromCurrency(tender.currency ?? "USD")} - + {hasValidCurrency && ( +
+
+

+ Currency +

+
+ + {currencyCode} + + + {getSymbolFromCurrency(currencyCode || "USD")} + +
-
- )} + )} + + {hasValidEstimatedValue && ( +
+
+

+ Estimated value +

+
+ + {new Intl.NumberFormat().format(tender.estimated_value)} + + {hasValidCurrency && ( + + {currencyCode} + + )} +
+
+ )} +
-
+ )}
Description diff --git a/src/components/FormElements/InputGroup/text-area.tsx b/src/components/FormElements/InputGroup/text-area.tsx index 1294229..8b9d3c5 100644 --- a/src/components/FormElements/InputGroup/text-area.tsx +++ b/src/components/FormElements/InputGroup/text-area.tsx @@ -18,6 +18,7 @@ type PropsType = { className?: string; icon?: React.ReactNode; defaultValue?: string; + dataCy?: string; handleChange?: (e: React.ChangeEvent) => void; register?: UseFormRegister; errors?: FieldErrors; @@ -33,6 +34,7 @@ export function TextAreaGroup({ className, icon, defaultValue, + dataCy, handleChange, register, errors, @@ -77,6 +79,7 @@ export function TextAreaGroup({ required={required} disabled={disabled} data-active={active} + data-cy={dataCy} /> {icon} diff --git a/src/components/FormElements/switch.tsx b/src/components/FormElements/switch.tsx index 358ad2b..c94f557 100644 --- a/src/components/FormElements/switch.tsx +++ b/src/components/FormElements/switch.tsx @@ -8,6 +8,7 @@ type PropsType = { backgroundSize?: "sm" | "default"; name?: string; checked?: boolean; + dataCy?: string; onToggle: (e: ToggleEvent) => void; onChange: (e: ChangeEvent) => void; }; @@ -18,6 +19,7 @@ export function Switch({ backgroundSize, name, checked = false, + dataCy, onToggle, onChange, }: PropsType) { @@ -37,6 +39,7 @@ export function Switch({ id={id} className="peer sr-only" checked={checked} + data-cy={dataCy} />