PageSourceSearch

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

js smrtbld.ai collected 2026-10-05 15:07:51 UTC 9,523 bytes, 262 lines download raw bytes

1/**
2 * bids-tendering-edit.js — Full-page "Edit Bids & Tendering" form
3 *
4 * Reuses the bids-tendering-create.html template (with isEdit=true conditionals).
5 * Key differences from bids-tendering-create.js:
6 * - No draft persistence (no localStorage, no debounce, no DRAFT_KEY)
7 * - Pre-populates all fields from loaded project data
8 * - Submit calls PUT (updateBidsTendering) instead of POST (createBidsTendering)
9 * - Navigation goes to /bids-tendering/{id} (not /bids-tendering)
10 * - Dirty tracking via simple isDirty flag
11 */
12
13import { projectsApi } from '../api/projects.js';
14import { confirm } from '../utils/modal.js';
15import { SearchableDropdown } from '../components/SearchableDropdown.js';
16import { DatePicker } from '../components/DatePicker.js';
17import { setLoading } from '../utils/dom.js';
18import { validateForm, clearError } from '../utils/form-validation.js';
19// CA-130 — both pages render `bids-tendering-create.html`, so the numeric rule set lives in the
20// create module and is imported here rather than copied. Same shape CA-131 used for
21// `ownsNumericRule`; the create module has no top-level side effects.
22import { bidsTenderingNumericRules, ownsBidsTenderingNumericRule } from './bids-tendering-create.js';
23import { populateEditForm } from '../utils/form-population.js';
24import { DraftManager } from '../utils/draft-manager.js';
25
26let abortController = null;
27let isDirty = false;
28let container = null;
29let currentId = null;
30let draft = null;
31
32// ========================================
33// PUBLIC API
34// ========================================
35
36export function initBidsTenderingEditPage(id, addOptions, data) {
37    abortController = new AbortController();
38    const signal = abortController.signal;
39    container = document.getElementById('bt-create-page');
40    currentId = id;
41    if (!container) return;
42
43    // Pre-populate form with existing data
44    populateForm(data, addOptions);
45
46    // Edit-backup: any in-flight edits from a prior session override the
47    // server values. Key is scoped per-record. Restore AFTER populateForm —
48    // draft wins so user edits aren't clobbered.
49    draft = new DraftManager({
50        key: `sb_draft_bids_tendering_edit_${id}`,
51        container,
52        statusSelector: '#bt-draft-status',
53        DatePicker,
54        collectFn: () => collectFormData(),
55        onRestore: (data) => {
56            if (data.preBidMeetingRequired) {
57                const hidden = container.querySelector('#bt-pre-bid-meeting');
58                if (hidden) hidden.value = data.preBidMeetingRequired;
59                updatePreBidMeetingUI(data.preBidMeetingRequired);
60            }
61            isDirty = true;   // a restored backup IS dirty work
62        },
63    });
64    draft.restore();
65
66    // Wire events
67    wireFormEvents(signal);
68    wirePreBidMeetingGroup(signal);
69    wireBackButton(signal);
70    wireCancelButton(signal);
71    wireSubmitButton(signal);
72
73    // Navigation guard
74    window.addEventListener('beforeunload', onBeforeUnload);
75}
76
77export function destroyBidsTenderingEditPage() {
78    window.removeEventListener('beforeunload', onBeforeUnload);
79
80    draft?.destroy();
81    draft = null;
82
83    if (container) {
84        SearchableDropdown.destroyAll(container);
85    }
86
87    abortController?.abort();
88    abortController = null;
89    container = null;
90    isDirty = false;
91    currentId = null;
92}
93
94// ========================================
95// FORM PRE-POPULATION
96// ========================================
97
98function populateForm(data, addOptions) {
99    if (!data || !container) return;
100
101    populateEditForm(container, data, {
102        dateFields: ['tenderDue', 'anticipatedAwardDate', 'constructionStartDate', 'constructionCompletion'],
103        currencyFields: ['constructionEstimate'],
104    });
105
106    // Pre-bid meeting segmented button
107    if (data.preBidMeetingRequired) {
108        const hidden = container.querySelector('#bt-pre-bid-meeting');
109        if (hidden) hidden.value = data.preBidMeetingRequired;
110        updatePreBidMeetingUI(data.preBidMeetingRequired);
111    }
112}
113
114// ========================================
115// FORM DATA COLLECTION
116// ========================================
117
118function collectFormData() {
119    const data = {};
120    if (!container) return data;
121
122    container.querySelectorAll('input[name], textarea[name]').forEach(el => {
123        data[el.name] = el.value;
124    });
125
126    container.querySelectorAll('select[name]').forEach(el => {
127        data[el.name] = el.value;
128    });
129
130    const hidden = container.querySelector('#bt-pre-bid-meeting');
131    if (hidden) data.preBidMeetingRequired = hidden.value;
132
133    return data;
134}
135
136// ========================================
137// EVENT WIRING
138// ========================================
139
140function wireFormEvents(signal) {
141    container.addEventListener('input', (e) => {
142        isDirty = true;
143        draft?.scheduleSave();
144        if (e.target.hasAttribute('data-required') && !ownsBidsTenderingNumericRule(e.target)) clearError(container, e.target);
145    }, { signal });
146
147    container.addEventListener('change', (e) => {
148        isDirty = true;
149        draft?.scheduleSave();
150        if (e.target.hasAttribute('data-required') && !ownsBidsTenderingNumericRule(e.target)) clearError(container, e.target);
151    }, { signal });
152}
153
154function wirePreBidMeetingGroup(signal) {
155    container.querySelectorAll('.prebid-seg-btn').forEach(btn => {
156        btn.addEventListener('click', () => {
157            const value = btn.dataset.value;
158            const hidden = container.querySelector('#bt-pre-bid-meeting');
159            if (hidden) hidden.value = value;
160            updatePreBidMeetingUI(value);
161            isDirty = true;
162            draft?.scheduleSave();
163        }, { signal });
164    });
165}
166
167function updatePreBidMeetingUI(activeValue) {
168    container.querySelectorAll('.prebid-seg-btn').forEach(btn => {
169        btn.classList.toggle('active', btn.dataset.value === activeValue);
170    });
171}
172
173function wireBackButton(signal) {
174    container.querySelector('#btn-bt-back')?.addEventListener('click', () => {
175        handleCancel();
176    }, { signal });
177}
178
179function wireCancelButton(signal) {
180    container.querySelector('#btn-bt-cancel')?.addEventListener('click', () => {
181        handleCancel();
182    }, { signal });
183}
184
185async function handleCancel() {
186    if (isDirty) {
187        const confirmed = await confirm({
188            title: 'Discard unsaved changes?',
189            message: 'Any changes you made will be lost.',
190            icon: 'trash-2',
191            confirmText: 'Discard',
192            confirmClass: 'btn-st-error',
193            // Page signal → the confirm auto-resolves false and closes on teardown,
194            // so it isn't orphaned over the next page.
195            signal: abortController?.signal,
196        });
197        if (!confirmed) return;
198        draft?.clear();
199    }
200    page(`/bids-tendering/${currentId}`);
201}
202
203function wireSubmitButton(signal) {
204    const btn = container.querySelector('#btn-bt-submit');
205    if (!btn) return;
206
207    btn.addEventListener('click', () => handleSubmit(btn), { signal });
208}
209
210async function handleSubmit(btn) {
211    // CA-130 — before the loading state and before any mapping; see the create module's note.
212    // CA-146 — one call, one round: presence, format and the numeric rules together.
213    if (!validateForm(container, bidsTenderingNumericRules(container))) return;
214
215    setLoading(btn, true, 'Saving...');
216
217    try {
218        const data = collectFormData();
219
220        await projectsApi.updateBidsTendering(currentId, {
221            projectTitle: data.projectTitle,
222            projectNumber: data.projectNumber || null,
223            clientReferenceNumber: data.clientReferenceNumber || null,
224            projectEmail: data.projectEmail,
225            // Exact `Number` semantics after the gate (intake step 6); `ConstructionEstimate` is a
226            // non-nullable `decimal` and the control is `data-required`, so the 0 branch is now
227            // unreachable rather than a silent fallback for an unreadable entry.
228            constructionEstimate: data.constructionEstimate?.trim() ? Number(data.constructionEstimate) : 0,
229            scopeOfWork: data.scopeOfWork || null,
230            tenderDue: data.tenderDue || null,
231            anticipatedAwardDate: data.anticipatedAwardDate || null,
232            constructionStartDate: data.constructionStartDate || null,
233            constructionCompletion: data.constructionCompletion || null,
234            linkedDesignDevId: data.linkedDesignDevId ? parseInt(data.linkedDesignDevId) : null,
235            biddingMethod: data.biddingMethod || null,
236            contractType: data.contractType || null,
237            numberOfBidSets: data.numberOfBidSets?.trim() ? Number(data.numberOfBidSets) : null,
238            preBidMeetingRequired: data.preBidMeetingRequired || null
239        });
240
241        isDirty = false;
242        draft?.clear();
243        window.showToast?.('Bid & tendering updated', 'success', { description: data.projectTitle });
244        page(`/bids-tendering/${currentId}`);
245    } catch (error) {
246        if (error.name === 'AbortError') return;
247        window.showToast?.('Failed to update project', 'error', { description: error.message });
248        setLoading(btn, false);
249    }
250}
251
252// ========================================
253// NAVIGATION GUARD
254// ========================================
255
256function onBeforeUnload(e) {
257    if (isDirty) {
258        draft?.save();          // synchronous localStorage write — survives browser close
259        e.preventDefault();
260        e.returnValue = '';
261    }
262}

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.