-import { inject } from 'vue'
+import { inject, ref } from 'vue'
import {
- localDateStr, fmtDur, shortAddr, dayLoadColor, stOf,
- ICON, jobSpansDate, techDayCapacityH, techDaySchedule, timeToH,
+ localDateStr, fmtDate, fmtDur, shortAddr, dayLoadColor, stOf,
+ ICON, jobSpansDate, techDayCapacityH, techDaySchedule, timeToH, expandRRule,
} from 'src/composables/useHelpers'
import { ABSENCE_REASONS } from 'src/composables/useTechManagement'
@@ -12,6 +12,7 @@ const props = defineProps({
selectedTechId: String,
dropGhost: Object,
todayStr: String,
+ colW: { type: Number, default: 0 }, // fixed column width (px), 0 = flex
})
const emit = defineEmits([
@@ -20,6 +21,7 @@ const emit = defineEmits([
'cal-drop', 'job-dragstart', 'job-click', 'job-dblclick', 'job-ctx',
'clear-filters',
'open-absence', 'end-absence', 'open-schedule',
+ 'ghost-click', 'ghost-materialize',
])
const store = inject('store')
@@ -27,7 +29,9 @@ const TECH_COLORS = inject('TECH_COLORS')
const jobColor = inject('jobColor')
const selectedJob = inject('selectedJob')
const isJobMultiSelected = inject('isJobMultiSelected')
+const ghostOccurrencesForDate = inject('ghostOccurrencesForDate', () => () => [])
const getTagColor = inject('getTagColor')
+const planningMode = inject('planningMode', ref(false))
function isDayToday (d) { return localDateStr(d) === props.todayStr }
@@ -76,19 +80,45 @@ function absenceInfo (tech, d) {
return { icon, label, isFullDay, hours: absHours, timeRange, remainH }
}
+// ── Planning mode: schedule availability info per tech/day ──────────────────
+function scheduleInfoForDay (tech, d) {
+ const ds = localDateStr(d)
+ const sched = techDaySchedule(tech, ds)
+ const info = { available: false, label: '', start: '', end: '', isOnCall: false, onCallLabel: '' }
+ if (sched) {
+ info.available = true
+ info.start = sched.start
+ info.end = sched.end
+ info.label = `${sched.start} – ${sched.end}`
+ }
+ // Check for on-call / extra shifts
+ const extras = (tech.extraShifts || []).filter(s => {
+ if (!s.rrule || !s.startTime || !s.endTime) return false
+ const dates = expandRRule(s.rrule, s.from || ds, ds, ds, [])
+ return dates.includes(ds)
+ })
+ if (extras.length) {
+ const ex = extras[0]
+ info.isOnCall = true
+ info.onCallLabel = `${ex.label || 'Garde'}: ${ex.startTime} – ${ex.endTime}`
+ }
+ return info
+}
+
defineExpose({ isDayToday })
-
+
Ressources {{ filteredResources.length }}
-
+
- {{ d.toLocaleDateString('fr-CA',{weekday:'short'}) }}
+ {{ fmtDate(d, {weekday:'short'}) }}
{{ d.getDate() }}
@@ -125,16 +155,30 @@ defineExpose({ isDayToday })
-
+
{}" @dragleave="()=>{}"
@drop.prevent="emit('cal-drop', $event, tech, localDateStr(d))">
-
+
+ {{ scheduleInfoForDay(tech, d).label }}
+
+
+ 🔔 {{ scheduleInfoForDay(tech, d).onCallLabel }}
+
+
+
{{ absenceInfo(tech, d).icon }} {{ absenceInfo(tech, d).label }}
Journée complète
@@ -142,8 +186,15 @@ defineExpose({ isDayToday })
{{ absenceInfo(tech, d).timeRange }} · reste {{ fmtDur(absenceInfo(tech, d).remainH) }}
-
-
+
+
🔄 {{ job.subject }}
+
{{ fmtDur(job.duration) }} · récurrent
+
+
-
+
-
{{ fmtDur(tech.queue.filter(j=>jobSpansDate(j,localDateStr(d))).reduce((a,j)=>a+(parseFloat(j.duration)||0),0)) }}/{{ fmtDur(techDayCapacityH(tech,localDateStr(d))||8) }}
+
{{ fmtDur(tech.queue.filter(j=>jobSpansDate(j,localDateStr(d),tech)).reduce((a,j)=>a+(parseFloat(j.duration)||0),0)) }}/{{ fmtDur(techDayCapacityH(tech,localDateStr(d))||8) }}
diff --git a/apps/ops/src/pages/DispatchPage.vue b/apps/ops/src/pages/DispatchPage.vue
index 8f42d95..989493e 100644
--- a/apps/ops/src/pages/DispatchPage.vue
+++ b/apps/ops/src/pages/DispatchPage.vue
@@ -1,5 +1,6 @@
@@ -588,6 +994,15 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
{{ selectedResIds.length }} ressource{{ selectedResIds.length>1?'s':'' }} ✕
Ressources…
+
+
+
+
@@ -599,7 +1014,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
@@ -655,6 +1081,10 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
+
{ filterTags = v; localStorage.setItem('sbv2-filterTags', JSON.stringify(v)) }"
:all-tags="store.allTags" :get-color="getTagColor" :can-create="false" :can-edit="false" placeholder="Filtrer par tag…" />
@@ -671,7 +1101,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
- ✕ Réinitialiser
+ ✕ Réinitialiser
✕
@@ -726,6 +1156,12 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
Date de fin
+
Ressources {{ filteredResources.length }}
-
-
{{ tick.label }}
+
+
+ {{ fmtDate(tick.day) }}
+
+ {{ tick.label }}
@@ -799,6 +1273,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
@hover-job="id => hoveredJobId=id" @unhover-job="hoveredJobId=null"
@block-move="startBlockMove" @block-resize="startResize"
@absence-resize="startAbsenceResize"
+ @ghost-click="onGhostClick" @ghost-materialize="materializeGhost"
@open-absence="openAbsenceModal" @end-absence="endAbsence"
@open-schedule="openScheduleModal" />
@@ -854,6 +1329,20 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
@assign-pending="() => rightPanel=null"
@update-tags="(job, v) => { job.tags = v; persistJobTags(job) }" />
+
+
+
+ onOfferAccept(o)"
+ @cancel="o => handleCancel(o.id)"
+ />
+
+
+
@@ -862,6 +1351,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
Sélections sauvegardées
-
- {{ p.name }} {{ p.ids.length }}
+ 👥
+ {{ p.name }} {{ p.type === 'group' ? store.technicians.filter(t => t.group === p.group && t.status !== 'inactive').length : p.ids.length }}
✕
@@ -965,8 +1457,14 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
Groupes
Tous
- {{ g }}
+
+ {{ g }}
+ {{ store.technicians.filter(t => t.group === g && t.status !== 'inactive').length }}
+
+ 💾
+
@@ -1221,6 +1719,33 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeyDown); document
Repos
+
+
+
+
+