- Planning mode toggle: shift availability as background blocks on timeline (week view shows green=available, yellow=on-call; month view per-tech) - On-call/guard shift editor with RRULE recurrence on tech schedules - Uber-style job offer pool: broadcast/targeted/pool modes with pricing, SMS notifications, accept/decline flow, overload detection alerts - Shared resource group presets via ERPNext Dispatch Preset doctype (replaces localStorage, shared between supervisors) - Google Calendar-style RecurrenceSelector component with contextual quick options + custom RRULE editor, integrated in booking overlay and extra shift editor - Remove default "Repos" ghost chips — only visible in planning mode - Clean up debug console.logs across API, store, and page layers - Add extra_shifts Custom Field on Dispatch Technician doctype Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.9 KiB
JavaScript
106 lines
3.9 KiB
JavaScript
import { ref, computed, watch } from 'vue'
|
||
import { localDateStr, startOfWeek, startOfMonth } from 'src/composables/useHelpers'
|
||
|
||
// Buffer: 1 period before, 2 after — biased toward the future for natural right-scroll
|
||
const BUFFER_BEFORE = 1
|
||
const BUFFER_AFTER = 2
|
||
|
||
export function usePeriodNavigation () {
|
||
const currentView = ref(localStorage.getItem('sbv2-view') || 'week')
|
||
const savedDate = localStorage.getItem('sbv2-date')
|
||
let initDate = new Date()
|
||
if (savedDate && /^\d{4}-\d{2}-\d{2}$/.test(savedDate)) {
|
||
const d = new Date(savedDate + 'T00:00:00')
|
||
if (!isNaN(d.getTime())) initDate = d
|
||
}
|
||
const anchorDate = ref(initDate)
|
||
|
||
watch(currentView, v => localStorage.setItem('sbv2-view', v))
|
||
watch(anchorDate, d => localStorage.setItem('sbv2-date', localDateStr(d)))
|
||
|
||
const periodStart = computed(() => {
|
||
const d = new Date(anchorDate.value); d.setHours(0,0,0,0)
|
||
if (currentView.value === 'day') return d
|
||
if (currentView.value === 'week') return startOfWeek(d)
|
||
return startOfMonth(d)
|
||
})
|
||
|
||
// The "core" period length (what the label describes)
|
||
const periodDays = computed(() => {
|
||
if (currentView.value === 'day') return 1
|
||
if (currentView.value === 'week') return 7
|
||
const s = periodStart.value
|
||
return new Date(s.getFullYear(), s.getMonth()+1, 0).getDate()
|
||
})
|
||
|
||
// Buffer: extra periods before/after for seamless scroll (week only, not day)
|
||
const bufferDaysBefore = computed(() => {
|
||
if (currentView.value !== 'week') return 0
|
||
return periodDays.value * BUFFER_BEFORE
|
||
})
|
||
|
||
const renderedDays = computed(() => {
|
||
if (currentView.value !== 'week') return periodDays.value
|
||
return periodDays.value * (1 + BUFFER_BEFORE + BUFFER_AFTER)
|
||
})
|
||
|
||
// The start date of all rendered columns (buffer included)
|
||
const renderedStart = computed(() => {
|
||
const ps = periodStart.value
|
||
if (!ps || isNaN(ps.getTime())) return new Date()
|
||
const d = new Date(ps)
|
||
d.setDate(d.getDate() - bufferDaysBefore.value)
|
||
return d
|
||
})
|
||
|
||
// dayColumns spans the full rendered range (prev + current + next)
|
||
const dayColumns = computed(() => {
|
||
const cols = []
|
||
const base = renderedStart.value
|
||
if (!base || isNaN(base.getTime())) return cols
|
||
for (let i = 0; i < renderedDays.value; i++) {
|
||
const d = new Date(base); d.setDate(d.getDate() + i); cols.push(d)
|
||
}
|
||
return cols
|
||
})
|
||
|
||
function safeFmt (d, opts) {
|
||
try { return d.toLocaleDateString('fr-CA', opts) } catch { return localDateStr(d) }
|
||
}
|
||
const periodLabel = computed(() => {
|
||
const s = periodStart.value
|
||
if (!s || isNaN(s.getTime())) return '—'
|
||
if (currentView.value === 'day')
|
||
return safeFmt(s, { weekday:'long', day:'numeric', month:'long', year:'numeric' })
|
||
if (currentView.value === 'week') {
|
||
const e = new Date(s); e.setDate(e.getDate() + 6)
|
||
return `${safeFmt(s,{day:'numeric',month:'short'})} – ${safeFmt(e,{day:'numeric',month:'short',year:'numeric'})}`
|
||
}
|
||
return safeFmt(s, { month:'long', year:'numeric' })
|
||
})
|
||
const todayStr = localDateStr(new Date())
|
||
|
||
function prevPeriod () {
|
||
const d = new Date(anchorDate.value)
|
||
if (currentView.value === 'day') d.setDate(d.getDate()-1)
|
||
if (currentView.value === 'week') d.setDate(d.getDate()-7)
|
||
if (currentView.value === 'month') d.setMonth(d.getMonth()-1)
|
||
anchorDate.value = d
|
||
}
|
||
function nextPeriod () {
|
||
const d = new Date(anchorDate.value)
|
||
if (currentView.value === 'day') d.setDate(d.getDate()+1)
|
||
if (currentView.value === 'week') d.setDate(d.getDate()+7)
|
||
if (currentView.value === 'month') d.setMonth(d.getMonth()+1)
|
||
anchorDate.value = d
|
||
}
|
||
function goToToday () { anchorDate.value = new Date(); currentView.value = 'day' }
|
||
function goToDay (d) { anchorDate.value = new Date(d); currentView.value = 'day' }
|
||
|
||
return {
|
||
currentView, anchorDate, periodStart, periodDays, dayColumns, periodLabel, todayStr,
|
||
bufferDaysBefore, renderedDays,
|
||
prevPeriod, nextPeriod, goToToday, goToDay,
|
||
}
|
||
}
|