// ── Selection composable: lasso, multi-select, hover linking, batch ops ─────── import { ref, computed } from 'vue' import { localDateStr } from './useHelpers' export function useSelection (deps) { const { store, periodStart, smartAssign, invalidateRoutes, fullUnassign } = deps const hoveredJobId = ref(null) const selectedJob = ref(null) // { job, techId, isAssist?, assistTechId? } const multiSelect = ref([]) // [{ job, techId, isAssist?, assistTechId? }] // ── Select / toggle ───────────────────────────────────────────────────────── function selectJob (job, techId, isAssist = false, assistTechId = null, event = null, rightPanel = null) { const entry = { job, techId, isAssist, assistTechId } const isMulti = event && (event.ctrlKey || event.metaKey) if (isMulti) { const idx = multiSelect.value.findIndex(s => s.job.id === job.id && s.isAssist === isAssist) if (idx >= 0) multiSelect.value.splice(idx, 1) else multiSelect.value.push(entry) selectedJob.value = entry } else { multiSelect.value = [] const same = selectedJob.value?.job?.id === job.id && selectedJob.value?.isAssist === isAssist && selectedJob.value?.assistTechId === assistTechId selectedJob.value = same ? null : entry if (!same && rightPanel !== undefined) { const tech = store.technicians.find(t => t.id === (techId || job.assignedTech)) if (rightPanel !== null && typeof rightPanel === 'object' && 'value' in rightPanel) { rightPanel.value = { mode: 'details', data: { job, tech: tech || null } } } } else if (rightPanel !== null && typeof rightPanel === 'object' && 'value' in rightPanel) { rightPanel.value = null } } } function isJobMultiSelected (jobId, isAssist = false) { return multiSelect.value.some(s => s.job.id === jobId && s.isAssist === isAssist) } // ── Batch ops ───────────────────────────────────────────────────────────────── function batchUnassign () { if (!multiSelect.value.length) return multiSelect.value.forEach(s => { if (s.isAssist && s.assistTechId) store.removeAssistant(s.job.id, s.assistTechId) else fullUnassign(s.job) }) multiSelect.value = []; selectedJob.value = null invalidateRoutes() } function batchMoveTo (techId) { if (!multiSelect.value.length) return const dayStr = localDateStr(periodStart.value) multiSelect.value.filter(s => !s.isAssist).forEach(s => smartAssign(s.job, techId, dayStr)) multiSelect.value = []; selectedJob.value = null invalidateRoutes() } // ── Lasso ───────────────────────────────────────────────────────────────────── const lasso = ref(null) const boardScroll = ref(null) const lassoStyle = computed(() => { if (!lasso.value) return {} const l = lasso.value return { left: Math.min(l.x1, l.x2) + 'px', top: Math.min(l.y1, l.y2) + 'px', width: Math.abs(l.x2 - l.x1) + 'px', height: Math.abs(l.y2 - l.y1) + 'px', } }) function startLasso (e) { if (e.target.closest('.sb-block, .sb-chip, .sb-res-cell, .sb-travel-trail, button, input, select, a')) return if (e.button !== 0) return e.preventDefault() if (!e.ctrlKey && !e.metaKey) { if (selectedJob.value || multiSelect.value.length) { selectedJob.value = null; multiSelect.value = [] } } const rect = boardScroll.value.getBoundingClientRect() const x = e.clientX - rect.left + boardScroll.value.scrollLeft const y = e.clientY - rect.top + boardScroll.value.scrollTop lasso.value = { x1: x, y1: y, x2: x, y2: y } } function moveLasso (e) { if (!lasso.value) return e.preventDefault() const rect = boardScroll.value.getBoundingClientRect() lasso.value.x2 = e.clientX - rect.left + boardScroll.value.scrollLeft lasso.value.y2 = e.clientY - rect.top + boardScroll.value.scrollTop } function endLasso () { if (!lasso.value) return const l = lasso.value const w = Math.abs(l.x2 - l.x1), h = Math.abs(l.y2 - l.y1) if (w > 10 && h > 10) { const boardRect = boardScroll.value.getBoundingClientRect() const lassoLeft = Math.min(l.x1, l.x2) - boardScroll.value.scrollLeft + boardRect.left const lassoTop = Math.min(l.y1, l.y2) - boardScroll.value.scrollTop + boardRect.top const lassoRight = lassoLeft + w, lassoBottom = lassoTop + h const blocks = boardScroll.value.querySelectorAll('.sb-block[data-job-id], .sb-chip') const selected = [] blocks.forEach(el => { const r = el.getBoundingClientRect() if (r.right > lassoLeft && r.left < lassoRight && r.bottom > lassoTop && r.top < lassoBottom) { const jobId = el.dataset?.jobId if (jobId) { const job = store.jobs.find(j => j.id === jobId) if (job) selected.push({ job, techId: job.assignedTech, isAssist: false, assistTechId: null }) } } }) if (selected.length) { multiSelect.value = selected if (selected.length === 1) selectedJob.value = selected[0] } } lasso.value = null } // ── Hover linking helpers ───────────────────────────────────────────────────── function techHasLinkedJob (tech) { const hId = hoveredJobId.value, sId = selectedJob.value?.job?.id if (hId && (tech.assistJobs || []).some(j => j.id === hId)) return true if (hId && tech.queue.some(j => j.id === hId)) return true if (sId && !selectedJob.value?.isAssist && (tech.assistJobs || []).some(j => j.id === sId)) return true if (sId && selectedJob.value?.isAssist && tech.queue.some(j => j.id === sId)) return true return false } function techIsHovered (tech) { const hId = hoveredJobId.value if (!hId) return false const job = tech.queue.find(j => j.id === hId) return job && job.assistants?.length > 0 } return { hoveredJobId, selectedJob, multiSelect, selectJob, isJobMultiSelected, batchUnassign, batchMoveTo, lasso, boardScroll, lassoStyle, startLasso, moveLasso, endLasso, techHasLinkedJob, techIsHovered, } }