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.