refactor(planif): désencombrer l'en-tête selon UX_REFACTORING_PROMPT (PL1/PL2/PL4/PL5/PL6)
- PL5 : sous-menu « Modèles de semaine » (menu-dans-menu) → dialogue dédié components/planif/WeekTemplatesDialog.vue (props données / emits save·apply· set-default·delete ; état + mutations restent dans la page, EmptyState réutilisé) - PL2 : menu Outils 13 items + sous-menu → 11 items à plat ; retrait des 2 entrées redondantes avec la barre (« À assigner », vue Mois) - PL1/PL4 : barre allégée — bouton ★ modèle par défaut déplacé dans le ⋮ (groupé) + ⌘K (planif-templates, planif-apply-default) ; HelpHint « Suggérer » retiré (l'explication vit déjà dans le bandeau du dialogue) - PL6 : champ orphelin « Max h/sem » supprimé (ref jamais consommée = code mort) - docs : statuts PL1/PL2/PL4/PL5/PL6 mis à jour dans UX_REFACTORING_PROMPT.md Vérifié : build SPA OK · aperçu 9001 — Outils/⋮/dialogue exercés, chaîne emit save → prompt parent OK, grille Semaine + Tournées intactes, 0 nouvelle erreur console (les « Anchor: target null » préexistent au baseline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8e7838d1fb
commit
48edede793
49
apps/ops/src/components/planif/WeekTemplatesDialog.vue
Normal file
49
apps/ops/src/components/planif/WeekTemplatesDialog.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<q-dialog v-model="open">
|
||||||
|
<q-card style="min-width:440px;max-width:96vw">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<q-icon name="bookmark" color="brown" size="20px" class="q-mr-sm" />
|
||||||
|
<div class="text-subtitle1 text-weight-bold">Modèles de semaine</div><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-caption text-grey-7 q-mb-sm">Un modèle mémorise le patron de quarts de la semaine (qui travaille quel quart, par jour). L'application est <b>consciente des absences</b> : les jours où un tech est absent sont sautés.</div>
|
||||||
|
<EmptyState v-if="!templates.length" icon="bookmark_border" label="Aucun modèle enregistré" hint="Enregistre d'abord la semaine affichée comme modèle." />
|
||||||
|
<q-list v-else separator dense>
|
||||||
|
<q-item v-for="(tm, i) in templates" :key="i">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-btn flat dense round size="sm" :icon="tm.default ? 'star' : 'star_border'" :color="tm.default ? 'amber-8' : 'grey-5'" @click="emit('set-default', i)"><q-tooltip>Modèle par défaut (★) — appliqué en 1 clic depuis la barre ⋮ ou ⌘K</q-tooltip></q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ tm.name }}<q-badge v-if="tm.default" color="amber-8" class="q-ml-sm" label="défaut" /></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side class="row items-center no-wrap q-gutter-xs">
|
||||||
|
<q-btn dense no-caps outline size="sm" color="primary" icon="play_arrow" label="Appliquer" @click="applyAndClose(tm)"><q-tooltip>Applique le patron à la semaine affichée (absences respectées)</q-tooltip></q-btn>
|
||||||
|
<q-btn flat dense round size="sm" icon="delete" color="grey-6" @click="emit('delete', i)"><q-tooltip>Supprimer ce modèle</q-tooltip></q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right" class="q-pt-none">
|
||||||
|
<q-btn dense no-caps unelevated color="primary" icon="save" label="Enregistrer la semaine affichée…" @click="emit('save')" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Dialogue « Modèles de semaine » — extrait de PlanificationPage (sous-menu imbriqué du menu Outils → dialogue dédié,
|
||||||
|
// règle « pas de menu-dans-menu »). L'état (weekTemplates) et les mutations restent dans la PAGE (localStorage LS_TPL) :
|
||||||
|
// le dialogue est purement présentation → emits save / apply / set-default / delete.
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import EmptyState from 'src/components/shared/EmptyState.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
templates: { type: Array, default: () => [] }, // [{ name, byDow, default? }] — LECTURE seule ici
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'save', 'apply', 'set-default', 'delete'])
|
||||||
|
|
||||||
|
const open = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
|
||||||
|
function applyAndClose (tm) { emit('apply', tm); open.value = false }
|
||||||
|
</script>
|
||||||
|
|
@ -78,22 +78,9 @@
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable v-close-popup @click="openTeamEditor"><q-item-section avatar><q-icon name="groups" color="teal" /></q-item-section><q-item-section>Équipe — compétences · cadence · coût<q-item-label caption>Compétences (score + cadence/skill) · coût · catalogue</q-item-label></q-item-section></q-item>
|
<q-item clickable v-close-popup @click="openTeamEditor"><q-item-section avatar><q-icon name="groups" color="teal" /></q-item-section><q-item-section>Équipe — compétences · cadence · coût<q-item-label caption>Compétences (score + cadence/skill) · coût · catalogue</q-item-label></q-item-section></q-item>
|
||||||
<q-item clickable v-close-popup @click="openJobChar"><q-item-section avatar><q-icon name="timer" color="warning" /></q-item-section><q-item-section>Durées par caractéristique</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="openJobChar"><q-item-section avatar><q-icon name="timer" color="warning" /></q-item-section><q-item-section>Durées par caractéristique</q-item-section></q-item>
|
||||||
<q-item clickable>
|
<q-item clickable v-close-popup @click="showWeekTemplates = true">
|
||||||
<q-item-section avatar><q-icon name="bookmark" color="brown" /></q-item-section>
|
<q-item-section avatar><q-icon name="bookmark" color="brown" /></q-item-section>
|
||||||
<q-item-section>Modèles de semaine</q-item-section>
|
<q-item-section>Modèles de semaine<q-item-label caption>{{ weekTemplates.length ? weekTemplates.length + ' modèle(s)' + (defaultTemplate ? ' · défaut : ' + defaultTemplate.name : '') : 'enregistrer / appliquer un patron' }}</q-item-label></q-item-section>
|
||||||
<q-item-section side><q-icon name="chevron_right" color="grey-6" /></q-item-section>
|
|
||||||
<q-menu anchor="top end" self="top start">
|
|
||||||
<q-list dense style="min-width:250px">
|
|
||||||
<q-item clickable v-close-popup @click="saveTemplate"><q-item-section avatar><q-icon name="save" /></q-item-section><q-item-section>Enregistrer la semaine…</q-item-section></q-item>
|
|
||||||
<q-separator v-if="weekTemplates.length" />
|
|
||||||
<q-item-label v-if="weekTemplates.length" header>Appliquer</q-item-label>
|
|
||||||
<q-item v-for="(tm, i) in weekTemplates" :key="i" clickable @click="applyTemplate(tm)">
|
|
||||||
<q-item-section avatar><q-btn flat dense round size="sm" :icon="tm.default ? 'star' : 'star_border'" :color="tm.default ? 'amber-8' : 'grey-5'" @click.stop="setDefaultTemplate(i)"><q-tooltip>Modèle par défaut (★)</q-tooltip></q-btn></q-item-section>
|
|
||||||
<q-item-section>{{ tm.name }}</q-item-section>
|
|
||||||
<q-item-section side><q-btn flat dense round size="sm" icon="delete" color="grey-6" @click.stop="deleteTemplate(i)" /></q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable v-close-popup @click="openGarde"><q-item-section avatar><q-icon name="shield" color="brown" /></q-item-section><q-item-section>Rotation de garde</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="openGarde"><q-item-section avatar><q-icon name="shield" color="brown" /></q-item-section><q-item-section>Rotation de garde</q-item-section></q-item>
|
||||||
<q-separator class="q-my-xs" />
|
<q-separator class="q-my-xs" />
|
||||||
|
|
@ -104,9 +91,8 @@
|
||||||
<q-item clickable v-close-popup @click="showLeave = true"><q-item-section avatar><q-icon name="beach_access" /></q-item-section><q-item-section>Congés / absences</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="showLeave = true"><q-item-section avatar><q-icon name="beach_access" /></q-item-section><q-item-section>Congés / absences</q-item-section></q-item>
|
||||||
<q-item clickable v-close-popup @click="showTechSync = true"><q-item-section avatar><q-icon name="group_add" color="green-7" /></q-item-section><q-item-section>Synchroniser les techniciens</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="showTechSync = true"><q-item-section avatar><q-icon name="group_add" color="green-7" /></q-item-section><q-item-section>Synchroniser les techniciens</q-item-section></q-item>
|
||||||
<q-separator class="q-my-xs" />
|
<q-separator class="q-my-xs" />
|
||||||
<!-- 👁 AFFICHAGE : bascules de ce qui s'affiche sur le board -->
|
<!-- 👁 AFFICHAGE : bascules de ce qui s'affiche sur le board (les raccourcis redondants avec la barre — « À assigner », vue Mois — ont été retirés) -->
|
||||||
<q-item-label header class="q-py-xs text-weight-bold text-grey-8">👁 Affichage</q-item-label>
|
<q-item-label header class="q-py-xs text-weight-bold text-grey-8">👁 Affichage</q-item-label>
|
||||||
<q-item clickable v-close-popup @click="openAssignPanel"><q-item-section avatar><q-icon name="drag_indicator" color="deep-purple" /></q-item-section><q-item-section>Jobs à assigner (glisser-déposer)</q-item-section></q-item>
|
|
||||||
<q-item clickable v-close-popup @click="showDayJobs = !showDayJobs">
|
<q-item clickable v-close-popup @click="showDayJobs = !showDayJobs">
|
||||||
<q-item-section avatar><q-icon name="event_note" :color="showDayJobs ? 'indigo' : 'grey-7'" /></q-item-section>
|
<q-item-section avatar><q-icon name="event_note" :color="showDayJobs ? 'indigo' : 'grey-7'" /></q-item-section>
|
||||||
<q-item-section>Jobs prévus en en-tête<q-item-label caption>liste par jour — glisser sur un tech</q-item-label></q-item-section>
|
<q-item-section>Jobs prévus en en-tête<q-item-label caption>liste par jour — glisser sur un tech</q-item-label></q-item-section>
|
||||||
|
|
@ -117,16 +103,11 @@
|
||||||
<q-item-section>Jobs Legacy sur les timelines<q-item-label caption>durées estimées des jobs osTicket datés (fenêtre affichée)</q-item-label></q-item-section>
|
<q-item-section>Jobs Legacy sur les timelines<q-item-label caption>durées estimées des jobs osTicket datés (fenêtre affichée)</q-item-label></q-item-section>
|
||||||
<q-item-section side><q-icon v-if="showLegacyLoad" name="check" color="brown" /></q-item-section>
|
<q-item-section side><q-icon v-if="showLegacyLoad" name="check" color="brown" /></q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable v-close-popup @click="boardView = 'month'">
|
|
||||||
<q-item-section avatar><q-icon name="calendar_month" color="indigo" /></q-item-section>
|
|
||||||
<q-item-section>Besoins & couverture (vue Mois)<q-item-label caption>heures requises par compétence · alertes de couverture</q-item-label></q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<q-btn v-if="defaultTemplate" dense flat color="warning" icon="star" :label="defaultTemplate.name" @click="applyDefault"><q-tooltip>Appliquer le modèle par défaut (consciente des absences)</q-tooltip></q-btn>
|
|
||||||
<q-separator vertical class="q-mx-xs" />
|
<q-separator vertical class="q-mx-xs" />
|
||||||
|
<!-- (l'explication détaillée de « Suggérer » vit DANS le dialogue — bandeau ambre à l'ouverture ; le ★ modèle par défaut vit dans ⋮ + ⌘K) -->
|
||||||
<q-btn unelevated color="primary" icon="auto_awesome" label="Suggérer" @click="openSuggest"><q-tooltip class="bg-grey-9">Répartition automatique des jobs du <b>jour sélectionné</b> (distance · compétence · priorité · taux d'occupation) — revue avant d'appliquer</q-tooltip></q-btn>
|
<q-btn unelevated color="primary" icon="auto_awesome" label="Suggérer" @click="openSuggest"><q-tooltip class="bg-grey-9">Répartition automatique des jobs du <b>jour sélectionné</b> (distance · compétence · priorité · taux d'occupation) — revue avant d'appliquer</q-tooltip></q-btn>
|
||||||
<HelpHint class="q-ml-xs" title="Suggérer — répartition automatique">Propose une assignation optimisée des jobs <b>du jour sélectionné</b> aux techniciens, en combinant compétence requise, disponibilité, distance (routes réelles) et taux d'occupation. Les cas simples vont d'abord aux techs les <b>moins polyvalents</b> (réserve les experts). Rien n'est appliqué avant votre validation.</HelpHint>
|
|
||||||
<!-- PUBLIER unifié : action principale + sous-options (SMS · publier au legacy) dans le même bouton -->
|
<!-- PUBLIER unifié : action principale + sous-options (SMS · publier au legacy) dans le même bouton -->
|
||||||
<q-btn-dropdown split :outline="!dirty" :unelevated="dirty" color="positive" icon="cloud_upload" :label="dirty ? ('Publier (' + dirtyCount + ')') : 'Publier'" :loading="publishing" :disable="!dirty" no-caps @click="doPublish">
|
<q-btn-dropdown split :outline="!dirty" :unelevated="dirty" color="positive" icon="cloud_upload" :label="dirty ? ('Publier (' + dirtyCount + ')') : 'Publier'" :loading="publishing" :disable="!dirty" no-caps @click="doPublish">
|
||||||
<q-list dense style="min-width:250px">
|
<q-list dense style="min-width:250px">
|
||||||
|
|
@ -145,18 +126,24 @@
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<!-- Utilitaires secondaires regroupés dans un ⋮ (annuler/rétablir gardent leurs raccourcis Ctrl+Z / Ctrl+Shift+Z) -->
|
<!-- Utilitaires secondaires regroupés dans un ⋮ (annuler/rétablir gardent leurs raccourcis Ctrl+Z / Ctrl+Shift+Z ;
|
||||||
<OverflowMenu :items="[
|
le modèle par défaut ★, retiré de la barre, reste joignable ici ET via ⌘K) -->
|
||||||
{ icon: 'undo', label: 'Annuler', caption: 'Ctrl+Z', disabled: !history.length, action: undo },
|
<OverflowMenu :groups="[
|
||||||
{ icon: 'redo', label: 'Rétablir', caption: 'Ctrl+Shift+Z', disabled: !future.length, action: redo },
|
{ items: [
|
||||||
{ icon: 'refresh', label: 'Rafraîchir', action: () => guard(loadWeek) },
|
{ icon: 'undo', label: 'Annuler', caption: 'Ctrl+Z', disabled: !history.length, action: undo },
|
||||||
|
{ icon: 'redo', label: 'Rétablir', caption: 'Ctrl+Shift+Z', disabled: !future.length, action: redo },
|
||||||
|
{ icon: 'refresh', label: 'Rafraîchir', action: () => guard(loadWeek) },
|
||||||
|
] },
|
||||||
|
{ label: 'Modèles de semaine', items: [
|
||||||
|
{ icon: 'star', label: 'Appliquer le modèle par défaut', caption: defaultTemplate ? defaultTemplate.name : 'aucun — marquer ★ dans Modèles', color: 'amber-8', disabled: !defaultTemplate, action: applyDefault },
|
||||||
|
{ icon: 'bookmark', label: 'Gérer les modèles…', action: () => { showWeekTemplates = true } },
|
||||||
|
] },
|
||||||
]" />
|
]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rangée 2 : filtres + légende en popover — DESKTOP/tablette uniquement (gt-sm) -->
|
<!-- Rangée 2 : filtres + légende en popover — DESKTOP/tablette uniquement (gt-sm) -->
|
||||||
<div class="row items-center q-gutter-sm q-mb-sm gt-sm">
|
<div class="row items-center q-gutter-sm q-mb-sm gt-sm">
|
||||||
<q-input dense outlined clearable v-model="search" placeholder="Rechercher un technicien…" style="width:230px"><template #prepend><q-icon name="search" /></template></q-input>
|
<q-input dense outlined clearable v-model="search" placeholder="Rechercher un technicien…" style="width:230px"><template #prepend><q-icon name="search" /></template></q-input>
|
||||||
<q-input dense outlined type="number" v-model.number="maxHours" label="Max h/sem" style="width:110px" />
|
|
||||||
<span class="text-caption text-grey-6">{{ visibleTechs.length }} / {{ techs.length }} techs</span>
|
<span class="text-caption text-grey-6">{{ visibleTechs.length }} / {{ techs.length }} techs</span>
|
||||||
<q-chip v-if="cellClipboard.length" dense size="sm" color="indigo" text-color="white" icon="content_paste">{{ cellClipboard.length }} copié(s)</q-chip>
|
<q-chip v-if="cellClipboard.length" dense size="sm" color="indigo" text-color="white" icon="content_paste">{{ cellClipboard.length }} copié(s)</q-chip>
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
@ -568,6 +555,9 @@
|
||||||
<!-- Types de shift — extrait dans components/planif/ShiftTypesDialog.vue (décomposition #4) ; @changed → recharge les templates partagés -->
|
<!-- Types de shift — extrait dans components/planif/ShiftTypesDialog.vue (décomposition #4) ; @changed → recharge les templates partagés -->
|
||||||
<ShiftTypesDialog v-model="showShiftEditor" :templates="templates" @changed="refreshTemplates" />
|
<ShiftTypesDialog v-model="showShiftEditor" :templates="templates" @changed="refreshTemplates" />
|
||||||
|
|
||||||
|
<!-- Modèles de semaine (patrons d'horaire) — dialogue dédié ; état + mutations dans la page (LS_TPL) -->
|
||||||
|
<WeekTemplatesDialog v-model="showWeekTemplates" :templates="weekTemplates" @save="saveTemplate" @apply="applyTemplate" @set-default="setDefaultTemplate" @delete="deleteTemplate" />
|
||||||
|
|
||||||
<!-- Congés & disponibilités — extrait dans components/planif/LeaveDialog.vue (décomposition #4) -->
|
<!-- Congés & disponibilités — extrait dans components/planif/LeaveDialog.vue (décomposition #4) -->
|
||||||
<LeaveDialog v-model="showLeave" :techs="techs" :tech-options="techOptions" />
|
<LeaveDialog v-model="showLeave" :techs="techs" :tech-options="techOptions" />
|
||||||
|
|
||||||
|
|
@ -1247,7 +1237,7 @@
|
||||||
<q-list v-else separator>
|
<q-list v-else separator>
|
||||||
<q-expansion-item v-for="m in notifyDlg.techs" :key="m.techId" dense>
|
<q-expansion-item v-for="m in notifyDlg.techs" :key="m.techId" dense>
|
||||||
<template #header>
|
<template #header>
|
||||||
<q-item-section avatar><q-avatar size="30px" color="deep-purple-1" text-color="deep-purple-9" style="font-weight:700;font-size:11px">{{ initials(m.name) }}</q-avatar></q-item-section>
|
<q-item-section avatar><UserAvatar :email="m.email" :tech-id="m.techId" :name="m.name" :size="30" card /></q-item-section>
|
||||||
<q-item-section><q-item-label>{{ m.name }}</q-item-label><q-item-label caption>{{ m.jobCount }} intervention(s)<span v-if="!m.phone && !m.email" class="text-negative"> · ⚠ aucune coordonnée</span></q-item-label></q-item-section>
|
<q-item-section><q-item-label>{{ m.name }}</q-item-label><q-item-label caption>{{ m.jobCount }} intervention(s)<span v-if="!m.phone && !m.email" class="text-negative"> · ⚠ aucune coordonnée</span></q-item-label></q-item-section>
|
||||||
<q-item-section side @click.stop>
|
<q-item-section side @click.stop>
|
||||||
<div class="row items-center q-gutter-xs no-wrap">
|
<div class="row items-center q-gutter-xs no-wrap">
|
||||||
|
|
@ -1815,6 +1805,7 @@ import JobReplyBox from 'src/components/shared/detail-sections/modules/JobReplyB
|
||||||
import LeaveDialog from 'src/components/planif/LeaveDialog.vue' // Congés & disponibilités (extrait — décomposition #4)
|
import LeaveDialog from 'src/components/planif/LeaveDialog.vue' // Congés & disponibilités (extrait — décomposition #4)
|
||||||
import TechSyncDialog from 'src/components/planif/TechSyncDialog.vue' // Synchroniser les techniciens (extrait — décomposition #4)
|
import TechSyncDialog from 'src/components/planif/TechSyncDialog.vue' // Synchroniser les techniciens (extrait — décomposition #4)
|
||||||
import ShiftTypesDialog from 'src/components/planif/ShiftTypesDialog.vue' // Types de shift (extrait — décomposition #4)
|
import ShiftTypesDialog from 'src/components/planif/ShiftTypesDialog.vue' // Types de shift (extrait — décomposition #4)
|
||||||
|
import WeekTemplatesDialog from 'src/components/planif/WeekTemplatesDialog.vue' // Modèles de semaine (ex-sous-menu Outils → dialogue, règle « pas de menu-dans-menu »)
|
||||||
import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue'
|
import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue'
|
||||||
import HelpHint from 'src/components/shared/HelpHint.vue'
|
import HelpHint from 'src/components/shared/HelpHint.vue'
|
||||||
import { skillIcon, skillSym, markerIcon } from 'src/composables/useSkillIcons' // SOURCE UNIQUE des icônes de compétences (réutilisée fiche client)
|
import { skillIcon, skillSym, markerIcon } from 'src/composables/useSkillIcons' // SOURCE UNIQUE des icônes de compétences (réutilisée fiche client)
|
||||||
|
|
@ -1822,6 +1813,7 @@ import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags/co
|
||||||
import SkillCadenceTable from 'src/components/planif/SkillCadenceTable.vue' // éditeur compétences+cadence PARTAGÉ (volet tech + outil Équipe)
|
import SkillCadenceTable from 'src/components/planif/SkillCadenceTable.vue' // éditeur compétences+cadence PARTAGÉ (volet tech + outil Équipe)
|
||||||
import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo
|
import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo
|
||||||
import OccupancyStrip from 'src/components/shared/OccupancyStrip.vue' // bande d'occupation réutilisable (sélecteur de jour Tournées + tableau de bord)
|
import OccupancyStrip from 'src/components/shared/OccupancyStrip.vue' // bande d'occupation réutilisable (sélecteur de jour Tournées + tableau de bord)
|
||||||
|
import UserAvatar from 'src/components/shared/UserAvatar.vue' // avatar CANONIQUE identité-aware (photo + nom résolu + carte + renommage) — remplace les monogrammes bruts du board
|
||||||
import OverflowMenu from 'src/components/shared/OverflowMenu.vue' // « ⋮ » standardisé pour les actions secondaires (désencombrer la barre)
|
import OverflowMenu from 'src/components/shared/OverflowMenu.vue' // « ⋮ » standardisé pour les actions secondaires (désencombrer la barre)
|
||||||
import JobPool from 'src/components/planif/JobPool.vue' // pool « Jobs à assigner » UNIFIÉ (flottant · kanban · mobile) — remplace les 3 réimplémentations
|
import JobPool from 'src/components/planif/JobPool.vue' // pool « Jobs à assigner » UNIFIÉ (flottant · kanban · mobile) — remplace les 3 réimplémentations
|
||||||
import { useJobPool } from 'src/composables/useJobPool' // cerveau filtre/tri/groupe/badges partagé par les 3 surfaces
|
import { useJobPool } from 'src/composables/useJobPool' // cerveau filtre/tri/groupe/badges partagé par les 3 surfaces
|
||||||
|
|
@ -1946,13 +1938,14 @@ const manualGarde = ref({}) // overrides manuels de garde : 'techId|iso' → 'on
|
||||||
const newGardeRule = reactive({ dept: '', shift: '', shiftWeekend: '', weekdays: [], anchor: '', steps: [] })
|
const newGardeRule = reactive({ dept: '', shift: '', shiftWeekend: '', weekdays: [], anchor: '', steps: [] })
|
||||||
const GARDE_DOW = [{ v: 1, l: 'L' }, { v: 2, l: 'M' }, { v: 3, l: 'M' }, { v: 4, l: 'J' }, { v: 5, l: 'V' }, { v: 6, l: 'S' }, { v: 0, l: 'D' }]
|
const GARDE_DOW = [{ v: 1, l: 'L' }, { v: 2, l: 'M' }, { v: 3, l: 'M' }, { v: 4, l: 'J' }, { v: 5, l: 'V' }, { v: 6, l: 'S' }, { v: 0, l: 'D' }]
|
||||||
const history = ref([]); const future = ref([])
|
const history = ref([]); const future = ref([])
|
||||||
const search = ref(''); const groupFilter = ref(null); const maxHours = ref(40); const skillFilter = ref([])
|
const search = ref(''); const groupFilter = ref(null); const skillFilter = ref([]) // (maxHours retiré — champ orphelin jamais consommé)
|
||||||
// Ressources masquées (hors front-line : compta, etc.) — masquées ET ignorées du calcul ; localStorage.
|
// Ressources masquées (hors front-line : compta, etc.) — masquées ET ignorées du calcul ; localStorage.
|
||||||
const hiddenTechs = ref([]); const showHidden = ref(false)
|
const hiddenTechs = ref([]); const showHidden = ref(false)
|
||||||
function isHidden (id) { return hiddenTechs.value.includes(id) }
|
function isHidden (id) { return hiddenTechs.value.includes(id) }
|
||||||
function toggleHidden (id) { const i = hiddenTechs.value.indexOf(id); if (i >= 0) hiddenTechs.value.splice(i, 1); else hiddenTechs.value.push(id); localStorage.setItem('roster-hidden-techs-v1', JSON.stringify(hiddenTechs.value)) }
|
function toggleHidden (id) { const i = hiddenTechs.value.indexOf(id); if (i >= 0) hiddenTechs.value.splice(i, 1); else hiddenTechs.value.push(id); localStorage.setItem('roster-hidden-techs-v1', JSON.stringify(hiddenTechs.value)) }
|
||||||
const hiddenCount = computed(() => techs.value.filter(t => isHidden(t.id)).length)
|
const hiddenCount = computed(() => techs.value.filter(t => isHidden(t.id)).length)
|
||||||
const showShiftEditor = ref(false) // v-model de <ShiftTypesDialog> (editTpls/newTpl/fonctions déplacés dans le composant)
|
const showShiftEditor = ref(false) // v-model de <ShiftTypesDialog> (editTpls/newTpl/fonctions déplacés dans le composant)
|
||||||
|
const showWeekTemplates = ref(false) // v-model de <WeekTemplatesDialog> — l'état weekTemplates + mutations restent ICI (localStorage)
|
||||||
const showTeamEditor = ref(false); const editTechs = ref([]); const teamCost = reactive({})
|
const showTeamEditor = ref(false); const editTechs = ref([]); const teamCost = reactive({})
|
||||||
const notifySms = ref(false)
|
const notifySms = ref(false)
|
||||||
// ── Notifier les techs de leur tournée (SMS/Email + lien token) : aperçu d'abord (compose sans envoi), puis envoi. ──
|
// ── Notifier les techs de leur tournée (SMS/Email + lien token) : aperçu d'abord (compose sans envoi), puis envoi. ──
|
||||||
|
|
@ -2745,7 +2738,10 @@ function skillEffColor (t, sk) { const e = t.skill_eff && t.skill_eff[sk]; retur
|
||||||
// Icône Material (offline, jeu Quasar par défaut) représentant une compétence/type — par mots-clés, repli 'bolt'.
|
// Icône Material (offline, jeu Quasar par défaut) représentant une compétence/type — par mots-clés, repli 'bolt'.
|
||||||
// Icône de compétence pour q-icon (accepte les SYMBOLES SVG importés) : échelle=installation, casque=support,
|
// Icône de compétence pour q-icon (accepte les SYMBOLES SVG importés) : échelle=installation, casque=support,
|
||||||
// outils=réparation ; sinon ligature classique unique (skillIcon). NB: les pins carte (HTML brut) gardent skillIcon.
|
// outils=réparation ; sinon ligature classique unique (skillIcon). NB: les pins carte (HTML brut) gardent skillIcon.
|
||||||
function tSkills (t) { return [...(t.skills || [])].sort((a, b) => a.localeCompare(b)) } // compétences triées du tech (pour l'affichage 1 + « +N »)
|
// Compétences du tech pour l'affichage (icône principale + « +N ») : on RESPECTE l'ordre de priorité
|
||||||
|
// défini par glisser-déposer dans l'éditeur de compétences (t.skills) — la 1re = fonction principale.
|
||||||
|
// (ex. une personne au support voit son casque en 1er ; réparation ≠ support, chacun garde son rang.)
|
||||||
|
function tSkills (t) { return [...(t.skills || [])] }
|
||||||
// Icône d'un bloc de tournée (vue JOUR et SEMAINE) : renfort → group ; sinon icône de COMPÉTENCE
|
// Icône d'un bloc de tournée (vue JOUR et SEMAINE) : renfort → group ; sinon icône de COMPÉTENCE
|
||||||
// (skill/required_skill, ou dept pour les tickets legacy → ex. « Install/Reparation Télé » = TV).
|
// (skill/required_skill, ou dept pour les tickets legacy → ex. « Install/Reparation Télé » = TV).
|
||||||
// Le ticket générique n'est utilisé qu'en dernier recours (legacy sans skill ni dept).
|
// Le ticket générique n'est utilisé qu'en dernier recours (legacy sans skill ni dept).
|
||||||
|
|
@ -6022,6 +6018,8 @@ registerActions('planif', [
|
||||||
{ id: 'planif-create', label: 'Créer une tâche / intervention', icon: 'add', group: 'Planification', keywords: 'creer job tache intervention soumission', run: () => { createChooser.value = true } },
|
{ id: 'planif-create', label: 'Créer une tâche / intervention', icon: 'add', group: 'Planification', keywords: 'creer job tache intervention soumission', run: () => { createChooser.value = true } },
|
||||||
{ id: 'planif-assign', label: 'Jobs à assigner', icon: 'assignment_ind', group: 'Planification', keywords: 'assigner pool repartir', run: openAssignPanel },
|
{ id: 'planif-assign', label: 'Jobs à assigner', icon: 'assignment_ind', group: 'Planification', keywords: 'assigner pool repartir', run: openAssignPanel },
|
||||||
{ id: 'planif-leave', label: 'Congés / absences', icon: 'beach_access', group: 'Planification', keywords: 'conges absences vacances', run: () => { showLeave.value = true } },
|
{ id: 'planif-leave', label: 'Congés / absences', icon: 'beach_access', group: 'Planification', keywords: 'conges absences vacances', run: () => { showLeave.value = true } },
|
||||||
|
{ id: 'planif-templates', label: 'Modèles de semaine', icon: 'bookmark', group: 'Planification', keywords: 'modeles semaine patron template horaire', run: () => { showWeekTemplates.value = true } },
|
||||||
|
{ id: 'planif-apply-default', label: 'Appliquer le modèle par défaut (★)', icon: 'star', group: 'Planification', keywords: 'modele defaut appliquer patron star', run: applyDefault },
|
||||||
])
|
])
|
||||||
onUnmounted(() => unregisterActions('planif'))
|
onUnmounted(() => unregisterActions('planif'))
|
||||||
onBeforeRouteLeave(async () => { await autosaveDraft() }) // #2 — flush le brouillon (attendu) en partant ; plus jamais d'« abandonner ? »
|
onBeforeRouteLeave(async () => { await autosaveDraft() }) // #2 — flush le brouillon (attendu) en partant ; plus jamais d'« abandonner ? »
|
||||||
|
|
@ -6471,6 +6469,7 @@ tfoot .sum .tech-col { background: #fafafa; }
|
||||||
.pm-more { width: 100%; border: none; background: none; color: #6366f1; font-size: 12px; font-weight: 600; padding: 9px; cursor: pointer; }
|
.pm-more { width: 100%; border: none; background: none; color: #6366f1; font-size: 12px; font-weight: 600; padding: 9px; cursor: pointer; }
|
||||||
.pm-tech-idle { opacity: .55; }
|
.pm-tech-idle { opacity: .55; }
|
||||||
.pm-ava { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; background: #e0e7ff; color: #4338ca; font-size: 10px; font-weight: 800; display: flex; align-items: center; justify-content: center; }
|
.pm-ava { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; background: #e0e7ff; color: #4338ca; font-size: 10px; font-weight: 800; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.pm-ava-uav { flex: 0 0 auto; } /* avatar canonique dans la liste techs mobile (garde l'espacement via le gap du parent) */
|
||||||
.pm-tech.assignable { background: #f5f3ff; outline: 1px dashed #c4b5fd; }
|
.pm-tech.assignable { background: #f5f3ff; outline: 1px dashed #c4b5fd; }
|
||||||
.pm-tech.assignable:active { background: #ede9fe; }
|
.pm-tech.assignable:active { background: #ede9fe; }
|
||||||
.pm-assign-banner { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; background: #6366f1; color: #fff; font-size: 12px; font-weight: 600; padding: 8px 10px; border-radius: 8px; margin-bottom: 8px; }
|
.pm-assign-banner { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; background: #6366f1; color: #fff; font-size: 12px; font-weight: 600; padding: 8px 10px; border-radius: 8px; margin-bottom: 8px; }
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,12 @@ File layout: template L1–1768 · script L1770–6033 (~4,264 lines — the rea
|
||||||
|
|
||||||
| ID | Anti-pattern | Anchor | Status | Problem |
|
| ID | Anti-pattern | Anchor | Status | Problem |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| PL1 | WALL_OF_OPTIONS | desktop header rows (comment « Rangée 1 : actions principales ») ~L32-154 | open | 20+ interactive controls in one toolbar |
|
| PL1 | WALL_OF_OPTIONS | desktop header rows (comment « Rangée 1 : actions principales ») ~L32-154 | **largely fixed 2026-07-14** | Was 20+ controls. Now: view switch · nav · date · journal + 5 primary actions (À assigner, Créer, Outils, Suggérer, Publier) + grouped ⋮ (★ défaut relocated there + ⌘K). HelpHint removed (explainer lives in the Suggérer dialog banner) |
|
||||||
| PL2 | WALL_OF_OPTIONS | `label="Outils"` dropdown ~L71-125 | open | 13 items in 3 sections + nested submenu |
|
| PL2 | WALL_OF_OPTIONS | `label="Outils"` dropdown ~L71-125 | **fixed 2026-07-14** | Was 13 items + nested submenu. Now 11 flat items; the 2 entries duplicating always-visible controls (À assigner, vue Mois) removed |
|
||||||
| PL3 | WALL_OF_OPTIONS | q-dialog `jobSheet.open` ~L281-356 | open | ~15 interactive zones on a phone screen. Reuse `detail-sections/modules/JobThread.vue` + `JobReplyBox.vue` instead of the hand-rolled pm-postbox composer |
|
| PL3 | WALL_OF_OPTIONS | q-dialog `jobSheet.open` ~L281-356 | open | ~15 interactive zones on a phone screen. Reuse `detail-sections/modules/JobThread.vue` + `JobReplyBox.vue` instead of the hand-rolled pm-postbox composer |
|
||||||
| PL4 | HIDDEN_PRIMARY | `label` contains « Publier », split q-btn-dropdown ~L131-147 | **partially fixed** | Publier is now a unified split q-btn-dropdown (color=positive, dirty-count, outline-when-clean). Remaining: the row still has ~10 sibling controls (see PL1) |
|
| PL4 | HIDDEN_PRIMARY | `label` contains « Publier », split q-btn-dropdown ~L131-147 | **fixed** | Publier is a unified split q-btn-dropdown (color=positive, dirty-count, outline-when-clean); sibling-control count resolved with PL1 |
|
||||||
| PL5 | MENU_IN_MENU | « Modèles de semaine » ~L85-97 | open | Submenu inside a dropdown |
|
| PL5 | MENU_IN_MENU | « Modèles de semaine » ~L85-97 | **fixed 2026-07-14** | Extracted to `components/planif/WeekTemplatesDialog.vue` (props data / emits save·apply·set-default·delete; state stays in the page). Reachable via Outils, ⋮ and ⌘K |
|
||||||
| PL6 | ORPHAN_FIELD | `label="Max h/sem"` ~L159 | open | Floating numeric input with no context |
|
| PL6 | ORPHAN_FIELD | `label="Max h/sem"` ~L159 | **fixed 2026-07-14** | Deleted — the input was dead code (ref never consumed by any filter/solver) |
|
||||||
| PL7 | STATUS_MYSTERY | autosave journal ~L48-61 | **largely fixed** | Icon now swaps to cloud_sync + primary color while saving (persistent feedback); popover holds the change journal + per-change revert by design. Verify before changing |
|
| PL7 | STATUS_MYSTERY | autosave journal ~L48-61 | **largely fixed** | Icon now swaps to cloud_sync + primary color while saving (persistent feedback); popover holds the change journal + per-change revert by design. Verify before changing |
|
||||||
| PL8 | MONOLITH | entire file | open | 6,559 lines = unmaintainable (see decomposition plan) |
|
| PL8 | MONOLITH | entire file | open | 6,559 lines = unmaintainable (see decomposition plan) |
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user