PageSourceSearch

https://smrtbld.ai/js/utils/project-divisions-editor.js

js smrtbld.ai collected 2026-10-05 15:10:51 UTC 4,136 bytes, 87 lines download raw bytes

1import { divisionsApi } from '../api/divisions.js';
2import { projectsApi } from '../api/projects.js';
3import { companySettings } from '../services/company-settings.js';
4import { showModal } from './modal.js';
5import { setLoading } from './dom.js';
6
7/**
8 * Shared editable "Divisions" affordance for any project-stage detail header.
9 *
10 * Pairs with the shared JsRender snippet `#tpl-project-divisions-header` (in
11 * components/sub-templates.html) — include it in a stage's header subtitle, render
12 * that header with `project.usesDivisions` set + a `{ canEdit }` helper, then call
13 * this once per page init on the (stable) header container.
14 *
15 * Clicking the affordance opens the manage modal (`#tpl-manage-divisions-modal`):
16 * a checkbox list of ALL divisions, pre-checked = currently assigned, Save = replace-all
17 * (POST /projects/{id}/divisions). One modal handles both add and remove.
18 *
19 * @param {Object} opts
20 * @param {string} opts.rootId - id of the STABLE header container the trigger lives in
21 *   (renderTo replaces its innerHTML, not the element, so a delegated listener survives).
22 * @param {() => (Object|null)} opts.getProject - returns the current project; read at
23 *   click time (never a stale closure). Needs { projectId, projectTitle, assignedDivisionIds }.
24 * @param {() => (void|Promise<void>)} opts.onChanged - called after a successful save
25 *   (re-fetch + re-render the detail so the header reflects the new divisions).
26 * @param {AbortSignal} [opts.signal] - page abort signal; auto-removes the listener on destroy.
27 */
28export function initProjectDivisionsEditor({ rootId, getProject, onChanged, signal }) {
29    const root = document.getElementById(rootId);
30    if (!root) return;
31
32    root.addEventListener('click', (e) => {
33        if (e.target.closest('.btn-project-manage-divisions')) {
34            openManageModal(getProject(), onChanged, signal);
35        }
36    }, { signal });
37}
38
39async function openManageModal(project, onChanged, signal) {
40    if (!project) return;
41    const assigned = new Set(project.assignedDivisionIds || []);
42    // When projectMulti is off, a project belongs to at most ONE division (single-select).
43    const multi = companySettings.getSettings()?.projectMulti ?? false;
44    try {
45        const resp = await divisionsApi.getAll();
46        const divisions = (resp?.divisions || []).map(d => ({
47            id: d.divisionId,
48            name: d.divisionTitle,
49            checked: assigned.has(d.divisionId),
50        }));
51
52        const modal = await showModal('#tpl-manage-divisions-modal', {
53            targetName: project.projectTitle,
54            divisions,
55            multi,
56        }, { signal });
57
58        // Enforce single-select when multi-assignment is disabled: checking one clears the rest
59        // (still allows unchecking to assign no division).
60        if (!multi) {
61            const boxes = modal.element.querySelectorAll('.division-checkbox');
62            boxes.forEach(box => box.addEventListener('change', () => {
63                if (box.checked) boxes.forEach(other => { if (other !== box) other.checked = false; });
64            }));
65        }
66
67        const saveBtn = modal.element.querySelector('#btn-save-divisions');
68        saveBtn?.addEventListener('click', async () => {
69            const ids = Array.from(modal.element.querySelectorAll('.division-checkbox:checked'))
70                .map(cb => parseInt(cb.value));
71            try {
72                setLoading(saveBtn, true, 'Saving...');
73                await projectsApi.updateDivisions(project.projectId, ids);
74                modal.close();
75                window.showToast?.('Divisions updated', 'success');
76                await onChanged?.();
77            } catch (error) {
78                if (error.name === 'AbortError') return;
79                window.showToast?.('Failed to update divisions', 'error', { description: error.message });
80                setLoading(saveBtn, false);
81            }
82        });
83    } catch (error) {
84        if (error.name === 'AbortError') return;
85        window.showToast?.('Failed to load divisions', 'error', { description: error.message });
86    }
87}

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.