feat(tender-details): enhance document handling and navigation in tender details

- Added a new documents section to the tender details page, improving organization and visibility of related documents.
- Updated the tender navigation items to conditionally include the documents section based on the loading state and document availability.
- Refactored the TenderDetailsContent component to manage document loading states and conditionally render the documents section.
- Enhanced the getTenderNavItems utility to accept options for showing the documents section, improving flexibility in navigation rendering.
This commit is contained in:
AmirReza Jamali
2026-05-13 08:55:40 +03:30
parent d65f8cdf48
commit 2178f25af5
6 changed files with 49 additions and 17 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
NEXT_PUBLIC_APP_VERSION=2.0.3 NEXT_PUBLIC_APP_VERSION=2.1.0
NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500 NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0 NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com
@@ -21,6 +21,8 @@ type TenderDetailsContentProps = {
tenderId: string; tenderId: string;
documents: TTenderDocumentsResponse[]; documents: TTenderDocumentsResponse[];
documentsLoading: boolean; documentsLoading: boolean;
/** False once documents have loaded and the list is empty — section and nav are omitted. */
showDocumentsSection: boolean;
}; };
export function TenderDetailsContent({ export function TenderDetailsContent({
@@ -30,6 +32,7 @@ export function TenderDetailsContent({
tenderId, tenderId,
documents, documents,
documentsLoading, documentsLoading,
showDocumentsSection,
}: TenderDetailsContentProps) { }: TenderDetailsContentProps) {
const showLocation = shouldShowLocationSection(tender); const showLocation = shouldShowLocationSection(tender);
@@ -54,11 +57,13 @@ export function TenderDetailsContent({
{showLocation && <TenderLocationSection tender={tender} />} {showLocation && <TenderLocationSection tender={tender} />}
<TenderTranslationSection tender={tender} /> <TenderTranslationSection tender={tender} />
{showDocumentsSection && (
<TenderDocumentsSection <TenderDocumentsSection
tenderId={tenderId} tenderId={tenderId}
documents={documents} documents={documents}
isLoading={documentsLoading} isLoading={documentsLoading}
/> />
)}
<TenderDescriptionSection tender={tender} /> <TenderDescriptionSection tender={tender} />
<TenderAiSummarySection tender={tender} /> <TenderAiSummarySection tender={tender} />
</div> </div>
+15 -5
View File
@@ -41,9 +41,20 @@ const TenderDetails = ({ params }: IProps) => {
]; ];
const tenderForNav = data?.data; const tenderForNav = data?.data;
const { data: documentsResponse, isPending: isDocumentsLoading } =
useGetTenderDocumentsQuery(details);
const documents = documentsResponse?.data ?? [];
const showDocumentsSection =
isDocumentsLoading || documents.length > 0;
const tenderNavItems = useMemo( const tenderNavItems = useMemo(
() => (tenderForNav ? getTenderNavItems(tenderForNav) : []), () =>
[tenderForNav], tenderForNav
? getTenderNavItems(tenderForNav, {
showDocuments: showDocumentsSection,
})
: [],
[tenderForNav, showDocumentsSection],
); );
const sectionIds = useMemo( const sectionIds = useMemo(
@@ -51,8 +62,6 @@ const TenderDetails = ({ params }: IProps) => {
[tenderNavItems], [tenderNavItems],
); );
const activeSectionId = useTenderSectionScrollSpy(sectionIds); const activeSectionId = useTenderSectionScrollSpy(sectionIds);
const { data: documentsResponse, isPending: isDocumentsLoading } =
useGetTenderDocumentsQuery(details);
const handleLanguageChange = useCallback( const handleLanguageChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => { (event: React.ChangeEvent<HTMLSelectElement>) => {
const nextParams = new URLSearchParams(searchParams.toString()); const nextParams = new URLSearchParams(searchParams.toString());
@@ -108,8 +117,9 @@ const TenderDetails = ({ params }: IProps) => {
tenderNavItems={tenderNavItems} tenderNavItems={tenderNavItems}
activeSectionId={activeSectionId} activeSectionId={activeSectionId}
tenderId={details} tenderId={details}
documents={documentsResponse?.data ?? []} documents={documents}
documentsLoading={isDocumentsLoading} documentsLoading={isDocumentsLoading}
showDocumentsSection={showDocumentsSection}
/> />
</ShowcaseSection> </ShowcaseSection>
</> </>
@@ -18,8 +18,12 @@ function analyzeLocationVisibility(tender: TTenderDetails) {
return { showLocation }; return { showLocation };
} }
export function getTenderNavItems(tender: TTenderDetails): TenderNavItem[] { export function getTenderNavItems(
tender: TTenderDetails,
options?: { showDocuments?: boolean },
): TenderNavItem[] {
const { showLocation } = analyzeLocationVisibility(tender); const { showLocation } = analyzeLocationVisibility(tender);
const showDocuments = options?.showDocuments ?? true;
const items: TenderNavItem[] = [ const items: TenderNavItem[] = [
{ id: TENDER_SECTION_IDS.procurement, label: "Procurement" }, { id: TENDER_SECTION_IDS.procurement, label: "Procurement" },
@@ -31,9 +35,11 @@ export function getTenderNavItems(tender: TTenderDetails): TenderNavItem[] {
label: "Location & currency", label: "Location & currency",
}); });
} }
items.push({ id: TENDER_SECTION_IDS.translation, label: "Translation" });
if (showDocuments) {
items.push({ id: TENDER_SECTION_IDS.documents, label: "Documents" });
}
items.push( items.push(
{ id: TENDER_SECTION_IDS.translation, label: "Translation" },
{ id: TENDER_SECTION_IDS.documents, label: "Documents" },
{ id: TENDER_SECTION_IDS.description, label: "Description" }, { id: TENDER_SECTION_IDS.description, label: "Description" },
{ id: TENDER_SECTION_IDS.aiSummary, label: "AI summary" }, { id: TENDER_SECTION_IDS.aiSummary, label: "AI summary" },
); );
@@ -58,7 +58,10 @@ export const useAdminsPresenter = () => {
setParams(newParams); setParams(newParams);
filterFormReset(newParams); filterFormReset(newParams);
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isPending } = useGetAdminListQuery({ ...params, ...apiDefaultParams }); const { data, isPending } = useGetAdminListQuery({
...apiDefaultParams,
...params,
});
const shouldShowPagination = const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10; !isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const { mutate: deleteAdmin } = useDeleteAdmin(() => { const { mutate: deleteAdmin } = useDeleteAdmin(() => {
@@ -1,6 +1,9 @@
import { GlobeIcon } from "@/assets/icons";
import DatePicker from "@/components/FormElements/DatePicker/DatePicker"; import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
import InputGroup from "@/components/FormElements/InputGroup"; import InputGroup from "@/components/FormElements/InputGroup";
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 { import {
Control, Control,
FieldValues, FieldValues,
@@ -55,11 +58,16 @@ const TenderListFilters = ({
type="text" type="text"
/> />
<InputGroup <Select<FieldValues>
{...filterRegister("country")} {...filterRegister("country")}
name="country" name="country"
label="country" label="country"
type="text" items={Countries}
placeholder="Country"
clearable
prefixIcon={<GlobeIcon />}
value={watch("country")}
onClear={() => setValue("country", "")}
/> />
<InputGroup <InputGroup