1/** 2 * pre-construction-create.js â Full-page "New Pre-Construction" form 3 * 4 * Mirrors contract-admin-create.js, leaner (precon has no dates / billing / parties): 5 * - Draft persistence (localStorage auto-save) 6 * - Design Bible validation (red borders, scroll-to-first) 7 * - Navigation guard (beforeunload) 8 * 9 * Shares the pre-construction-create.html template with pre-construction-edit.js (isEdit flag). 10 */ 11 12import { preConstructionApi } from '../api/pre-construction.js'; 13import { confirm } from '../utils/modal.js'; 14import { SearchableDropdown } from '../components/SearchableDropdown.js'; 15import { DatePicker } from '../components/DatePicker.js'; 16import { setLoading } from '../utils/dom.js'; 17import { validateForm, clearError, resetForm } from '../utils/form-validation.js'; 18import { DraftManager } from '../utils/draft-manager.js'; 19 20let abortController = null; 21let container = null; 22let draft = null; 23 24// ======================================== 25// PUBLIC API 26// ======================================== 27 28export function initPreConstructionCreatePage(addOptions) { 29 abortController = new AbortController(); 30 const signal = abortController.signal; 31 container = document.getElementById('precon-create-page'); 32 if (!container) return; 33 34 // Draft persistence 35 draft = new DraftManager({ 36 key: 'sb_precon_draft', 37 container, 38 statusSelector: '#precon-draft-status', 39 DatePicker, 40 collectFn: () => collectFormData(), 41 }); 42 draft.restore(); 43 44 // Wire events 45 wireFormEvents(signal); 46 wireBackButton(signal); 47 wireCancelButton(signal); 48 wireClearButton(signal); 49 wireSubmitButton(signal); 50 51 // Navigation guard 52 window.addEventListener('beforeunload', onBeforeUnload); 53} 54 55export function destroyPreConstructionCreatePage() { 56 draft?.destroy(); 57 draft = null; 58 59 window.removeEventListener('beforeunload', onBeforeUnload); 60 61 if (container) SearchableDropdown.destroyAll(container); 62 63 abortController?.abort(); 64 abortController = null; 65 container = null; 66} 67 68// ======================================== 69// FORM DATA COLLECTION + DRAFT 70// ======================================== 71 72function collectFormData() { 73 const data = {}; 74 if (!container) return data; 75 container.querySelectorAll('input[name], textarea[name], select[name]').forEach(el => { 76 data[el.name] = el.value; 77 }); 78 return data; 79} 80 81function scheduleDraftSave() { draft?.scheduleSave(); } 82function clearDraft() { draft?.clear(); } 83 84// ======================================== 85// EVENT WIRING 86// ======================================== 87 88function wireFormEvents(signal) { 89 container.addEventListener('input', (e) => { 90 scheduleDraftSave(); 91 if (e.target.hasAttribute('data-required')) clearError(container, e.target); 92 }, { signal }); 93 94 container.addEventListener('change', (e) => { 95 scheduleDraftSave(); 96 if (e.target.hasAttribute('data-required')) clearError(container, e.target); 97 }, { signal }); 98} 99 100function wireBackButton(signal) { 101 container.querySelector('#btn-precon-back')?.addEventListener('click', () => handleCancel(), { signal }); 102} 103 104function wireCancelButton(signal) { 105 container.querySelector('#btn-precon-cancel')?.addEventListener('click', () => handleCancel(), { signal }); 106} 107 108async function handleCancel() { 109 if (draft?.isDirty) { 110 const confirmed = await confirm({ 111 title: 'Discard unsaved changes?', 112 message: 'Your draft will be discarded. This cannot be undone.', 113 icon: 'trash-2', 114 confirmText: 'Discard', 115 confirmClass: 'btn-st-error', 116 // Page signal â the confirm auto-resolves false and closes on teardown, 117 // so it isn't orphaned over the next page. 118 signal: abortController?.signal, 119 }); 120 if (!confirmed) return; 121 clearDraft(); 122 } 123 page('/pre-construction'); 124} 125 126function wireClearButton(signal) { 127 container.querySelector('#btn-precon-clear')?.addEventListener('click', () => handleClearForm(signal), { signal }); 128} 129 130async function handleClearForm(signal) { 131 const originController = abortController; 132 const originContainer = container; 133 const originDraft = draft; 134 const cleared = await resetForm(originContainer, originDraft?.key, { 135 DatePicker, 136 SearchableDropdown, 137 signal, 138 }); 139 if (!cleared) return; 140 if (signal?.aborted || abortController !== originController || container !== originContainer || draft !== originDraft) return; 141 originDraft?.cancelScheduledSave(); 142 if (originDraft) originDraft.isDirty = false;
143 originDraft?.showStatus('Form cleared'); 144} 145 146function wireSubmitButton(signal) { 147 const btn = container.querySelector('#btn-precon-submit'); 148 if (!btn) return; 149 btn.addEventListener('click', () => handleSubmit(btn), { signal }); 150} 151 152async function handleSubmit(btn) { 153 if (!validateForm(container)) return; 154 155 setLoading(btn, true, 'Creating...'); 156 157 try { 158 const data = collectFormData(); 159 160 // Form field names use the update-API shape; the create endpoint takes short names. 161 const result = await preConstructionApi.createPreConstructionProject({ 162 title: data.projectTitle, 163 description: data.description || null, 164 address: data.projectAddress || null, 165 city: data.projectCity || null, 166 province: data.projectProvince || null, 167 country: data.projectCountry || null, 168 systemTimeZoneID: data.systemTimeZoneID || null, 169 projectEmail: data.projectEmail || null, 170 }); 171 172 if (draft) draft.isDirty = false; 173 clearDraft(); 174 window.showToast?.('Pre-construction project created', 'success', { description: data.projectTitle }); 175 176 if (result?.projectId) page(`/pre-construction/${result.projectId}`); 177 else page('/pre-construction'); 178 } catch (error) { 179 if (error.name === 'AbortError') return; 180 window.showToast?.('Failed to create project', 'error', { description: error.message }); 181 setLoading(btn, false); 182 } 183} 184 185// ======================================== 186// NAVIGATION GUARD 187// ======================================== 188 189function onBeforeUnload(e) { 190 if (draft?.isDirty) { 191 draft.save(); 192 e.preventDefault(); 193 e.returnValue = ''; 194 } 195}
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.