feat(planif): fenêtre usuelle APPRISE + persistée par tech (préréglage sur jours vides)
Un jour VIDE (ou une semaine future non planifiée) ne voyait aucun quart → le préréglage retombait sur 8–16 générique. Désormais on APPREND la fenêtre la plus fréquente de chaque tech depuis ses vrais quarts à chaque loadWeek et on la PERSISTE (localStorage roster-usual-win-v1). Sur un jour sans quart, le préréglage propose donc le « quart normal le plus probable » du tech (ex. Houssam → 8–18), même sur une semaine future. Priorité : perso enregistré > usuel appris > patron hebdo > 8–16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6f4a2f0cb3
commit
53cf52bb13
|
|
@ -2175,27 +2175,44 @@ const hiddenCount = computed(() => techs.value.filter(t => isHidden(t.id)).lengt
|
|||
// personnalisés depuis le menu de cellule (ex. Philip 7:30→17:30) → réutilisés à chaque clic « Jour »/« Soir ».
|
||||
const LS_SHIFT_PRESETS = 'roster-shift-presets-v1'
|
||||
const techShiftPresets = ref({}) // { [techId]: { day:{start,end}, eve:{start,end} } }
|
||||
// Préréglage Jour = perso enregistré, SINON la fenêtre USUELLE du tech (« gabarit le plus probable »), SINON 8–16.
|
||||
// Fenêtre USUELLE APPRISE + PERSISTÉE par tech : { techId: {start,end} }. Alimentée depuis les VRAIS quarts chaque fois
|
||||
// qu'une semaine est chargée (refreshUsualCache) → survit aux semaines VIDES/futures (là où le scan direct ne verrait rien)
|
||||
// ET aux rechargements. C'est le « quart normal le plus probable » demandé (ex. Houssam 8–18 même sur un jour sans quart).
|
||||
const LS_USUAL = 'roster-usual-win-v1'
|
||||
const usualCache = ref({})
|
||||
// Préréglage Jour = perso enregistré, SINON la fenêtre USUELLE du tech, SINON 8–16.
|
||||
function presetDayFor (techId) { const p = techId && techShiftPresets.value[techId]; return (p && p.day) || usualWindowFor(techId) || { start: 8, end: 16 } }
|
||||
function presetEveFor (techId) { const p = techId && techShiftPresets.value[techId]; return (p && p.eve) || { start: 16, end: 20 } }
|
||||
// Fenêtre USUELLE d'un tech : la plus fréquente de son patron hebdo (weekly_schedule), sinon de ses quarts réguliers
|
||||
// déjà assignés (hors garde). Pré-remplit le préréglage Jour (ex. Houssam 8–18) → l'utilisateur n'a plus à ressaisir.
|
||||
// Recalcule la fenêtre la plus fréquente de CHAQUE tech depuis les quarts réguliers chargés → met à jour le cache persistant
|
||||
// (les techs sans quart chargé gardent leur valeur connue). Appelé après chaque loadWeek.
|
||||
function refreshUsualCache () {
|
||||
const byTech = {}
|
||||
for (const a of (assignments.value || [])) {
|
||||
const tp = tplByName.value[a.shift]; if (!tp || tp.on_call) continue
|
||||
const k = (tp.start_time || '').slice(0, 5) + '-' + (tp.end_time || '').slice(0, 5); if (k === '-') continue
|
||||
;(byTech[a.tech] || (byTech[a.tech] = {}))[k] = (byTech[a.tech][k] || 0) + 1
|
||||
}
|
||||
let changed = false; const next = { ...usualCache.value }
|
||||
for (const tid of Object.keys(byTech)) {
|
||||
const best = Object.keys(byTech[tid]).sort((x, y) => byTech[tid][y] - byTech[tid][x])[0]
|
||||
const [s, e] = best.split('-'); const sd = hToNum(s); const ed = hToNum(e)
|
||||
if (sd != null && ed != null && ed > sd && (!next[tid] || next[tid].start !== sd || next[tid].end !== ed)) { next[tid] = { start: sd, end: ed }; changed = true }
|
||||
}
|
||||
if (changed) { usualCache.value = next; try { localStorage.setItem(LS_USUAL, JSON.stringify(next)) } catch (e) {} }
|
||||
}
|
||||
// Fenêtre USUELLE d'un tech : d'abord la fenêtre APPRISE (cache, = ses vrais quarts, persistée), sinon son patron hebdo
|
||||
// (weekly_schedule). null si vraiment rien de connu → l'appelant retombe sur 8–16.
|
||||
function usualWindowFor (techId) {
|
||||
if (!techId) return null
|
||||
const counts = {}
|
||||
const add = (s, e) => { if (s && e) { const k = String(s).slice(0, 5) + '-' + String(e).slice(0, 5); counts[k] = (counts[k] || 0) + 1 } }
|
||||
const t = (techs.value || []).find(x => x.id === techId)
|
||||
const ws = t && t.weekly_schedule
|
||||
const c = usualCache.value[techId]; if (c && c.start != null && c.end != null) return c
|
||||
const t = (techs.value || []).find(x => x.id === techId); const ws = t && t.weekly_schedule
|
||||
if (ws && typeof ws === 'object') {
|
||||
const counts = {}; const add = (s, e) => { if (s && e) { const k = String(s).slice(0, 5) + '-' + String(e).slice(0, 5); counts[k] = (counts[k] || 0) + 1 } }
|
||||
for (const k of Object.keys(ws)) { const day = ws[k]; if (Array.isArray(day)) day.forEach(w => w && add(w.start, w.end)); else if (day && day.start && day.end) add(day.start, day.end) }
|
||||
}
|
||||
if (!Object.keys(counts).length) { // repli : quarts réguliers (hors garde) déjà assignés au tech
|
||||
for (const a of (assignments.value || [])) { if (a.tech !== techId) continue; const tp = tplByName.value[a.shift]; if (!tp || tp.on_call) continue; add(tp.start_time, tp.end_time) }
|
||||
}
|
||||
const best = Object.keys(counts).sort((x, y) => counts[y] - counts[x])[0]
|
||||
if (!best) return null
|
||||
const [s, e] = best.split('-'); const sd = hToNum(s); const ed = hToNum(e)
|
||||
return (sd != null && ed != null && ed > sd) ? { start: sd, end: ed } : null
|
||||
if (best) { const [s, e] = best.split('-'); const sd = hToNum(s); const ed = hToNum(e); if (sd != null && ed != null && ed > sd) return { start: sd, end: ed } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function onSetPreset (techId, { kind, start, end } = {}) {
|
||||
if (!techId || start == null || end == null) return
|
||||
|
|
@ -6149,6 +6166,7 @@ async function loadWeek () {
|
|||
const a = await roster.listAssignments(start.value, days.value); assignments.value = a.assignments || []
|
||||
snapshotServer(assignments.value); history.value = []; future.value = []; solverStats.value = null
|
||||
lastWeek.start = start.value; lastWeek.days = days.value
|
||||
refreshUsualCache() // apprend/persiste la fenêtre usuelle par tech depuis les quarts chargés (sert les semaines vides)
|
||||
await loadStats()
|
||||
} catch (e) { err(e) } finally { loading.value = false }
|
||||
}
|
||||
|
|
@ -6256,7 +6274,7 @@ async function doWeekStatus (mode) {
|
|||
}
|
||||
|
||||
// demande
|
||||
function loadLS () { try { holidays.value = JSON.parse(localStorage.getItem(LS_HOL) || '[]') } catch { holidays.value = [] } try { weekTemplates.value = JSON.parse(localStorage.getItem(LS_TPL) || '[]') } catch { weekTemplates.value = [] } try { gardeRules.value = JSON.parse(localStorage.getItem(LS_GARDE) || '[]') } catch { gardeRules.value = [] } try { manualGarde.value = JSON.parse(localStorage.getItem(LS_GARDE_MANUAL) || '{}') } catch { manualGarde.value = {} } try { customTags.value = JSON.parse(localStorage.getItem('roster-skill-tags-v1') || '[]') } catch { customTags.value = [] } try { hiddenTechs.value = JSON.parse(localStorage.getItem('roster-hidden-techs-v1') || '[]') } catch { hiddenTechs.value = [] } try { pendingAbs.value = JSON.parse(localStorage.getItem(LS_PENDING_ABS) || '{}') } catch { pendingAbs.value = {} } try { pendingPause.value = JSON.parse(localStorage.getItem(LS_PENDING_PAUSE) || '{}') } catch { pendingPause.value = {} } try { techShiftPresets.value = JSON.parse(localStorage.getItem(LS_SHIFT_PRESETS) || '{}') || {} } catch { techShiftPresets.value = {} } }
|
||||
function loadLS () { try { holidays.value = JSON.parse(localStorage.getItem(LS_HOL) || '[]') } catch { holidays.value = [] } try { weekTemplates.value = JSON.parse(localStorage.getItem(LS_TPL) || '[]') } catch { weekTemplates.value = [] } try { gardeRules.value = JSON.parse(localStorage.getItem(LS_GARDE) || '[]') } catch { gardeRules.value = [] } try { manualGarde.value = JSON.parse(localStorage.getItem(LS_GARDE_MANUAL) || '{}') } catch { manualGarde.value = {} } try { customTags.value = JSON.parse(localStorage.getItem('roster-skill-tags-v1') || '[]') } catch { customTags.value = [] } try { hiddenTechs.value = JSON.parse(localStorage.getItem('roster-hidden-techs-v1') || '[]') } catch { hiddenTechs.value = [] } try { pendingAbs.value = JSON.parse(localStorage.getItem(LS_PENDING_ABS) || '{}') } catch { pendingAbs.value = {} } try { pendingPause.value = JSON.parse(localStorage.getItem(LS_PENDING_PAUSE) || '{}') } catch { pendingPause.value = {} } try { techShiftPresets.value = JSON.parse(localStorage.getItem(LS_SHIFT_PRESETS) || '{}') || {} } catch { techShiftPresets.value = {} } try { usualCache.value = JSON.parse(localStorage.getItem(LS_USUAL) || '{}') || {} } catch { usualCache.value = {} } }
|
||||
|
||||
// ── Rotation de garde par département (récurrence + rotation) ────────────────
|
||||
const GARDE_EPOCH = '2026-01-05' // lundi de référence pour l'index de semaine
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user