PageSourceSearch

https://smrtbld.ai/js/pages/pre-construction-create.js

js smrtbld.ai collected 2026-10-05 15:07:41 UTC 6,414 bytes, 195 lines download raw bytes

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.