// ── Bottom panel composable: unassigned jobs table, multi-select, criteria ──── import { ref, computed, watch } from 'vue' import { localDateStr, fmtDate } from './useHelpers' export function useBottomPanel (store, todayStr, unscheduledJobs, { pushUndo, smartAssign, invalidateRoutes, periodStart }) { const bottomPanelOpen = ref(localStorage.getItem('sbv2-bottomPanel') !== 'false') const bottomPanelH = ref(parseInt(localStorage.getItem('sbv2-bottomH')) || 220) watch(bottomPanelOpen, v => localStorage.setItem('sbv2-bottomPanel', v ? 'true' : 'false')) // ── Tri / regroupement (date · ville · priorité) ───────────────────────────── const bottomSort = ref(localStorage.getItem('sbv2-bottomSort') || 'date') watch(bottomSort, v => localStorage.setItem('sbv2-bottomSort', v)) const PRIO = { urgent: 0, high: 1, medium: 2, low: 3 } // Ville : 2e segment de l'adresse libre, sinon 1er token du sujet avant « | » (tickets legacy), sinon vide. function cityOf (job) { const a = String(job.address || '') const parts = a.split(',').map(s => s.trim()).filter(Boolean) if (parts.length >= 2) return parts[1] const subj = String(job.subject || ''); if (subj.includes('|')) return subj.split('|')[0].trim() return parts[0] || '' } const unassignedGrouped = computed(() => { const today = todayStr const sort = bottomSort.value const jobs = unscheduledJobs.value.slice() const byDate = (a, b) => { // ordre secondaire : date (aujourd'hui d'abord) const da = a.scheduledDate || '9999-99-99'; const db = b.scheduledDate || '9999-99-99' const at = da === today ? 0 : 1; const bt = db === today ? 0 : 1 return at !== bt ? at - bt : da.localeCompare(db) } jobs.sort((a, b) => { if (sort === 'priority') return (PRIO[a.priority] ?? 3) - (PRIO[b.priority] ?? 3) || byDate(a, b) if (sort === 'city') return cityOf(a).localeCompare(cityOf(b)) || byDate(a, b) return byDate(a, b) || ((PRIO[a.priority] ?? 3) - (PRIO[b.priority] ?? 3)) }) const keyOf = job => sort === 'priority' ? (job.priority || 'low') : sort === 'city' ? (cityOf(job) || 'Sans ville') : (job.scheduledDate || null) const labelOf = key => { if (sort === 'priority') return ({ urgent: '🔴 Urgent', high: '🟠 Élevée', medium: '🔵 Moyenne', low: '⚪ Basse' })[key] || key if (sort === 'city') return key return key === today ? "Aujourd'hui" : (key ? fmtDate(new Date(key + 'T00:00:00')) : 'Sans date') } const groups = []; let cur = Symbol('init') jobs.forEach(job => { const k = keyOf(job) if (k !== cur) { cur = k; groups.push({ key: String(k), date: sort === 'date' ? k : null, label: labelOf(k), jobs: [] }) } groups.at(-1).jobs.push(job) }) return groups }) // ── Resize ─────────────────────────────────────────────────────────────────── function startBottomResize (e) { e.preventDefault() const startY = e.clientY, startH = bottomPanelH.value function onMove (ev) { bottomPanelH.value = Math.max(100, Math.min(window.innerHeight * 0.6, startH - (ev.clientY - startY))) } function onUp () { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); localStorage.setItem('sbv2-bottomH', String(bottomPanelH.value)) } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp) } // ── Multi-select ───────────────────────────────────────────────────────────── const bottomSelected = ref(new Set()) function toggleBottomSelect (jobId, event) { const s = new Set(bottomSelected.value) // Checkbox click: always toggle (no modifier needed) // Shift+click: range select if (event?.shiftKey && s.size) { const flat = unassignedGrouped.value.flatMap(g => g.jobs) const ids = flat.map(j => j.id) const lastId = [...s].pop() const fromIdx = ids.indexOf(lastId), toIdx = ids.indexOf(jobId) if (fromIdx >= 0 && toIdx >= 0) { const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx] for (let i = lo; i <= hi; i++) s.add(ids[i]) } } else { // Simple toggle (no Ctrl needed) if (s.has(jobId)) s.delete(jobId); else s.add(jobId) } bottomSelected.value = s } function selectAllBottom () { const s = new Set(); unscheduledJobs.value.forEach(j => s.add(j.id)); bottomSelected.value = s } function clearBottomSelect () { bottomSelected.value = new Set() } function batchAssignBottom (techId) { const dayStr = localDateStr(periodStart.value) bottomSelected.value.forEach(jobId => { const job = store.jobs.find(j => j.id === jobId) if (job) { pushUndo({ type: 'unassignJob', jobId: job.id, techId: job.assignedTech, routeOrder: job.routeOrder, scheduledDate: job.scheduledDate, assistants: [...(job.assistants || [])] }) smartAssign(job, techId, dayStr) } }) bottomSelected.value = new Set() invalidateRoutes() } // ── Dispatch criteria ──────────────────────────────────────────────────────── const defaultCriteria = [ { id: 'urgency', label: 'Urgence (priorité haute en premier)', enabled: true }, { id: 'balance', label: 'Équilibrage de charge (tech le moins chargé)', enabled: true }, { id: 'proximity', label: 'Proximité géographique', enabled: true }, { id: 'skills', label: 'Correspondance des tags/skills', enabled: false }, ] const dispatchCriteria = ref(JSON.parse(localStorage.getItem('sbv2-dispatchCriteria') || 'null') || defaultCriteria.map(c => ({ ...c }))) const dispatchCriteriaModal = ref(false) function saveDispatchCriteria () { localStorage.setItem('sbv2-dispatchCriteria', JSON.stringify(dispatchCriteria.value)); dispatchCriteriaModal.value = false } function moveCriterion (idx, dir) { const arr = dispatchCriteria.value, newIdx = idx + dir if (newIdx < 0 || newIdx >= arr.length) return const tmp = arr[idx]; arr[idx] = arr[newIdx]; arr[newIdx] = tmp } // ── Column widths ──────────────────────────────────────────────────────────── const btColWidths = ref(JSON.parse(localStorage.getItem('sbv2-btColW') || '{}')) function btColW (col, def) { return (btColWidths.value[col] || def) + 'px' } function startColResize (e, col) { e.preventDefault(); e.stopPropagation() const startX = e.clientX, startW = btColWidths.value[col] || parseInt(getComputedStyle(e.target.parentElement).width) function onMove (ev) { btColWidths.value = { ...btColWidths.value, [col]: Math.max(40, startW + (ev.clientX - startX)) } } function onUp () { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); localStorage.setItem('sbv2-btColW', JSON.stringify(btColWidths.value)) } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp) } return { bottomPanelOpen, bottomPanelH, unassignedGrouped, bottomSort, startBottomResize, bottomSelected, toggleBottomSelect, selectAllBottom, clearBottomSelect, batchAssignBottom, dispatchCriteria, dispatchCriteriaModal, saveDispatchCriteria, moveCriterion, btColWidths, btColW, startColResize, } }