PageSourceSearch

https://smrtbld.ai/js/pages/design-dev-edit.js

js smrtbld.ai collected 2026-10-05 15:07:49 UTC 8,047 bytes, 230 lines download raw bytes

1/**
2 * design-dev-edit.js — Full-page "Edit Design & Dev" form
3 *
4 * Reuses the design-dev-create.html template (with isEdit=true conditionals).
5 * Key differences from design-dev-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 (updateDesignDev) instead of POST (createDesignDev)
9 * - Navigation goes to /design-dev/{id} (not /design-dev)
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-132: one rule for one column. The create twin owns the derivation comment.
20import { DESIGNER_BUDGET } from './design-dev-create.js';
21import { populateEditForm } from '../utils/form-population.js';
22import { DraftManager } from '../utils/draft-manager.js';
23
24let abortController = null;
25let isDirty = false;
26let container = null;
27let currentId = null;
28let draft = null;
29
30// ========================================
31// PUBLIC API
32// ========================================
33
34export function initDesignDevEditPage(id, addOptions, data) {
35    abortController = new AbortController();
36    const signal = abortController.signal;
37    container = document.getElementById('dd-create-page');
38    currentId = id;
39    if (!container) return;
40
41    // Pre-populate form with existing data
42    populateForm(data, addOptions);
43
44    // Edit-backup: any in-flight edits from a prior session override the
45    // server values. Key is scoped per-record. Restore AFTER populateForm —
46    // draft wins so user edits aren't clobbered.
47    draft = new DraftManager({
48        key: `sb_draft_design_dev_edit_${id}`,
49        container,
50        statusSelector: '#dd-draft-status',
51        DatePicker,
52        collectFn: () => collectFormData(),
53        onRestore: () => {
54            isDirty = true;   // a restored backup IS dirty work
55        },
56    });
57    draft.restore();
58
59    // Wire events
60    wireFormEvents(signal);
61    wireBackButton(signal);
62    wireCancelButton(signal);
63    wireSubmitButton(signal);
64
65    // Navigation guard
66    window.addEventListener('beforeunload', onBeforeUnload);
67}
68
69export function destroyDesignDevEditPage() {
70    window.removeEventListener('beforeunload', onBeforeUnload);
71
72    draft?.destroy();
73    draft = null;
74
75    if (container) {
76        SearchableDropdown.destroyAll(container);
77    }
78
79    abortController?.abort();
80    abortController = null;
81    container = null;
82    isDirty = false;
83    currentId = null;
84}
85
86// ========================================
87// FORM PRE-POPULATION
88// ========================================
89
90function populateForm(data, addOptions) {
91    if (!data || !container) return;
92
93    populateEditForm(container, data, {
94        dateFields: ['submissionDueDate', 'anticipatedAwardDate', 'anticipatedConstructionStart', 'constructionCompletion'],
95        currencyFields: ['projectBudget'],
96    });
97}
98
99// ========================================
100// FORM DATA COLLECTION
101// ========================================
102
103function collectFormData() {
104    const data = {};
105    if (!container) return data;
106
107    container.querySelectorAll('input[name], textarea[name]').forEach(el => {
108        data[el.name] = el.value;
109    });
110
111    container.querySelectorAll('select[name]').forEach(el => {
112        data[el.name] = el.value;
113    });
114
115    return data;
116}
117
118// ========================================
119// EVENT WIRING
120// ========================================
121
122function wireFormEvents(signal) {
123    container.addEventListener('input', (e) => {
124        isDirty = true;
125        draft?.scheduleSave();
126        if (e.target.hasAttribute('data-required')) clearError(container, e.target);
127    }, { signal });
128
129    container.addEventListener('change', (e) => {
130        isDirty = true;
131        draft?.scheduleSave();
132        if (e.target.hasAttribute('data-required')) clearError(container, e.target);
133    }, { signal });
134}
135
136function wireBackButton(signal) {
137    container.querySelector('#btn-dd-back')?.addEventListener('click', () => {
138        handleCancel();
139    }, { signal });
140}
141
142function wireCancelButton(signal) {
143    container.querySelector('#btn-dd-cancel')?.addEventListener('click', () => {
144        handleCancel();
145    }, { signal });
146}
147
148async function handleCancel() {
149    if (isDirty) {
150        const confirmed = await confirm({
151            title: 'Discard unsaved changes?',
152            message: 'Any changes you made will be lost.',
153            icon: 'trash-2',
154            confirmText: 'Discard',
155            confirmClass: 'btn-st-error',
156            // Page signal → the confirm auto-resolves false and closes on teardown,
157            // so it isn't orphaned over the next page.
158            signal: abortController?.signal,
159        });
160        if (!confirmed) return;
161        draft?.clear();
162    }
163    page(`/design-dev/${currentId}`);
164}
165
166function wireSubmitButton(signal) {
167    const btn = container.querySelector('#btn-dd-submit');
168    if (!btn) return;
169
170    btn.addEventListener('click', () => handleSubmit(btn), { signal });
171}
172
173async function handleSubmit(btn) {
174    // CA-132 — before the loading state, before the map, before the PUT. The stored value cannot
175    // fail this rule: `becc_pm_projects_designer.Budget` is `decimal(10,2)`, so a reload of an
176    // untouched record is never blocked by it.
177    // CA-146 — the numeric rule rides `validateForm`'s own pass, so a missing required field and an
178    // unreadable budget are marked, summarised and announced in ONE submit round.
179    if (!validateForm(container, [
180        { el: container.querySelector('#dd-budget'), label: 'Project Budget', required: false, numeric: DESIGNER_BUDGET },
181    ])) return;
182
183    setLoading(btn, true, 'Saving...');
184
185    try {
186        const data = collectFormData();
187
188        await projectsApi.updateDesignDev(currentId, {
189            projectTitle: data.projectTitle,
190            projectNumber: data.projectNumber || null,
191            clientReferenceNumber: data.clientReferenceNumber || null,
192            projectEmail: data.projectEmail,
193            // CA-132: exact `Number`, not `parseFloat`. Blank contract unchanged and stated —
194            // `UpdateDesignDevRequest.ProjectBudget` is a NON-nullable `decimal`, so a blank box
195            // has always meant 0 here.
196            projectBudget: data.projectBudget ? Number(data.projectBudget) : 0,
197            scopeOfWork: data.scopeOfWork || null,
198            submissionDueDate: data.submissionDueDate || null,
199            anticipatedAwardDate: data.anticipatedAwardDate || null,
200            anticipatedConstructionStart: data.anticipatedConstructionStart || null,
201            constructionCompletion: data.constructionCompletion || null,
202            linkedProposalId: data.linkedProposalId ? parseInt(data.linkedProposalId) : null,
203            currentPhase: data.currentPhase || null,
204            deliveryMethod: data.deliveryMethod || null,
205            buildingClassification: data.buildingClassification || null,
206            constructionType: data.constructionType || null
207        });
208
209        isDirty = false;
210        draft?.clear();
211        window.showToast?.('Design & Dev updated', 'success', { description: data.projectTitle });
212        page(`/design-dev/${currentId}`);
213    } catch (error) {
214        if (error.name === 'AbortError') return;
215        window.showToast?.('Failed to update project', 'error', { description: error.message });
216        setLoading(btn, false);
217    }
218}
219
220// ========================================
221// NAVIGATION GUARD
222// ========================================
223
224function onBeforeUnload(e) {
225    if (isDirty) {
226        draft?.save();          // synchronous localStorage write — survives browser close
227        e.preventDefault();
228        e.returnValue = '';
229    }
230}

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.