gigafibre-fsm/apps/ops/src/composables/usePeriodNavigation.js
louispaulb 0c77afdb3b feat: dispatch planning mode, offer pool, shared presets, recurrence selector
- 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>
2026-04-08 22:44:18 -04:00

106 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}