|
|
|
|
@ -400,7 +400,8 @@
|
|
|
|
|
<q-btn flat round dense size="9px" icon="event" :color="isPaused(t) ? 'orange-8' : 'primary'" @click.stop="openTechSchedule(t)"><q-tooltip>Horaire de {{ t.name }} — récurrent · pause · congés{{ isPaused(t) ? ' (en pause)' : '' }}</q-tooltip></q-btn>
|
|
|
|
|
<q-badge v-if="techRank(t)" color="teal">{{ techRank(t) }}<q-tooltip>Priorité {{ techRank(t) }} — combine maîtrise (niveau) + vitesse (efficacité) + coût.</q-tooltip></q-badge>
|
|
|
|
|
<!-- Clic sur la RESSOURCE (nom ou chips) → propriétés de l'employé (compétences, horaire) -->
|
|
|
|
|
<span class="tech-name clk" @click.stop="openSkillEditor(t, $event)"><q-tooltip>Réglages de {{ t.name }} — compétences · cadence · horaire · domicile · appareil GPS · voir sa tournée</q-tooltip>{{ t.name }}</span>
|
|
|
|
|
<span class="tech-name clk" :class="{ 'tech-paused-pending': !!pendingPause[t.id] }" @click.stop="openSkillEditor(t, $event)"><q-tooltip>Réglages de {{ t.name }} — compétences · cadence · horaire · domicile · appareil GPS · voir sa tournée</q-tooltip>{{ t.name }}</span>
|
|
|
|
|
<q-chip v-if="pendingPause[t.id]" dense size="sm" color="orange-9" text-color="white" icon="pause_circle" class="q-ml-xs">pause à publier<q-tooltip class="bg-grey-9">Pause indéfinie en attente — Publier pour appliquer (retire ses quarts futurs + relève ses jobs)</q-tooltip></q-chip>
|
|
|
|
|
<span v-if="t.group" class="grp">{{ t.group }}</span>
|
|
|
|
|
<span v-if="hoursOf(t.id)" class="th text-grey-6">{{ hoursOf(t.id) }}h<q-tooltip class="bg-grey-9" style="font-size:11px">Heures planifiées sur l'étendue affichée ({{ days }} j) — total, PAS une limite hebdomadaire</q-tooltip></span>
|
|
|
|
|
<q-btn flat dense round size="9px" icon="more_vert" color="grey-6" @click.stop><q-tooltip>Actions — {{ t.name }}</q-tooltip>
|
|
|
|
|
@ -1338,12 +1339,13 @@
|
|
|
|
|
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
|
|
|
|
</q-card-section>
|
|
|
|
|
<q-card-section class="q-pt-sm">
|
|
|
|
|
<div class="text-body2 q-mb-sm"><b class="text-positive">{{ pubSummary.added }}</b> quart(s)<span v-if="pubSummary.removed"> · <b class="text-negative">{{ pubSummary.removed }}</b> retiré(s)</span><span v-if="pubSummary.absAdded"> · <b class="text-orange-8">{{ pubSummary.absAdded }}</b> congé(s)</span><span v-if="pubSummary.absRemoved"> · <b class="text-teal-7">{{ pubSummary.absRemoved }}</b> congé(s) levé(s)</span> · <b>{{ notifySms ? pubSummary.added + ' SMS aux techs' : 'sans SMS' }}</b></div>
|
|
|
|
|
<div class="text-body2 q-mb-sm"><b class="text-positive">{{ pubSummary.added }}</b> quart(s)<span v-if="pubSummary.removed"> · <b class="text-negative">{{ pubSummary.removed }}</b> retiré(s)</span><span v-if="pubSummary.paused"> · <b class="text-orange-9">{{ pubSummary.paused }}</b> pause(s)</span><span v-if="pubSummary.absAdded"> · <b class="text-orange-8">{{ pubSummary.absAdded }}</b> congé(s)</span><span v-if="pubSummary.absRemoved"> · <b class="text-teal-7">{{ pubSummary.absRemoved }}</b> congé(s) levé(s)</span> · <b>{{ notifySms ? pubSummary.added + ' SMS aux techs' : 'sans SMS' }}</b></div>
|
|
|
|
|
<q-list dense bordered style="border-radius:8px;max-height:44vh;overflow:auto">
|
|
|
|
|
<template v-for="g in pubSummary.techs" :key="g.tech">
|
|
|
|
|
<q-item-label header class="q-py-xs text-weight-bold row items-center">
|
|
|
|
|
<UserAvatar :tech-id="g.tech" :name="g.name" :size="18" card class="q-mr-xs" style="flex:0 0 auto" />{{ g.name }}
|
|
|
|
|
</q-item-label>
|
|
|
|
|
<q-item v-if="g.pause" dense><q-item-section avatar style="min-width:24px"><q-icon name="pause_circle" color="orange-9" size="16px" /></q-item-section><q-item-section class="text-orange-9 text-weight-medium">Pause indéfinie<span class="text-grey-6" v-if="g.pause.reason"> · {{ g.pause.reason }}</span><span class="text-grey-6" v-if="g.rem.length"> · {{ g.rem.length }} quart(s) futur(s) retiré(s)</span></q-item-section></q-item>
|
|
|
|
|
<q-item v-for="(a, i) in g.add" :key="'a' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="add" color="positive" size="16px" /></q-item-section><q-item-section>{{ a.shift }}<span class="text-grey-6"> · {{ fmtDueLabel(a.date) }}</span></q-item-section></q-item>
|
|
|
|
|
<q-item v-for="(r, i) in g.rem" :key="'r' + i" dense><q-item-section avatar style="min-width:24px"><q-icon name="remove" color="negative" size="16px" /></q-item-section><q-item-section><span style="text-decoration:line-through">{{ r.shift }}</span><span class="text-grey-6"> · {{ fmtDueLabel(r.date) }}</span></q-item-section></q-item>
|
|
|
|
|
<q-item v-for="(ab, i) in g.abs" :key="'ab' + i" dense><q-item-section avatar style="min-width:24px"><q-icon :name="ab.op === 'set' ? 'event_busy' : 'event_available'" :color="ab.op === 'set' ? 'orange-8' : 'teal-7'" size="16px" /></q-item-section><q-item-section>{{ ab.op === 'set' ? (ab.type || 'Congé') : 'congé levé' }}<span class="text-grey-6"> · {{ fmtDueLabel(ab.date) }}</span></q-item-section></q-item>
|
|
|
|
|
@ -1754,8 +1756,8 @@
|
|
|
|
|
<ProjectWizard v-model="projectWizardOpen" :customer="quoteCustomer" :initial-tier="quoteTier" @created="onQuoteCreated" />
|
|
|
|
|
<!-- Génération de quarts hebdo (modèles + N semaines + LOT multi-techs) → écrit les Shift Assignment -->
|
|
|
|
|
<WeeklyScheduleEditor v-model="schedGenOpen" :techs="schedGenTechs" :tech-name="schedGenTechs[0] && schedGenTechs[0].name" @apply="onScheduleApply" />
|
|
|
|
|
<TechScheduleDialog v-model="techSchedOpen" :tech="techSchedTech" :pending-abs="pendingAbs" :garde-map="gardeEffective" :refresh-key="techSchedRefresh"
|
|
|
|
|
@edit-schedule="onTechSchedEdit" @changed="onTechSchedChanged" @stage-abs="onStageAbs"
|
|
|
|
|
<TechScheduleDialog v-model="techSchedOpen" :tech="techSchedTech" :pending-abs="pendingAbs" :pending-paused="!!(techSchedTech && pendingPause[techSchedTech.id])" :garde-map="gardeEffective" :refresh-key="techSchedRefresh"
|
|
|
|
|
@edit-schedule="onTechSchedEdit" @changed="onTechSchedChanged" @stage-abs="onStageAbs" @stage-pause="onStagePause"
|
|
|
|
|
@set-shift="onTechSchedSetShift" @remove-shift="onTechSchedRemoveShift" @clear-shifts="onTechSchedClearShifts" @toggle-garde="onTechSchedToggleGarde" />
|
|
|
|
|
</q-page>
|
|
|
|
|
</template>
|
|
|
|
|
@ -2683,6 +2685,32 @@ function onStageAbs ({ techId, changes } = {}) {
|
|
|
|
|
}
|
|
|
|
|
if (n) { $q.notify({ type: 'info', message: n + ' changement(s) de congé en attente — Publier pour appliquer', timeout: 2200 }); scheduleDraftSave() }
|
|
|
|
|
}
|
|
|
|
|
// PAUSE INDÉFINIE (émise par TechScheduleDialog) : on STAGE (want=true) → prend effet visuellement, commise au Publier.
|
|
|
|
|
// Dé-stager (want=false) une pause en attente = simple retrait. Réactiver un tech DÉJÀ en pause (serveur) reste immédiat
|
|
|
|
|
// (statut Disponible + re-matérialisation du patron) — hors du flux « brouillon ».
|
|
|
|
|
async function onStagePause ({ techId, want, reason } = {}) {
|
|
|
|
|
if (!techId) return
|
|
|
|
|
const t = (techs.value || []).find(x => x.id === techId); const nm = (t && t.name) || techId
|
|
|
|
|
if (want) {
|
|
|
|
|
if (isServerPaused(t) && !pendingPause.value[techId]) { // déjà en pause côté serveur → simple maj du motif (immédiat)
|
|
|
|
|
try { await roster.pauseTechnician(techId, true, reason || '') } catch (e) { err(e) }
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const already = !!pendingPause.value[techId]
|
|
|
|
|
pendingPause.value = { ...pendingPause.value, [techId]: { reason: reason || '' } }; savePendingPause()
|
|
|
|
|
if (!already) { // 1re mise en attente → journal + notif (les re-émissions « motif » sur blur restent silencieuses)
|
|
|
|
|
logChange('⏸ Pause indéfinie à publier · ' + nm)
|
|
|
|
|
$q.notify({ type: 'info', message: nm + ' — pause en attente. Publier pour appliquer (retire ses quarts + relève ses jobs).', timeout: 3000 })
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (pendingPause.value[techId]) { // annule une pause en attente (jamais écrite)
|
|
|
|
|
const m = { ...pendingPause.value }; delete m[techId]; pendingPause.value = m; savePendingPause()
|
|
|
|
|
logChange('Pause annulée (non publiée) · ' + nm)
|
|
|
|
|
} else if (isServerPaused(t)) { // réactivation immédiate d'un tech en pause
|
|
|
|
|
try { await roster.pauseTechnician(techId, false, ''); if (t) t.status = 'Disponible'; await roster.materializeShifts({ weeks: 6, tech: techId }).catch(() => {}); $q.notify({ type: 'positive', message: nm + ' réactivé — horaire récurrent re-matérialisé' }); await loadWeek() } catch (e) { err(e) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function onTechSchedChanged (ev) {
|
|
|
|
|
// pause : reflète le statut localement ; archivage : recharge la base (le tech disparaît). Congés : recharge la semaine (hachures).
|
|
|
|
|
if (ev && ev.status) { const tt = techs.value.find(x => x.id === ev.id); if (tt) tt.status = ev.status }
|
|
|
|
|
@ -3995,8 +4023,8 @@ const diffKeys = computed(() => { const cur = currentSet.value; const srv = serv
|
|
|
|
|
// #2/#3 — « dirty » = quarts NON PUBLIÉS (statut ≠ Publié). Tout est AUTO-SAUVÉ en brouillon → plus d'alerte « modifications non publiées » à la navigation.
|
|
|
|
|
const unpublished = computed(() => assignments.value.filter(a => a.status && a.status !== 'Publié'))
|
|
|
|
|
// « dirty » = quarts NON PUBLIÉS + congés en attente (publish-required) → tout doit passer par « Publier ».
|
|
|
|
|
const dirty = computed(() => unpublished.value.length > 0 || pendingAbsCount.value > 0)
|
|
|
|
|
const dirtyCount = computed(() => unpublished.value.length + pendingAbsCount.value)
|
|
|
|
|
const dirty = computed(() => unpublished.value.length > 0 || pendingAbsCount.value > 0 || pendingPauseCount.value > 0)
|
|
|
|
|
const dirtyCount = computed(() => unpublished.value.length + pendingAbsCount.value + pendingPauseCount.value)
|
|
|
|
|
const dirtyCells = computed(() => { const s = new Set(unpublished.value.map(a => a.tech + '|' + a.date)); for (const k of Object.keys(pendingAbs.value)) s.add(k); return s })
|
|
|
|
|
function isCellDirty (techId, iso) { return dirtyCells.value.has(techId + '|' + iso) }
|
|
|
|
|
// Statut agrégé de la semaine (pour l'affichage #3) : le « plus bas » statut non publié présent.
|
|
|
|
|
@ -4011,7 +4039,7 @@ const pubSummary = computed(() => {
|
|
|
|
|
const nameOf = id => { const t = (techs.value || []).find(x => x.id === id); return (t && t.name) || id }
|
|
|
|
|
const shiftLabel = s => { const t = tplByName.value[s]; return (t && t.template_name) || s }
|
|
|
|
|
const byTech = {}
|
|
|
|
|
const bucket = tid => (byTech[tid] = byTech[tid] || { tech: tid, name: nameOf(tid), add: [], rem: [], abs: [] })
|
|
|
|
|
const bucket = tid => (byTech[tid] = byTech[tid] || { tech: tid, name: nameOf(tid), add: [], rem: [], abs: [], pause: null })
|
|
|
|
|
// Quarts à publier = état courant non Publié (Proposé/Soumis/Approuvé → deviendront Publié)
|
|
|
|
|
for (const a of unpublished.value) bucket(a.tech).add.push({ date: a.date, shift: a.shift_name || shiftLabel(a.shift) })
|
|
|
|
|
// Quarts RETIRÉS = présents au chargement mais plus dans l'état courant → seront supprimés au Publier
|
|
|
|
|
@ -4030,11 +4058,18 @@ const pubSummary = computed(() => {
|
|
|
|
|
bucket(key.slice(0, i1)).abs.push({ date: key.slice(i1 + 1), type: set ? (v.type || 'Congé') : null, op: set ? 'set' : 'remove' })
|
|
|
|
|
if (set) absAdded++; else absRemoved++
|
|
|
|
|
}
|
|
|
|
|
// PAUSES INDÉFINIES en attente : marque le tech + liste ses quarts FUTURS (aujourd'hui→) qui seront RETIRÉS au Publier.
|
|
|
|
|
// (Ces quarts sont encore dans assignments — donc pas déjà comptés en « rem » ci-dessus ; on les ajoute ici.)
|
|
|
|
|
const todayIso = todayISO(); let paused = 0
|
|
|
|
|
for (const [tid, v] of Object.entries(pendingPause.value || {})) {
|
|
|
|
|
const g = bucket(tid); g.pause = { reason: (v && v.reason) || '' }; paused++
|
|
|
|
|
for (const a of assignments.value) { if (a.tech === tid && a.date >= todayIso) g.rem.push({ date: a.date, shift: a.shift_name || shiftLabel(a.shift), pause: true }) }
|
|
|
|
|
}
|
|
|
|
|
const list = Object.values(byTech)
|
|
|
|
|
const byDate = (x, y) => String(x.date).localeCompare(String(y.date))
|
|
|
|
|
for (const g of list) { g.add.sort(byDate); g.rem.sort(byDate); g.abs.sort(byDate) }
|
|
|
|
|
list.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
|
|
|
return { added: unpublished.value.length, removed, absAdded, absRemoved, techs: list }
|
|
|
|
|
return { added: unpublished.value.length, removed, absAdded, absRemoved, paused, techs: list }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const holSet = computed(() => new Set(holidays.value))
|
|
|
|
|
@ -4074,6 +4109,15 @@ const LS_PENDING_ABS = 'roster-pending-abs-v1'
|
|
|
|
|
const pendingAbs = ref({})
|
|
|
|
|
function savePendingAbs () { try { localStorage.setItem(LS_PENDING_ABS, JSON.stringify(pendingAbs.value)) } catch (e) {} }
|
|
|
|
|
const pendingAbsCount = computed(() => Object.keys(pendingAbs.value).length)
|
|
|
|
|
// PAUSE INDÉFINIE en attente (publish-required, comme les congés) : { [techId]: { reason } }. Prend effet VISUELLEMENT
|
|
|
|
|
// (badge « pause à publier » + quarts futurs listés « à retirer » dans le résumé), mais n'est COMMISE qu'au Publier
|
|
|
|
|
// (status En pause + suppression des quarts futurs + relève des jobs). Persistée (survit au refresh, fait partie du brouillon).
|
|
|
|
|
const LS_PENDING_PAUSE = 'roster-pending-pause-v1'
|
|
|
|
|
const pendingPause = ref({})
|
|
|
|
|
function savePendingPause () { try { localStorage.setItem(LS_PENDING_PAUSE, JSON.stringify(pendingPause.value)) } catch (e) {} }
|
|
|
|
|
const pendingPauseCount = computed(() => Object.keys(pendingPause.value).length)
|
|
|
|
|
const isServerPaused = (t) => !!(t && t.status === 'En pause')
|
|
|
|
|
const isEffectivelyPaused = (id) => !!pendingPause.value[id] || isServerPaused((techs.value || []).find(t => t.id === id))
|
|
|
|
|
// Type d'absence EFFECTIF = état serveur écrasé par le delta local en attente. null = pas absent.
|
|
|
|
|
function effAbsType (techId, iso) { const k = techId + '|' + iso; const p = pendingAbs.value[k]; if (p) return p.op === 'set' ? (p.type || 'Congé') : null; return absByTechDay.value[k] || null }
|
|
|
|
|
function isAbsent (techId, iso) { return !!effAbsType(techId, iso) }
|
|
|
|
|
@ -5696,14 +5740,31 @@ async function flushPendingAbsences () {
|
|
|
|
|
async function doPublishConfirmed () {
|
|
|
|
|
publishing.value = true
|
|
|
|
|
try {
|
|
|
|
|
// PAUSES en attente : la charge publiée EXCLUT les quarts futurs (aujourd'hui→) des techs mis en pause → publish-week
|
|
|
|
|
// les SUPPRIME côté serveur (diff). Les quarts passés restent (historique). Rien retiré si aucune pause.
|
|
|
|
|
const pausedIds = Object.keys(pendingPause.value || {})
|
|
|
|
|
const todayIso = todayISO()
|
|
|
|
|
const payload = pausedIds.length
|
|
|
|
|
? assignments.value.filter(a => !(pausedIds.includes(a.tech) && a.date >= todayIso))
|
|
|
|
|
: assignments.value
|
|
|
|
|
// Mode 'publish' : promeut les brouillons (Proposé/Soumis/Approuvé) → Publié + SMS. Les éditions étaient déjà auto-sauvées.
|
|
|
|
|
const r = await roster.publishWeek(start.value, days.value, assignments.value, notifySms.value, 'publish')
|
|
|
|
|
const r = await roster.publishWeek(start.value, days.value, payload, notifySms.value, 'publish')
|
|
|
|
|
const done = (r.created || 0) + (r.promoted || 0)
|
|
|
|
|
const abs = await flushPendingAbsences() // congés en attente → serveur
|
|
|
|
|
$q.notify({ type: r.errors ? 'warning' : 'positive', message: `Publié : ${done} quart(s)` + (abs.n ? ` · ${abs.n} congé(s)` : '') + (r.deleted ? ` (${r.deleted} retirés)` : '') + (r.errors ? ` · ${r.errors} erreurs` : '') + (r.notified ? ` · ${r.notified} SMS` : '') })
|
|
|
|
|
// PAUSES : applique le statut « En pause » (retire du dispatch/solveur + stoppe la matérialisation du patron).
|
|
|
|
|
const pauseKeys = [] // tech|date futurs des techs mis en pause → relève des jobs (IROPS)
|
|
|
|
|
let pausedDone = 0
|
|
|
|
|
for (const tid of pausedIds) {
|
|
|
|
|
try { const pr = await roster.pauseTechnician(tid, true, (pendingPause.value[tid] || {}).reason || ''); if (!(pr && pr.ok === false)) pausedDone++ } catch (e) { err(e) }
|
|
|
|
|
for (const a of assignments.value) if (a.tech === tid && a.date >= todayIso) pauseKeys.push(tid + '|' + a.date)
|
|
|
|
|
}
|
|
|
|
|
if (pausedIds.length) { pendingPause.value = {}; savePendingPause() }
|
|
|
|
|
$q.notify({ type: r.errors ? 'warning' : 'positive', message: `Publié : ${done} quart(s)` + (abs.n ? ` · ${abs.n} congé(s)` : '') + (pausedDone ? ` · ${pausedDone} pause(s)` : '') + (r.deleted ? ` (${r.deleted} retirés)` : '') + (r.errors ? ` · ${r.errors} erreurs` : '') + (r.notified ? ` · ${r.notified} SMS` : '') })
|
|
|
|
|
pubConfirm.value = false
|
|
|
|
|
await loadWeek()
|
|
|
|
|
if (abs.newlyAbsent.length) await checkAbsenceImpact(abs.newlyAbsent) // IROPS : redistribue les jobs des jours désormais en congé
|
|
|
|
|
// IROPS : jobs des jours désormais indisponibles (congés + pauses) → dialogue de redistribution.
|
|
|
|
|
const impact = [...new Set([...(abs.newlyAbsent || []), ...pauseKeys])]
|
|
|
|
|
if (impact.length) await checkAbsenceImpact(impact)
|
|
|
|
|
} catch (e) { err(e) } finally { publishing.value = false }
|
|
|
|
|
}
|
|
|
|
|
// #3 — étape d'approbation FACULTATIVE : Soumettre (→ Soumis) / Approuver (→ Approuvé), sans SMS. « Publier » reste l'étape finale (SMS).
|
|
|
|
|
@ -5718,7 +5779,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 = {} } }
|
|
|
|
|
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 = {} } }
|
|
|
|
|
|
|
|
|
|
// ── 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
|
|
|
|
|
@ -6305,6 +6366,7 @@ th.clk, td.clk { cursor: pointer; }
|
|
|
|
|
tr.res-hidden { opacity: .5; background: repeating-linear-gradient(45deg, #fafafa, #fafafa 6px, #f0f0f0 6px, #f0f0f0 12px); } /* ressource masquée affichée en grisé */
|
|
|
|
|
tr.res-hidden .hide-eye { opacity: 1; }
|
|
|
|
|
.tech-name.clk:hover { text-decoration: underline; }
|
|
|
|
|
.tech-name.tech-paused-pending { color: var(--ops-text-muted, #94a3b8); text-decoration: line-through; }
|
|
|
|
|
.hol-toggle { font-size: 9px; color: #ccc; cursor: pointer; border: 1px solid #eee; border-radius: 3px; width: 14px; margin: 1px auto 0; line-height: 12px; }
|
|
|
|
|
.hol-toggle.on { background: #ff9800; color: #fff; border-color: #ff9800; }
|
|
|
|
|
.cell { cursor: pointer; min-height: 24px; min-width: 132px; position: relative; background: #eef1f4; } /* gris pâle = INDISPONIBLE par défaut ; un quart crée une zone blanche (.tl-shift) */
|
|
|
|
|
|