PageSourceSearch

https://smrtbld.ai/js/pages/bids-tendering.js

js smrtbld.ai collected 2026-10-05 15:07:34 UTC 54,750 bytes, 1,317 lines download raw bytes

1import { getInitials } from '../utils/jsrender-helpers.js';
2import { projectsApi } from '../api/projects.js';
3import { offlineApi } from '../offline/offline-api.js';
4import { btOverviewApi } from '../api/bids-tendering-overview.js';
5import { confirm } from '../utils/modal.js';
6import { getSubModuleIcon, renderSubModuleTabs } from '../utils/sub-module-icons.js';
7import { renderTo, loadTemplates } from '../utils/templates.js';
8import { auth } from '../auth.js';
9import {
10    initBtOverviewDetails,
11    destroyBtOverviewDetails,
12    getCurrentBtOverviewTab,
13    getCurrentBtOverviewTaskId,
14    syncBtOverviewFromRoute,
15} from './bids-tendering-overview.js';
16import { initProjectTeam, destroyProjectTeam, TEAM_PAGE_LINKS } from './project-team.js';
17import { initProjectDocuments, destroyProjectDocuments, getCurrentDocumentFolderId, syncProjectDocumentsRoute, DOCUMENTS_PAGE_LINKS } from './project-documents.js';
18import { initProjectMeetings, destroyProjectMeetings, MEETINGS_PAGE_LINKS, getCurrentMeetingId } from './project-meetings.js';
19import { initBidTenders, destroyBidTenders, getCurrentBidTenderTaskId, BID_TENDER_PAGE_LINKS } from './bids-bid-tenders.js';
20import { initTenderClarifications, destroyTenderClarifications, getCurrentTenderClarificationTaskId, TENDER_CLARIFICATION_PAGE_LINKS } from './bids-tender-clarifications.js';
21import { initBidsContracts, destroyBidsContracts, getCurrentBidsContractTaskId, BIDS_CONTRACT_PAGE_LINKS } from './bids-contracts.js';
22import { initQuotations, destroyQuotations, getCurrentQuotationTaskId, QUOTATION_PAGE_LINKS } from './bids-quotations.js';
23import { initBidEvaluations, destroyBidEvaluations, getCurrentBidEvaluationTaskId, BID_EVALUATION_PAGE_LINKS } from './bids-evaluations.js';
24import { initProjectActivityLog, destroyProjectActivityLog, ACTIVITY_LOG_PAGE_LINKS } from './project-activity-log.js';
25import { FastTrackBanner } from '../components/FastTrackBanner.js';
26import { destroyTabNav } from '../utils/tab-nav-mode.js';
27import { DropdownNavigator } from '../components/DropdownNavigator.js';
28import { isMobile, isMobileOrTablet } from '../utils/mobile.js';
29import { DivisionFilter } from '../components/DivisionFilter.js';
30import { loadReadinessRings, clearReadinessRings } from '../utils/dashboard-charts.js';
31import { companySettings } from '../services/company-settings.js';
32import { initProjectDivisionsEditor } from '../utils/project-divisions-editor.js';
33
34let abortController = null;
35let divisionFilter = null;
36let currentBidsTenderingId = null;
37let currentProject = null;
38let bidsTenderingItems = [];
39let filteredBidsTenderingItems = [];
40let filterPopoverEl = null;
41let selectionGeneration = 0;
42let switchGeneration = 0;
43let routeOperationGeneration = 0;
44
45// Sub-module / collapse state
46let activeSubModule = 'overview';
47let listCollapsed = false;
48let _navigating = false;
49
50// Dynamic sub-modules from API (cached across page navigations)
51const SECTION_CODE = 'BIDS';
52const OVERVIEW_TAB = { id: 'overview', label: 'Overview', icon: 'layout-dashboard' };
53const ACTIVITY_LOG_TAB = { id: 'activity-log', label: 'Activity Log', icon: 'activity' };
54let subModules = [];
55let subModulesLoaded = false;
56let btTemplatesLoaded = false;
57
58async function loadSubModules() {
59    if (subModulesLoaded) return;
60    try {
61        const response = await projectsApi.getPageAccess(SECTION_CODE, { signal: abortController?.signal });
62        const apiTabs = (response.pages || []).map(p => ({
63            id: p.pageLink,
64            label: p.pageTitle,
65            icon: getSubModuleIcon(p.pageLink)
66        }));
67        subModules = [OVERVIEW_TAB, ...apiTabs, ACTIVITY_LOG_TAB];
68        subModulesLoaded = true;
69    } catch (error) {
70        if (error.name === 'AbortError') return;
71        subModules = [OVERVIEW_TAB, ACTIVITY_LOG_TAB];
72    }
73}
74
75/**
76 * Session-boundary reset (CA-051 / MCF-0080). Called from teardownAuthenticatedServices
77 * (router.js) so the next in-tab session derives its tab set from its own getPageAccess.
78 * Never called on ordinary navigation — the per-session cache above stays intact.
79 */
80export function resetSubModulesCache() {
81    subModules = [];
82    subModulesLoaded = false;
83}
84
85// Filter state — defaults: active only, all membership
86let filterState = {
87    status: ['active'],
88    membership: ['mine', 'all']
89};
90
91/**
92 * Initialize Bids & Tendering Page
93 * @param {number|null} selectedId - Optional project ID to auto-select (from URL)
94 * @param {string|null} subModule - Optional sub-module to activate
95 * @param {string|null} subTab - Optional sub-tab (reserved for future use)
96 */
97export async function initBidsTenderingPage(selectedId = null, subModule = null, subTab = null, taskId = null, taskTargetInvalid = false) {
98    const routeGeneration = ++routeOperationGeneration;
99    _navigating = true;
100    try {
101        abortController = new AbortController();
102        const pageSignal = abortController.signal;
103        initProjectDivisionsEditor({ rootId: 'detail-header', getProject: () => currentProject, onChanged: refreshBidsTenderingDivisions, signal: abortController.signal });
104
105        if (!btTemplatesLoaded) {
106            await loadTemplates('components/bids-tendering-sub-templates');
107            btTemplatesLoaded = true;
108        }
109
110        currentBidsTenderingId = null;
111        currentProject = null;
112        bidsTenderingItems = [];
113        filteredBidsTenderingItems = [];
114        filterState = { status: ['active'], membership: ['mine', 'all'] };
115        filterPopoverEl = null;
116        activeSubModule = 'overview';
117        listCollapsed = false;
118
119        initListHandlers();
120        initMobileBackHandler();
121        initDropdownNavigator();
122        initToggleHandler();
123        initSubModuleTabHandlers();
124
125        divisionFilter = await DivisionFilter.create({ onChange: () => loadBidsTenderingItems() });
126        if (divisionFilter) {
127            const searchRow = document.getElementById('bids-tendering-search')?.closest('.shrink-0');
128            searchRow?.after(divisionFilter.element);
129        }
130
131        await loadSubModules();
132        await loadBidsTenderingItems();
133        if (pageSignal.aborted || abortController?.signal !== pageSignal) return;
134
135        if (selectedId) {
136            const target = subModule || 'overview';
137            const selection = await selectBidsTendering(selectedId, {
138                routeOwned: true,
139                targetSubModule: target,
140                targetOverviewTab: target === 'overview' ? subTab : null,
141                targetOverviewTaskId: target === 'overview' ? taskId : null,
142            });
143            if (!selection) return;
144
145            if (subModule && subModule !== activeSubModule) {
146                await settleBidsTenderingAction(switchSubModule(subModule, subTab || null, { overviewTaskId: taskId, routeOwned: true }));
147            }
148            if (target === 'overview' && isOverviewChildTab(subTab)
149                && (taskTargetInvalid || selection.overviewSettled === false)) {
150                canonicalizeBtOverviewRoute(selectedId, routeGeneration);
151            }
152        }
153    } finally {
154        if (routeGeneration === routeOperationGeneration) _navigating = false;
155    }
156}
157
158/**
159 * Clean up Bids & Tendering Page
160 */
161export function destroyBidsTenderingPage() {
162    selectionGeneration++;
163    routeOperationGeneration++;
164    clearBidsTenderingSelectionTransition();
165    destroyTabNav(document.getElementById('sub-module-tabs'));
166    destroyProjectTeam();
167    destroyProjectDocuments();
168    destroyProjectMeetings();
169    destroyBidTenders();
170    destroyTenderClarifications();
171    destroyBidsContracts();
172    destroyQuotations();
173    destroyBidEvaluations();
174    destroyBtOverviewDetails();
175    destroyProjectActivityLog();
176    clearReadinessRings('bt');
177    DropdownNavigator.destroy(DROPDOWN_NAV_ID);
178    divisionFilter?.destroy();
179    divisionFilter = null;
180    abortController?.abort();
181    abortController = null;
182    currentBidsTenderingId = null;
183    currentProject = null;
184    bidsTenderingItems = [];
185    filteredBidsTenderingItems = [];
186    filterState = { status: ['active'], membership: ['mine', 'all'] };
187    filterPopoverEl = null;
188    activeSubModule = 'overview';
189    listCollapsed = false;
190    _navigating = false;
191}
192
193/**
194 * Lightweight in-page navigation (called by route guard on same-page back/forward).
195 * Reuses existing DOM, event handlers, and cached data — no full re-render.
196 * Sets _navigating flag to prevent URL pushes (URL is already correct from popstate).
197 */
198export async function navigateBidsTenderingPage(id = null, subModule = null, subTab = null, taskId = null, taskTargetInvalid = false) {
199    const routeGeneration = ++routeOperationGeneration;
200    _navigating = true;
201    try {
202        if (id && id !== currentBidsTenderingId) {
203            const target = subModule || 'overview';
204            const selection = await selectBidsTendering(id, {
205                routeOwned: true,
206                targetSubModule: target,
207                targetOverviewTab: target === 'overview' ? subTab : null,
208                targetOverviewTaskId: target === 'overview' ? taskId : null,
209            });
210            if (!selection) return;
211            let overviewSettled = selection.overviewSettled;
212            if (target !== activeSubModule) {
213                overviewSettled = await settleBidsTenderingAction(switchSubModule(target, subTab, { overviewTaskId: taskId, routeOwned: true }));
214            }
215            if (DOCUMENTS_PAGE_LINKS.includes(target)) await syncProjectDocumentsRoute(subTab);
216            if (target === 'overview' && isOverviewChildTab(subTab)
217                && (taskTargetInvalid || overviewSettled === false)) {
218                canonicalizeBtOverviewRoute(id, routeGeneration);
219            }
220        } else if (id && id === currentBidsTenderingId) {
221            const target = subModule || 'overview';
222            if (target !== activeSubModule) {
223                const settled = await settleBidsTenderingAction(switchSubModule(target, subTab, { overviewTaskId: taskId, routeOwned: true }));
224                if (target === 'overview' && isOverviewChildTab(subTab)
225                    && (taskTargetInvalid || settled === false)) {
226                    canonicalizeBtOverviewRoute(id, routeGeneration);
227                }
228            } else if (DOCUMENTS_PAGE_LINKS.includes(target)) {
229                await syncProjectDocumentsRoute(subTab);
230            } else if (target === 'overview') {
231                const targetOverviewTab = subTab || 'dashboard';
232                const settled = await syncBtOverviewFromRoute(
233                    targetOverviewTab,
234                    isOverviewChildTab(targetOverviewTab) && !taskTargetInvalid ? taskId : null,
235                );
236                if (isOverviewChildTab(targetOverviewTab) && (taskTargetInvalid || !settled)) {
237                    canonicalizeBtOverviewRoute(id, routeGeneration);
238                }
239            }
240        } else if (!id && currentBidsTenderingId) {
241            showMobileList();
242        }
243    } finally {
244        if (routeGeneration === routeOperationGeneration) _navigating = false;
245    }
246}
247
248// ========================================
249// LIST LOGIC
250// ========================================
251
252async function loadBidsTenderingItems() {
253    const listContainer = document.getElementById('bids-tendering-list');
254    if (!listContainer) return;
255
256    // Show skeleton loading state
257    listContainer.innerHTML = $.templates('#tpl-skeleton-list-compact').render({});
258
259    try {
260        const response = await projectsApi.getBidsTendering(divisionFilter?.getValue());
261        bidsTenderingItems = await offlineApi.filterProjectsToOffline(response.projects || []);
262
263        // Enrich list data with computed fields
264        bidsTenderingItems.forEach(enrichBidsTenderingData);
265
266        applyFilters();
267        renderBidsTenderingList();
268
269    } catch (error) {
270        if (error.name === 'AbortError') return;
271
272        window.showToast?.('Failed to load projects', 'error', { description: error.message });
273    }
274}
275
276async function renderBidsTenderingList() {
277    const listContainer = document.getElementById('bids-tendering-list');
278    const countEl = document.getElementById('bids-tendering-count');
279    if (!listContainer) return;
280
281    if (filteredBidsTenderingItems.length === 0) {
282        await renderTo('#bids-tendering-list', '#tpl-bids-tendering-empty', {});
283    } else {
284        await renderTo('#bids-tendering-list', '#tpl-bids-tendering-row', filteredBidsTenderingItems, {
285            getInitials
286        });
287
288        // Handle selection state
289        if (currentBidsTenderingId) {
290            const row = listContainer.querySelector(`[data-id="${currentBidsTenderingId}"]`);
291            if (row) row.classList.add('border-st-primary', 'bg-st-primary/5');
292        }
293    }
294
295    if (countEl) countEl.textContent = `${filteredBidsTenderingItems.length} projects`;
296
297    // Keep mini list in sync if collapsed
298    if (listCollapsed) {
299        renderMiniBidsTenderingList();
300    }
301
302    // Fetch readiness for each project in parallel and update rings
303    loadReadinessRings(listContainer, filteredBidsTenderingItems, (id) => btOverviewApi.getReadiness(id, { signal: abortController?.signal }), 'bt');
304
305    // Sync mobile dropdown navigator
306    syncDropdownNavigator();
307}
308
309function initListHandlers() {
310    const listContainer = document.getElementById('bids-tendering-list');
311    const searchInput = document.getElementById('bids-tendering-search');
312    const filterBtn = document.getElementById('btn-filter');
313    const addBtn = document.getElementById('btn-create-bid');
314
315    listContainer?.addEventListener('click', (e) => {
316        const row = e.target.closest('.bids-tendering-row');
317        if (row) {
318            const id = parseInt(row.dataset.id);
319            settleBidsTenderingAction(selectBidsTendering(id));
320        }
321        // Handle empty state create button
322        if (e.target.closest('[data-action="create-bid"]')) {
323            handleCreateBidsTendering();
324        }
325    }, { signal: abortController.signal });
326
327    searchInput?.addEventListener('input', (e) => {
328        applyFilters(e.target.value);
329        renderBidsTenderingList();
330    }, { signal: abortController.signal });
331
332    filterBtn?.addEventListener('click', () => toggleFilterPopover(), { signal: abortController.signal });
333
334    // Click-outside to close filter popover
335    document.addEventListener('click', (e) => {
336        if (filterPopoverEl && !filterPopoverEl.contains(e.target) && !e.target.closest('#btn-filter')) {
337            closeFilterPopover();
338        }
339    }, { signal: abortController.signal });
340
341    addBtn?.addEventListener('click', () => handleCreateBidsTendering(), { signal: abortController.signal });
342}
343
344function applyFilters(searchTerm = '') {
345    const term = (searchTerm || '').toLowerCase();
346    filteredBidsTenderingItems = bidsTenderingItems.filter(p => {
347        // Text search
348        const matchesSearch = !term ||
349            (p.projectTitle && p.projectTitle.toLowerCase().includes(term)) ||
350            (p.projectNumber && p.projectNumber.toLowerCase().includes(term)) ||
351            (p.divisionName && p.divisionName.toLowerCase().includes(term)) ||
352            (p.divisionNames && p.divisionNames.toLowerCase().includes(term)) ||
353            (p.projectCity && p.projectCity.toLowerCase().includes(term));
354
355        // Status filter
356        const isActive = !p.isArchived;
357        const matchesStatus =
358            (isActive && filterState.status.includes('active')) ||
359            (!isActive && filterState.status.includes('archived'));
360
361        // Membership filter
362        const matchesMembership =
363            filterState.membership.includes('all') ||
364            (filterState.membership.includes('mine') && p.isMemberOfProject);
365
366        return matchesSearch && matchesStatus && matchesMembership;
367    });
368}
369
370// ========================================
371// FILTER POPOVER
372// ========================================
373
374function toggleFilterPopover() {
375    if (filterPopoverEl) {
376        closeFilterPopover();
377        return;
378    }
379
380    const filterBtn = document.getElementById('btn-filter');
381    if (!filterBtn) return;
382
383    // Create popover container
384    filterPopoverEl = document.createElement('div');
385    filterPopoverEl.className = 'absolute right-0 top-full mt-2 w-56 card-st p-4 shadow-sb-lg z-50';
386
387    // Render template with current filter state as helpers
388    const tpl = $.templates('#tpl-bids-tendering-filter-popover');
389    filterPopoverEl.innerHTML = tpl.render({}, {
390        statusActive: filterState.status.includes('active'),
391        statusArchived: filterState.status.includes('archived'),
392        memberMine: filterState.membership.includes('mine'),
393        memberAll: filterState.membership.includes('all')
394    });
395
396    // Position relative to filter button
397    filterBtn.parentElement.style.position = 'relative';
398    filterBtn.parentElement.appendChild(filterPopoverEl);
399
400    // Checkbox change handlers
401    filterPopoverEl.querySelectorAll('input[type="checkbox"]').forEach(cb => {
402        cb.addEventListener('change', () => {
403            const filterGroup = cb.dataset.filter;
404            const value = cb.value;
405
406            if (filterGroup === 'status') {
407                if (cb.checked) {
408                    if (!filterState.status.includes(value)) filterState.status.push(value);
409                } else {
410                    filterState.status = filterState.status.filter(v => v !== value);
411                }
412            } else if (filterGroup === 'membership') {
413                if (cb.checked) {
414                    if (!filterState.membership.includes(value)) filterState.membership.push(value);
415                } else {
416                    filterState.membership = filterState.membership.filter(v => v !== value);
417                }
418            }
419
420            const searchInput = document.getElementById('bids-tendering-search');
421            applyFilters(searchInput?.value || '');
422            renderBidsTenderingList();
423            updateFilterIndicator();
424        });
425    });
426}
427
428function closeFilterPopover() {
429    if (filterPopoverEl) {
430        filterPopoverEl.remove();
431        filterPopoverEl = null;
432    }
433}
434
435function isFilterNonDefault() {
436    const defaultStatus = ['active'];
437    const defaultMembership = ['mine', 'all'];
438
439    const statusMatch = filterState.status.length === defaultStatus.length &&
440        defaultStatus.every(s => filterState.status.includes(s));
441    const membershipMatch = filterState.membership.length === defaultMembership.length &&
442        defaultMembership.every(m => filterState.membership.includes(m));
443
444    return !statusMatch || !membershipMatch;
445}
446
447function updateFilterIndicator() {
448    const indicator = document.getElementById('filter-indicator');
449    if (indicator) {
450        indicator.classList.toggle('hidden', !isFilterNonDefault());
451    }
452}
453
454// ========================================
455// URL MANAGEMENT
456// ========================================
457
458function buildUrl() {
459    const base = '/bids-tendering';
460    if (!currentBidsTenderingId) return base;
461    let url = `${base}/${currentBidsTenderingId}`;
462
463    // Overview sub-tabs: /bids-tendering/123/overview/procurement
464    if (activeSubModule === 'overview') {
465        const btOverviewTab = getCurrentBtOverviewTab();
466        if (btOverviewTab && btOverviewTab !== 'dashboard') {
467            url += `/overview/${btOverviewTab}`;
468            const btOverviewTaskId = getCurrentBtOverviewTaskId();
469            if (btOverviewTaskId) url += `/${btOverviewTaskId}`;
470        }
471        return url;
472    }
473
474    if (activeSubModule && activeSubModule !== 'overview') {
475        url += `/${activeSubModule}`;
476        const taskId = getActiveTaskId();
477        if (taskId) url += `/${taskId}`;
478    }
479    return url;
480}
481
482function getActiveTaskId() {
483    if (BID_TENDER_PAGE_LINKS.includes(activeSubModule)) return getCurrentBidTenderTaskId();
484    if (TENDER_CLARIFICATION_PAGE_LINKS.includes(activeSubModule)) return getCurrentTenderClarificationTaskId();
485    if (BIDS_CONTRACT_PAGE_LINKS.includes(activeSubModule)) return getCurrentBidsContractTaskId();
486    if (QUOTATION_PAGE_LINKS.includes(activeSubModule)) return getCurrentQuotationTaskId();
487    if (BID_EVALUATION_PAGE_LINKS.includes(activeSubModule)) return getCurrentBidEvaluationTaskId();
488    if (MEETINGS_PAGE_LINKS.includes(activeSubModule)) {
489        const meetingId = getCurrentMeetingId();
490        if (meetingId) return meetingId;
491    }
492    if (DOCUMENTS_PAGE_LINKS.includes(activeSubModule)) {
493        // Documents deep-links the open FOLDER (0 / root → no segment).
494        const folderId = getCurrentDocumentFolderId();
495        if (folderId) return folderId;
496    }
497    return null;
498}
499
500function updateUrl(push = false) {
501    const url = buildUrl();
502    if (push && !_navigating) {
503        page.show(url);
504    } else {
505        history.replaceState(null, '', url);
506    }
507}
508
509function handleBtOverviewTabChange() {
510    updateUrl();
511    const tab = getCurrentBtOverviewTab();
512    if (tab === 'dashboard') {
513        if (listCollapsed) expandList();
514    } else {
515        if (!listCollapsed) collapseList();
516    }
517}
518
519// ========================================
520// SUB-MODULE TABS
521// ========================================
522
523function initSubModuleTabHandlers() {
524    const tabContainer = document.getElementById('sub-module-tabs');
525    if (!tabContainer) return;
526
527    tabContainer.addEventListener('click', (e) => {
528        const btn = e.target.closest('[data-tab]');
529        if (!btn || btn.classList.contains('tab-nav-mode-toggle') || btn.classList.contains('tab-overflow-more') || btn.classList.contains('tab-overflow-item')) return;
530        settleBidsTenderingAction(switchSubModule(btn.dataset.tab));
531    }, { signal: abortController.signal });
532}
533
534async function switchSubModule(moduleId, initialTaskId = null, options = {}) {
535    if (!options.routeOwned) {
536        routeOperationGeneration += 1;
537        _navigating = false;
538    }
539    let documentsInitPromise = null;
540    // Same tab clicked — destroy current detail view so it re-inits back to list
541    if (moduleId === activeSubModule && !initialTaskId) {
542        if (BID_TENDER_PAGE_LINKS.includes(moduleId)) destroyBidTenders();
543        else if (TENDER_CLARIFICATION_PAGE_LINKS.includes(moduleId)) destroyTenderClarifications();
544        else if (BIDS_CONTRACT_PAGE_LINKS.includes(moduleId)) destroyBidsContracts();
545        else if (QUOTATION_PAGE_LINKS.includes(moduleId)) destroyQuotations();
546        else if (BID_EVALUATION_PAGE_LINKS.includes(moduleId)) destroyBidEvaluations();
547        else return; // overview, team, docs, etc. — no detail view to reset
548    }
549
550    const generation = ++switchGeneration;
551    const recordGeneration = selectionGeneration;
552    const selectedId = currentBidsTenderingId;
553    const pageSignal = abortController?.signal || null;
554    const ownsSwitch = () => generation === switchGeneration && recordGeneration === selectionGeneration && selectedId === currentBidsTenderingId
555        && abortController?.signal === pageSignal && !pageSignal?.aborted;
556
557    // Clean up previous sub-module state
558    if (activeSubModule === 'overview' && moduleId !== 'overview') {
559        destroyBtOverviewDetails();
560    }
561    if (TEAM_PAGE_LINKS.includes(activeSubModule) && !TEAM_PAGE_LINKS.includes(moduleId)) {
562        destroyProjectTeam();
563    }
564    if (DOCUMENTS_PAGE_LINKS.includes(activeSubModule) && !DOCUMENTS_PAGE_LINKS.includes(moduleId)) {
565        destroyProjectDocuments();
566    }
567    if (MEETINGS_PAGE_LINKS.includes(activeSubModule) && !MEETINGS_PAGE_LINKS.includes(moduleId)) {
568        destroyProjectMeetings();
569    }
570    if (BID_TENDER_PAGE_LINKS.includes(activeSubModule) && !BID_TENDER_PAGE_LINKS.includes(moduleId)) {
571        destroyBidTenders();
572    }
573    if (TENDER_CLARIFICATION_PAGE_LINKS.includes(activeSubModule) && !TENDER_CLARIFICATION_PAGE_LINKS.includes(moduleId)) {
574        destroyTenderClarifications();
575    }
576    if (BIDS_CONTRACT_PAGE_LINKS.includes(activeSubModule) && !BIDS_CONTRACT_PAGE_LINKS.includes(moduleId)) {
577        destroyBidsContracts();
578    }
579    if (QUOTATION_PAGE_LINKS.includes(activeSubModule) && !QUOTATION_PAGE_LINKS.includes(moduleId)) {
580        destroyQuotations();
581    }
582    if (BID_EVALUATION_PAGE_LINKS.includes(activeSubModule) && !BID_EVALUATION_PAGE_LINKS.includes(moduleId)) {
583        destroyBidEvaluations();
584    }
585    if (ACTIVITY_LOG_PAGE_LINKS.includes(activeSubModule) && !ACTIVITY_LOG_PAGE_LINKS.includes(moduleId)) {
586        await destroyProjectActivityLog();
587        if (!ownsSwitch()) return false;
588    }
589
590    activeSubModule = moduleId;
591
592    // Toggle header compact/expanded based on active tab
593    const headerEl = document.getElementById('detail-header');
594    if (headerEl) {
595        headerEl.classList.toggle('header-compact', moduleId !== 'overview');
596    }
597
598    // Update tab active state
599    const tabContainer = document.getElementById('sub-module-tabs');
600    tabContainer?.querySelectorAll('.bids-tendering-tab-btn').forEach(btn => {
601        const on = btn.dataset.tab === moduleId;
602        btn.classList.toggle('active', on);
603        btn.setAttribute('aria-selected', String(on));
604    });
605
606    // Show/hide sub-tab navs based on active main tab
607    document.querySelectorAll('#sub-tab-container > nav[data-parent-tab]').forEach(nav => {
608        nav.classList.toggle('hidden', nav.dataset.parentTab !== moduleId);
609    });
610
611    // Swap tab panels
612    const overviewPanel = document.getElementById('tab-overview');
613    const teamPanel = document.getElementById('tab-projteam');
614    const documentsPanel = document.getElementById('tab-projdocuments');
615    const meetingsPanel = document.getElementById('tab-meetings');
616    const comingSoonPanel = document.getElementById('tab-coming-soon');
617    const bidTendersPanel = document.getElementById('tab-bidstenders');
618    const tenderClarPanel = document.getElementById('tab-tenderclarification');
619    const contractsPanel = document.getElementById('tab-contracts');
620    const quotationsPanel = document.getElementById('tab-quotations');
621    const evaluationPanel = document.getElementById('tab-evaluation');
622    const activityLogPanel = document.getElementById('tab-activity-log');
623
624    // Hide all panels
625    overviewPanel?.classList.add('hidden');
626    teamPanel?.classList.add('hidden');
627    documentsPanel?.classList.add('hidden');
628    meetingsPanel?.classList.add('hidden');
629    bidTendersPanel?.classList.add('hidden');
630    tenderClarPanel?.classList.add('hidden');
631    contractsPanel?.classList.add('hidden');
632    quotationsPanel?.classList.add('hidden');
633    evaluationPanel?.classList.add('hidden');
634    activityLogPanel?.classList.add('hidden');
635    comingSoonPanel?.classList.add('hidden');
636
637    if (moduleId === 'overview') {
638        overviewPanel?.classList.remove('hidden');
639        const btSubTab = initialTaskId;
640        let overviewSettled = true;
641        if (currentBidsTenderingId) {
642            overviewSettled = await initBtOverviewDetails(currentBidsTenderingId, abortController?.signal, {
643                initialTab: btSubTab || undefined,
644                initialTaskId: options.overviewTaskId || null,
645                onTabChange: handleBtOverviewTabChange,
646                onTaskNavigate: handleBtOverviewRecordNavigate,
647                projectData: currentProject,
648            });
649            if (!ownsSwitch()) return false;
650        }
651
652        // Collapse/expand based on overview sub-tab
653        if (btSubTab && btSubTab !== 'dashboard') {
654            if (!listCollapsed) collapseList();
655        } else {
656            if (listCollapsed) expandList();
657        }
658        if (!overviewSettled) return false;
659    } else if (TEAM_PAGE_LINKS.includes(moduleId)) {
660        teamPanel?.classList.remove('hidden');
661        if (currentBidsTenderingId) {
662            initProjectTeam(currentBidsTenderingId, abortController?.signal);
663        }
664
665        if (!listCollapsed) collapseList();
666    } else if (DOCUMENTS_PAGE_LINKS.includes(moduleId)) {
667        documentsPanel?.classList.remove('hidden');
668        if (currentBidsTenderingId) {
669            documentsInitPromise = initProjectDocuments(currentBidsTenderingId, abortController?.signal, {
670                folderId: initialTaskId ? parseInt(initialTaskId, 10) || 0 : null,
671                onNavigate: ({ push = false } = {}) => updateUrl(push),
672            });
673        }
674
675        if (!listCollapsed) collapseList();
676    } else if (MEETINGS_PAGE_LINKS.includes(moduleId)) {
677        meetingsPanel?.classList.remove('hidden');
678        if (currentBidsTenderingId) {
679            initProjectMeetings(currentBidsTenderingId, abortController?.signal, {
680                initialMeetingId: initialTaskId || null,
681                onNavigate: () => updateUrl(),
682            });
683        }
684
685        if (!listCollapsed) collapseList();
686    } else if (BID_TENDER_PAGE_LINKS.includes(moduleId)) {
687        bidTendersPanel?.classList.remove('hidden');
688        if (currentBidsTenderingId) {
689            initBidTenders(currentBidsTenderingId, abortController?.signal, {
690                initialTaskId: initialTaskId || null,
691                onNavigate: () => updateUrl(),
692            });
693        }
694        if (!listCollapsed) collapseList();
695    } else if (TENDER_CLARIFICATION_PAGE_LINKS.includes(moduleId)) {
696        tenderClarPanel?.classList.remove('hidden');
697        if (currentBidsTenderingId) {
698            initTenderClarifications(currentBidsTenderingId, abortController?.signal, {
699                initialTaskId: initialTaskId || null,
700                onNavigate: () => updateUrl(),
701            });
702        }
703        if (!listCollapsed) collapseList();
704    } else if (BIDS_CONTRACT_PAGE_LINKS.includes(moduleId)) {
705        contractsPanel?.classList.remove('hidden');
706        if (currentBidsTenderingId) {
707            initBidsContracts(currentBidsTenderingId, abortController?.signal, {
708                initialTaskId: initialTaskId || null,
709                onNavigate: () => updateUrl(),
710            });
711        }
712        if (!listCollapsed) collapseList();
713    } else if (QUOTATION_PAGE_LINKS.includes(moduleId)) {
714        quotationsPanel?.classList.remove('hidden');
715        if (currentBidsTenderingId) {
716            initQuotations(currentBidsTenderingId, abortController?.signal, {
717                initialTaskId: initialTaskId || null,
718                onNavigate: () => updateUrl(),
719            });
720        }
721        if (!listCollapsed) collapseList();
722    } else if (BID_EVALUATION_PAGE_LINKS.includes(moduleId)) {
723        evaluationPanel?.classList.remove('hidden');
724        if (currentBidsTenderingId) {
725            initBidEvaluations(currentBidsTenderingId, abortController?.signal, {
726                initialTaskId: initialTaskId || null,
727                onNavigate: () => updateUrl(),
728            });
729        }
730        if (!listCollapsed) collapseList();
731    } else if (ACTIVITY_LOG_PAGE_LINKS.includes(moduleId)) {
732        activityLogPanel?.classList.remove('hidden');
733        if (!listCollapsed) collapseList();
734        if (currentBidsTenderingId) {
735            await initProjectActivityLog(currentBidsTenderingId, abortController?.signal, {
736                containerId: 'bt-activity-log-container',
737            });
738            if (!ownsSwitch()) return false;
739        }
740    } else {
741        comingSoonPanel?.classList.remove('hidden');
742
743        // Update coming-soon content with module info
744        const mod = subModules.find(m => m.id === moduleId);
745        if (mod) {
746            const iconEl = document.getElementById('coming-soon-icon');
747            const labelEl = document.getElementById('coming-soon-label');
748            if (iconEl) {
749                iconEl.setAttribute('data-lucide', mod.icon);
750                if (window.lucide) window.lucide.createIcons({ root: iconEl.parentElement });
751            }
752            if (labelEl) {
753                labelEl.textContent = `${mod.label} is being ported to smrtBuild. Stay tuned!`;
754            }
755        }
756
757        // Auto-collapse list when entering a sub-module
758        if (!listCollapsed) collapseList();
759    }
760
761    if (!_navigating) updateUrl(true);
762    return documentsInitPromise;
763}
764
765// ========================================
766// COLLAPSE / EXPAND
767// ========================================
768
769function initToggleHandler() {
770    const toggleBtn = document.getElementById('btn-toggle-list');
771    if (!toggleBtn) return;
772
773    toggleBtn.addEventListener('click', () => {
774        if (listCollapsed) {
775            expandList();
776        } else {
777            collapseList();
778        }
779    }, { signal: abortController.signal });
780
781    // Collapsed sidebar buttons
782    const listColumn = document.getElementById('bids-tendering-list-column');
783    if (listColumn) {
784        listColumn.querySelector('.collapsed-expand-btn')?.addEventListener('click', () => expandList(), { signal: abortController.signal });
785        listColumn.querySelector('.collapsed-add-btn')?.addEventListener('click', () => {
786            document.getElementById('btn-create-bid')?.click();
787        }, { signal: abortController.signal });
788    }
789}
790
791function collapseList() {
792    if (isMobileOrTablet()) return;
793
794    const listColumn = document.getElementById('bids-tendering-list-column');
795    const toggleBtn = document.getElementById('btn-toggle-list');
796    if (!listColumn) return;
797
798    listCollapsed = true;
799    listColumn.classList.add('collapsed');
800
801    if (toggleBtn) {
802        toggleBtn.classList.remove('expanded');
803        toggleBtn.title = 'Expand list';
804    }
805
806    renderMiniBidsTenderingList();
807}
808
809function expandList() {
810    const listColumn = document.getElementById('bids-tendering-list-column');
811    const toggleBtn = document.getElementById('btn-toggle-list');
812    if (!listColumn) return;
813
814    listCollapsed = false;
815    listColumn.classList.remove('collapsed');
816
817    if (toggleBtn) {
818        toggleBtn.classList.add('expanded');
819        toggleBtn.title = 'Collapse list';
820    }
821}
822
823async function renderMiniBidsTenderingList() {
824    const miniContainer = document.getElementById('bids-tendering-list-mini');
825    if (!miniContainer) return;
826
827    if (filteredBidsTenderingItems.length === 0) {
828        miniContainer.innerHTML = '';
829        return;
830    }
831
832    await renderTo(miniContainer, '#tpl-bids-tendering-row-mini', filteredBidsTenderingItems);
833
834    // Highlight selected
835    if (currentBidsTenderingId) {
836        miniContainer.querySelectorAll('.bids-tendering-row-mini').forEach(btn => {
837            const isSelected = parseInt(btn.dataset.id) === currentBidsTenderingId;
838            btn.classList.toggle('bg-st-primary/10', isSelected);
839            btn.classList.toggle('ring-2', isSelected);
840            btn.classList.toggle('ring-st-primary', isSelected);
841            btn.querySelectorAll('span').forEach(s => s.classList.toggle('!text-st-primary', isSelected));
842        });
843    }
844
845    // Click handler for mini list items (delegated)
846    miniContainer.onclick = (e) => {
847        const row = e.target.closest('.bids-tendering-row-mini');
848        if (row) {
849            const id = parseInt(row.dataset.id);
850            settleBidsTenderingAction(selectBidsTendering(id));
851        }
852    };
853}
854
855// ========================================
856// DETAIL LOGIC
857// ========================================
858
859async function settleBidsTenderingAction(promise) {
860    try {
861        return await promise;
862    } catch (error) {
863        if (error?.name !== 'AbortError') console.error('[BidsTendering] Action failed:', error);
864        return false;
865    }
866}
867
868function handleBtOverviewRecordNavigate() {
869    updateUrl(true);
870}
871
872function isOverviewChildTab(tabId) {
873    return tabId === 'bidders' || tabId === 'distribution';
874}
875
876function canonicalizeBtOverviewRoute(projectId, routeGeneration) {
877    if (routeGeneration !== routeOperationGeneration
878        || projectId !== currentBidsTenderingId
879        || activeSubModule !== 'overview') return false;
880    history.replaceState(null, '', buildUrl());
881    return true;
882}
883
884function clearBidsTenderingSelectionTransition() {
885    const statusContainer = document.getElementById('detail-transition-status');
886    statusContainer?.replaceChildren();
887    statusContainer?.classList.add('hidden');
888}
889
890async function beginBidsTenderingSelectionTransition(ownsSelection) {
891    const visiblePanelIds = Array.from(document.querySelectorAll('#detail-content .tab-panel:not(.hidden)'))
892        .map(panel => panel.id)
893        .filter(Boolean);
894    currentProject = null;
895    showDetailPane();
896
897    const detailContent = document.getElementById('detail-content');
898    detailContent?.querySelector('.fast-track-banner')?.remove();
899    document.querySelectorAll('#detail-content .tab-panel').forEach(panel => panel.classList.add('hidden'));
900    document.querySelectorAll('#sub-tab-container >
900 nav[data-parent-tab]').forEach(nav => nav.classList.add('hidden'));
901    document.getElementById('sub-module-tabs')?.classList.add('hidden');
902
903    const headerContainer = document.getElementById('detail-header');
904    headerContainer?.replaceChildren();
905    headerContainer?.classList.remove('header-compact');
906    clearBidsTenderingSelectionTransition();
907    const statusContainer = document.getElementById('detail-transition-status');
908    const statusStaging = document.createElement('div');
909    await renderTo(statusStaging, '#tpl-state-loading', { message: 'Loading selected project…' });
910    if (!ownsSelection() || !statusContainer?.isConnected) return null;
911    statusContainer.replaceChildren(...statusStaging.childNodes);
912    statusContainer.classList.remove('hidden');
913    return { visiblePanelIds, statusContainer };
914}
915
916async function selectBidsTendering(id, {
917    routeOwned = false,
918    targetSubModule = activeSubModule,
919    targetOverviewTab = null,
920    targetOverviewTaskId = null,
921} = {}) {
922    if (!routeOwned) {
923        routeOperationGeneration += 1;
924        _navigating = false;
925    }
926    if (currentBidsTenderingId === id) return { overviewSettled: true };
927    const generation = ++selectionGeneration;
928    const pageSignal = abortController?.signal || null;
929    const ownsSelection = () => generation === selectionGeneration
930        && currentBidsTenderingId === id
931        && abortController?.signal === pageSignal
932        && !pageSignal?.aborted;
933
934    // Update visual selection in full list
935    const listContainer = document.getElementById('bids-tendering-list');
936    listContainer?.querySelectorAll('.bids-tendering-row').forEach(r => {
937        r.classList.remove('border-st-primary', 'bg-st-primary/5');
938    });
939
940    const selectedRow = listContainer?.querySelector(`[data-id="${id}"]`);
941    if (selectedRow) selectedRow.classList.add('border-st-primary', 'bg-st-primary/5');
942
943    currentBidsTenderingId = id;
944    const transitionPromise = beginBidsTenderingSelectionTransition(ownsSelection);
945
946    // Update mini list selection
947    updateMiniListSelection();
948
949    // Sync mobile dropdown navigator
950    DropdownNavigator.setSelected(DROPDOWN_NAV_ID, id);
951
952    // User selection pushes once. Route hydration already owns its URL and writes nothing.
953    if (!_navigating) updateUrl(true);
954
955    try {
956        const transition = await transitionPromise;
957        if (!transition || !ownsSelection()) return false;
958        const activityWasActive = ACTIVITY_LOG_PAGE_LINKS.includes(activeSubModule);
959        if (activityWasActive && ACTIVITY_LOG_PAGE_LINKS.includes(targetSubModule)) {
960            await initProjectActivityLog(id, pageSignal, { containerId: 'bt-activity-log-container' });
961        } else if (activityWasActive) {
962            await destroyProjectActivityLog();
963        }
964        if (!ownsSelection()) return false;
965
966        // Show detail pane immediately with list data (fast preview)
967        const routeOutcome = { overviewSettled: null };
968        const listItem = bidsTenderingItems.find(p => p.projectId === id);
969        if (listItem && !await renderBidsTenderingDetail(listItem, {
970            generation, id, pageSignal, targetSubModule, transition,
971            targetOverviewTab, targetOverviewTaskId, mountOverview: false, routeOutcome,
972        })) return false;
973        if (!ownsSelection()) return false;
974        showDetailPane();
975
976        // Fetch full detail from API
977        const fullProject = await projectsApi.getProject(id, { signal: pageSignal });
978        if (!ownsSelection()) return false;
979        enrichBidsTenderingData(fullProject);
980        if (!await renderBidsTenderingDetail(fullProject, {
981            generation, id, pageSignal, targetSubModule, transition,
982            targetOverviewTab, targetOverviewTaskId, mountOverview: true, routeOutcome,
983        })) return false;
984        return { overviewSettled: routeOutcome.overviewSettled };
985    } catch (error) {
986        if (error.name === 'AbortError' || !ownsSelection()) return false;
987        window.showToast?.('Failed to load project', 'error', { description: error.message });
988        hideDetailPane();
989        updateUrl();
990        return false;
991    }
992}
993
994function updateMiniListSelection() {
995    const miniContainer = document.getElementById('bids-tendering-list-mini');
996    if (!miniContainer) return;
997
998    miniContainer.querySelectorAll('.bids-tendering-row-mini').forEach(btn => {
999        const isSelected = parseInt(btn.dataset.id) === currentBidsTenderingId;
1000        btn.classList.toggle('bg-st-primary/10', isSelected);
1001        btn.classList.toggle('ring-2', isSelected);
1002        btn.classList.toggle('ring-st-primary', isSelected);
1003    });
1004}
1005
1006/**
1007 * Enrich project data with computed fields
1008 */
1009function enrichBidsTenderingData(project) {
1010    if (!project) return;
1011
1012    // Normalize field names — list query uses shorter names than detail query
1013    if (project.anticipatedConStart && !project.anticipatedConstructionStart) {
1014        project.anticipatedConstructionStart = project.anticipatedConStart;
1015    }
1016    if (project.conCompletion && !project.constructionCompletion) {
1017        project.constructionCompletion = project.conCompletion;
1018    }
1019
1020    if (project.projectEmail) {
1021        const company = auth.getCompany();
1022        project.projectEmailWithDomain = company?.subdomain
1023            ? `${project.projectEmail}@${company.subdomain}`
1024            : project.projectEmail;
1025    }
1026
1027    // Build milestone cards for the rich detail header
1028    const datedMilestones = (project.milestones || []).filter(m => m.projectDate);
1029    if (datedMilestones.length > 0) {
1030        project.milestoneCards = datedMilestones.slice(0, 4).map(m => ({
1031            label: m.typeText,
1032            date: m.projectDate
1033        }));
1034    } else {
1035        project.milestoneCards = [
1036            { label: 'Tender Due', date: project.tenderDue },
1037            { label: 'Award Date', date: project.anticipatedAwardDate },
1038            { label: 'Construction Start', date: project.constructionStartDate },
1039            { label: 'Completion', date: project.constructionCompletion }
1040        ].filter(m => m.date);
1041    }
1042}
1043
1044/** Re-fetch divisions after a header manage-modal save; re-render header subtitle. */
1045async function refreshBidsTenderingDivisions() {
1046    if (!currentProject?.projectId) return;
1047    const fresh = await projectsApi.getProject(currentProject.projectId);
1048    Object.assign(currentProject, {
1049        divisions: fresh.divisions, assignedDivisionIds: fresh.assignedDivisionIds,
1050        divisionNames: fresh.divisionNames, divisionName: fresh.divisionName,
1051        usesDivisions: companySettings.usesDivisions(),
1052    });
1053    const headerContainer = document.getElementById('detail-header');
1054    if (headerContainer) {
1055        await renderTo(headerContainer, '#tpl-bids-tendering-header', currentProject, { canEdit: !!currentProject.canEdit });
1056        document.getElementById('btn-edit-bid')?.addEventListener('click', () => handleEditBidsTendering(currentProject), { signal: abortController.signal });
1057        document.getElementById('btn-archive-bid')?.addEventListener('click', () => handleArchiveBidsTendering(currentProject.projectId), { signal: abortController.signal });
1058        if (activeSubModule !== 'overview') headerContainer.classList.add('header-compact');
1059    }
1060}
1061
1062async function renderBidsTenderingDetail(project, {
1063    generation, id, pageSignal, targetSubModule, transition,
1064    targetOverviewTab = null, targetOverviewTaskId = null,
1065    mountOverview = true, routeOutcome = null,
1066}) {
1067    const ownsSelection = () => generation === selectionGeneration
1068        && currentBidsTenderingId === id
1069        && abortController?.signal === pageSignal
1070        && !pageSignal?.aborted;
1071    if (!ownsSelection()) return false;
1072    project.usesDivisions = companySettings.usesDivisions();
1073    const emptyState = document.getElementById('detail-empty');
1074    const content = document.getElementById('detail-content');
1075    const headerContainer = document.getElementById('detail-header');
1076    const detailContent = document.getElementById('detail-content');
1077    const tabContainer = document.getElementById('sub-module-tabs');
1078    const headerStaging = document.createElement('div');
1079    await renderTo(headerStaging, '#tpl-bids-tendering-header', project, { canEdit: !!project.canEdit });
1080    if (!ownsSelection()) return false;
1081
1082    const bannerStaging = document.createElement('div');
1083    if (project.projectId) {
1084        await FastTrackBanner.check(bannerStaging, project.projectId, { signal: pageSignal });
1085        if (!ownsSelection()) return false;
1086    }
1087    const visibleTabs = await offlineApi.filterSubModulesToOffline(subModules, project?.projectId);
1088    if (!ownsSelection() || !headerContainer?.isConnected || !detailContent?.isConnected || !tabContainer?.isConnected) return false;
1089
1090    currentProject = project;
1091    if (emptyState) emptyState.classList.add('hidden');
1092    if (content) content.classList.remove('hidden');
1093    content?.removeAttribute('aria-busy');
1094    clearBidsTenderingSelectionTransition();
1095    headerContainer.replaceChildren(...headerStaging.childNodes);
1096    headerContainer.classList.toggle('header-compact', activeSubModule !== 'overview');
1097    detailContent.querySelector('.fast-track-banner')?.remove();
1098    const banner = bannerStaging.querySelector('.fast-track-banner');
1099    if (banner) detailContent.prepend(banner);
1100    tabContainer.classList.remove('hidden');
1101    renderSubModuleTabs(visibleTabs, activeSubModule, 'bids-tendering');
1102    if (targetSubModule === activeSubModule) {
1103        transition?.visiblePanelIds?.forEach(panelId => document.getElementById(panelId)?.classList.remove('hidden'));
1104    }
1105
1106    document.getElementById('btn-edit-bid')?.addEventListener('click', () => handleEditBidsTendering(project), { signal: abortController.signal });
1107    document.getElementById('btn-archive-bid')?.addEventListener('click', () => handleArchiveBidsTendering(project.projectId), { signal: abortController.signal });
1108
1109    // Show sub-tab nav for the active main tab
1110    document.querySelectorAll('#sub-tab-container > nav[data-parent-tab]').forEach(nav => {
1111        nav.classList.toggle('hidden', nav.dataset.parentTab !== activeSubModule);
1112    });
1113
1114    // Initialize overview sub-tabs (Dashboard, Bidders, Procurement, Distribution)
1115    if (project.projectId && targetSubModule === 'overview' && activeSubModule === 'overview' && mountOverview) {
1116        const overviewSettled = await initBtOverviewDetails(project.projectId, abortController?.signal, {
1117            initialTab: targetOverviewTab || undefined,
1118            initialTaskId: targetOverviewTaskId,
1119            onTabChange: handleBtOverviewTabChange,
1120            onTaskNavigate: handleBtOverviewRecordNavigate,
1121            projectData: project,
1122        });
1123        if (!ownsSelection()) return false;
1124        if (routeOutcome) routeOutcome.overviewSettled = overviewSettled;
1125    }
1126
1127    // If Project Team tab is active, re-init with current project
1128    if (TEAM_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1129        initProjectTeam(project.projectId, abortController?.signal);
1130    }
1131
1132    // If Project Documents tab is active, re-init with current project
1133    if (DOCUMENTS_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1134        await initProjectDocuments(project.projectId, abortController?.signal, { onNavigate: ({ push = false } = {}) => updateUrl(push) });
1135        if (!ownsSelection()) return false;
1136    }
1137
1138    // If Meetings tab is active, re-init with current project
1139    if (MEETINGS_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1140        initProjectMeetings(project.projectId, abortController?.signal, {
1141            onNavigate: () => updateUrl(),
1142        });
1143    }
1144
1145    // If any Bids sub-module tab is active, re-init with current project
1146    if (BID_TENDER_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1147        initBidTenders(project.projectId, abortController?.signal, { onNavigate: () => updateUrl() });
1148    }
1149    if (TENDER_CLARIFICATION_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1150        initTenderClarifications(project.projectId, abortController?.signal, { onNavigate: () => updateUrl() });
1151    }
1152    if (BIDS_CONTRACT_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1153        initBidsContracts(project.projectId, abortController?.signal, { onNavigate: () => updateUrl() });
1154    }
1155    if (QUOTATION_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1156        initQuotations(project.projectId, abortController?.signal, { onNavigate: () => updateUrl() });
1157    }
1158    if (BID_EVALUATION_PAGE_LINKS.includes(activeSubModule) && project.projectId) {
1159        initBidEvaluations(project.projectId, abortController?.signal, { onNavigate: () => updateUrl() });
1160    }
1161
1162    // Update mini list selection if collapsed
1163    if (listCollapsed) {
1164        updateMiniListSelection();
1165    }
1166    return ownsSelection();
1167}
1168
1169// ========================================
1170// MOBILE SUPPORT
1171// ========================================
1172
1173function showDetailPane() {
1174    const emptyState = document.getElementById('detail-empty');
1175    const detailContent = document.getElementById('detail-content');
1176    const listColumn = document.getElementById('bids-tendering-list-column');
1177    const detailColumn = document.getElementById('bids-tendering-detail-column');
1178    const mobileBackHeader = document.getElementById('bids-tendering-mobile-back-header');
1179
1180    if (emptyState) emptyState.classList.add('hidden');
1181    if (detailContent) detailContent.classList.remove('hidden');
1182
1183    if (isMobileOrTablet()) {
1184        if (listColumn) listColumn.classList.add('hidden');
1185        if (detailColumn) {
1186            detailColumn.classList.remove('hidden', 'lg:flex');
1187            detailColumn.classList.add('flex', 'w-full');
1188        }
1189        if (mobileBackHeader) mobileBackHeader.classList.remove('hidden');
1190    }
1191}
1192
1193function hideDetailPane() {
1194    selectionGeneration++;
1195    void destroyProjectActivityLog();
1196    clearBidsTenderingSelectionTransition();
1197    const emptyState = document.getElementById('detail-empty');
1198    const detailContent = document.getElementById('detail-content');
1199
1200    if (emptyState) emptyState.classList.remove('hidden');
1201    if (detailContent) detailContent.classList.add('hidden');
1202
1203    currentBidsTenderingId = null;
1204    currentProject = null;
1205}
1206
1207function showMobileList() {
1208    selectionGeneration++;
1209    void destroyProjectActivityLog();
1210    clearBidsTenderingSelectionTransition();
1211    const listColumn = document.getElementById('bids-tendering-list-column');
1212    const detailColumn = document.getElementById('bids-tendering-detail-column');
1213    const mobileBackHeader = document.getElementById('bids-tendering-mobile-back-header');
1214    const emptyState = document.getElementById('detail-empty');
1215    const detailContent = document.getElementById('detail-content');
1216
1217    if (listColumn) listColumn.classList.remove('hidden');
1218
1219    if (detailColumn) {
1220        detailColumn.classList.remove('flex', 'w-full');
1221        detailColumn.classList.add('hidden', 'lg:flex');
1222    }
1223
1224    if (mobileBackHeader) mobileBackHeader.classList.add('hidden');
1225    if (emptyState) emptyState.classList.remove('hidden');
1226    if (detailContent) detailContent.classList.add('hidden');
1227
1228    document.querySelectorAll('#bids-tendering-list .bids-tendering-row').forEach(card => {
1229        card.classList.remove('border-st-primary', 'bg-st-primary/5');
1230    });
1231
1232    currentBidsTenderingId = null;
1233    activeSubModule = 'overview';
1234    updateUrl(true);
1235}
1236
1237function initMobileBackHandler() {
1238    const backBtn = document.getElementById('btn-bids-tendering-mobile-back');
1239    if (backBtn) {
1240        backBtn.addEventListener('click', showMobileList, { signal: abortController.signal });
1241    }
1242}
1243
1244// ========================================
1245// DROPDOWN NAVIGATOR (Mobile < 768px)
1246// ========================================
1247
1248const DROPDOWN_NAV_ID = 'bidstendering-dropdown-nav';
1249
1250function initDropdownNavigator() {
1251    const container = document.getElementById(DROPDOWN_NAV_ID);
1252    if (!container) return;
1253
1254    // Only show on mobile
1255    if (isMobile()) {
1256        container.classList.remove('hidden');
1257    }
1258
1259    DropdownNavigator.init({
1260        containerId: DROPDOWN_NAV_ID,
1261        items: buildDropdownItems(),
1262        selectedId: currentBidsTenderingId,
1263        onSelect: (id) => settleBidsTenderingAction(selectBidsTendering(id)),
1264        onNew: () => page('/phase-gateway/BIDS'),
1265        newLabel: 'New Bid',
1266        emptyLabel: 'No bids found',
1267        moduleTitle: 'Bids & Tendering',
1268        moduleIcon: 'gavel'
1269    });
1270}
1271
1272function syncDropdownNavigator() {
1273    DropdownNavigator.setItems(DROPDOWN_NAV_ID, buildDropdownItems());
1274    DropdownNavigator.setSelected(DROPDOWN_NAV_ID, currentBidsTenderingId);
1275}
1276
1277function buildDropdownItems() {
1278    return filteredBidsTenderingItems.map(p => ({
1279        id: p.projectId,
1280        badge: p.projectNumber || null,
1281        name: p.projectTitle || 'Untitled',
1282        subtitle: p.projectCity || p.divisionName || null
1283    }));
1284}
1285
1286// ========================================
1287// CRUD ACTIONS
1288// ========================================
1289
1290function handleCreateBidsTendering() {
1291    page('/phase-gateway/BIDS');
1292}
1293
1294function handleEditBidsTendering(project) {
1295    page(`/bids-tendering/${project.projectId}/edit`);
1296}
1297
1298async function handleArchiveBidsTendering(id) {
1299    await confirm({
1300        title: 'Archive this project?',
1301        message: 'The project will be moved to the archive.',
1302        icon: 'archive',
1303        confirmText: 'Archive',
1304        confirmClass: 'btn-st-error',
1305        async onConfirm() {
1306            await projectsApi.archiveProject(id);
1307            window.showToast?.('Project archived', 'success');
1308            await loadBidsTenderingItems();
1309            hideDetailPane();
1310        }
1311    });
1312}
1313
1314// ========================================
1315// Exports
1316// ========================================
1317export { selectBidsTendering, hideDetailPane, showMobileList };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.