feat(planif): native OPS job creation (#17) — Dispatch deprecated, create from Planification
Job/installation creation now lives in Planification (Dispatch board is
being retired). Reuses the shared UnifiedCreateModal (work-order mode:
subject, job_type, address+RQA geocode, duration, skills/tags, tech,
date) so it's the same proven form.
- "Créer un job" button (desktop toolbar + mobile ⋮ menu) → modal →
createJob → @created refreshes the pool ("À assigner") + occupancy.
- "Créneau" button → SuggestSlotsDialog (P1-C, re-homed from Dispatch) →
pick tech+date+time → prefills the create modal.
- useUnifiedCreate: work-order flow now carries start_time + prefilled
coords (from a booked slot) through resetForm → payload.
Sync F transitoire: OPS-native jobs have NO legacy_ticket_id, so the F→
OPS sync and purgeStaleOrphans (which only touch legacy_ticket_id jobs)
never clobber them — they coexist. No F push for now (billing stays via
the sales path).
Verified: modal opens in Planification with all 9 work-order fields;
button in toolbar (gt-sm) + mobile menu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7a6baf8b5
commit
df23f232b3
|
|
@ -41,6 +41,7 @@ export function useUnifiedCreate (mode, opts = {}) {
|
||||||
address: '',
|
address: '',
|
||||||
assigned_tech: null,
|
assigned_tech: null,
|
||||||
scheduled_date: '',
|
scheduled_date: '',
|
||||||
|
start_time: '', // heure du créneau (réservation « Trouver un créneau ») — optionnel
|
||||||
depends_on: null, // parent dependency (ticket/task/job name)
|
depends_on: null, // parent dependency (ticket/task/job name)
|
||||||
tags: [], // [{ tag, level?, required? }] or ['label', ...]
|
tags: [], // [{ tag, level?, required? }] or ['label', ...]
|
||||||
_latitude: null,
|
_latitude: null,
|
||||||
|
|
@ -217,6 +218,9 @@ export function useUnifiedCreate (mode, opts = {}) {
|
||||||
form.address = ctx.address || ''
|
form.address = ctx.address || ''
|
||||||
form.assigned_tech = ctx.assigned_tech || null
|
form.assigned_tech = ctx.assigned_tech || null
|
||||||
form.scheduled_date = ctx.scheduled_date || ''
|
form.scheduled_date = ctx.scheduled_date || ''
|
||||||
|
form.start_time = ctx.start_time || ''
|
||||||
|
form._latitude = ctx._latitude != null ? ctx._latitude : (ctx.latitude != null ? ctx.latitude : null) // coords pré-remplies (réservation par créneau)
|
||||||
|
form._longitude = ctx._longitude != null ? ctx._longitude : (ctx.longitude != null ? ctx.longitude : null)
|
||||||
form.depends_on = ctx.depends_on || null
|
form.depends_on = ctx.depends_on || null
|
||||||
form.tags = ctx.tags || []
|
form.tags = ctx.tags || []
|
||||||
form._latitude = ctx.latitude || null
|
form._latitude = ctx.latitude || null
|
||||||
|
|
@ -280,6 +284,7 @@ export function useUnifiedCreate (mode, opts = {}) {
|
||||||
service_location: form.service_location || '',
|
service_location: form.service_location || '',
|
||||||
depends_on: form.depends_on || '',
|
depends_on: form.depends_on || '',
|
||||||
scheduled_date: form.scheduled_date || (form.assigned_tech ? nextWeekday() : ''),
|
scheduled_date: form.scheduled_date || (form.assigned_tech ? nextWeekday() : ''),
|
||||||
|
start_time: form.start_time || '', // heure du créneau réservé (sinon vide = non planifié à l'heure)
|
||||||
notes: form.description || '',
|
notes: form.description || '',
|
||||||
assigned_tech: form.assigned_tech || '',
|
assigned_tech: form.assigned_tech || '',
|
||||||
latitude: form._latitude || '',
|
latitude: form._latitude || '',
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
<q-item clickable v-close-popup @click="$router.push('/dispatch')"><q-item-section avatar><q-icon name="dashboard" color="indigo" /></q-item-section><q-item-section>Tableau Dispatch</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="$router.push('/dispatch')"><q-item-section avatar><q-icon name="dashboard" color="indigo" /></q-item-section><q-item-section>Tableau Dispatch</q-item-section></q-item>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-item tag="label" clickable><q-item-section avatar><q-icon name="sms" /></q-item-section><q-item-section>Notifier par SMS</q-item-section><q-item-section side><q-toggle v-model="notifySms" dense /></q-item-section></q-item>
|
<q-item tag="label" clickable><q-item-section avatar><q-icon name="sms" /></q-item-section><q-item-section>Notifier par SMS</q-item-section><q-item-section side><q-toggle v-model="notifySms" dense /></q-item-section></q-item>
|
||||||
|
<q-item clickable v-close-popup @click="openCreateJob"><q-item-section avatar><q-icon name="add_task" color="teal-7" /></q-item-section><q-item-section>Créer un job</q-item-section></q-item>
|
||||||
|
<q-item clickable v-close-popup @click="showSuggestSlots = true"><q-item-section avatar><q-icon name="event_available" color="teal-8" /></q-item-section><q-item-section>Trouver un créneau</q-item-section></q-item>
|
||||||
<q-item clickable v-close-popup @click="openLegacyPush"><q-item-section avatar><q-icon name="sync_alt" color="deep-orange" /></q-item-section><q-item-section>Publier au legacy</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="openLegacyPush"><q-item-section avatar><q-icon name="sync_alt" color="deep-orange" /></q-item-section><q-item-section>Publier au legacy</q-item-section></q-item>
|
||||||
<q-item clickable v-close-popup @click="() => guard(loadWeek)"><q-item-section avatar><q-icon name="refresh" /></q-item-section><q-item-section>Rafraîchir</q-item-section></q-item>
|
<q-item clickable v-close-popup @click="() => guard(loadWeek)"><q-item-section avatar><q-icon name="refresh" /></q-item-section><q-item-section>Rafraîchir</q-item-section></q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
@ -52,6 +54,9 @@
|
||||||
<q-badge v-if="assignPanel.jobs.length" color="red" floating>{{ assignPanel.jobs.length }}</q-badge>
|
<q-badge v-if="assignPanel.jobs.length" color="red" floating>{{ assignPanel.jobs.length }}</q-badge>
|
||||||
<q-tooltip>Afficher / masquer les jobs à répartir (liste · carte · lasso · suggestion auto)</q-tooltip>
|
<q-tooltip>Afficher / masquer les jobs à répartir (liste · carte · lasso · suggestion auto)</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<!-- Création de job NATIVE OPS (#17) — Dispatch déprécié → tout ici. « Créer » = pool · « Créneau » = choisir tech+date+heure d'abord -->
|
||||||
|
<q-btn dense unelevated no-caps color="teal-7" icon="add_task" label="Créer un job" @click="openCreateJob"><q-tooltip>Créer un job / une installation (apparaît dans « À assigner »)</q-tooltip></q-btn>
|
||||||
|
<q-btn dense outline no-caps color="teal-8" icon="event_available" label="Créneau" @click="showSuggestSlots = true"><q-tooltip>Trouver le meilleur créneau (tech + date + heure) puis créer le job réservé</q-tooltip></q-btn>
|
||||||
<!-- Menu OUTILS : regroupe la config secondaire pour désencombrer -->
|
<!-- Menu OUTILS : regroupe la config secondaire pour désencombrer -->
|
||||||
<q-btn-dropdown dense outline color="grey-8" icon="build" label="Outils" no-caps>
|
<q-btn-dropdown dense outline color="grey-8" icon="build" label="Outils" no-caps>
|
||||||
<q-list dense style="min-width:230px">
|
<q-list dense style="min-width:230px">
|
||||||
|
|
@ -1723,6 +1728,12 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- #17 — Création de job NATIVE OPS (work-order) : réutilise le modal partagé. @created → rafraîchit le pool. -->
|
||||||
|
<UnifiedCreateModal v-model="createJobOpen" mode="work-order" :context="createJobCtx"
|
||||||
|
:technicians="techs" :external-tags="tagCatalog" :external-get-color="getTagColor" @created="onJobCreated" />
|
||||||
|
<!-- « Trouver un créneau » → choisit tech+date+heure puis pré-remplit la création -->
|
||||||
|
<SuggestSlotsDialog v-model="showSuggestSlots" @select="onSlotSelected" />
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -1764,6 +1775,8 @@ import TechSelect from 'src/components/shared/TechSelect.vue'
|
||||||
import SkillSelect from 'src/components/shared/SkillSelect.vue'
|
import SkillSelect from 'src/components/shared/SkillSelect.vue'
|
||||||
import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags partagé (Dispatch) : condensé, création à la volée, couleurs
|
import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags partagé (Dispatch) : condensé, création à la volée, couleurs
|
||||||
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 UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' // création de job NATIVE OPS (work-order) — Dispatch déprécié, tout ici
|
||||||
|
import SuggestSlotsDialog from 'src/modules/dispatch/components/SuggestSlotsDialog.vue' // « Trouver un créneau » → pré-remplit la création (tech+date+heure+adresse)
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -2011,6 +2024,17 @@ async function fetchKbMatrix (techId) {
|
||||||
let _kbMatT = null // le watch déclencheur est enregistré PLUS BAS (après la déclaration de occByTechDay) pour éviter la zone morte temporelle
|
let _kbMatT = null // le watch déclencheur est enregistré PLUS BAS (après la déclaration de occByTechDay) pour éviter la zone morte temporelle
|
||||||
const kbColor = (j) => j.skill ? getTagColor(j.skill) : (j.required_skill ? getTagColor(j.required_skill) : (legacyDeptColor(j.legacy_dept) || '#90a4ae'))
|
const kbColor = (j) => j.skill ? getTagColor(j.skill) : (j.required_skill ? getTagColor(j.required_skill) : (legacyDeptColor(j.legacy_dept) || '#90a4ae'))
|
||||||
async function reloadPool () { try { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } catch (e) { /* non bloquant */ } }
|
async function reloadPool () { try { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } catch (e) { /* non bloquant */ } }
|
||||||
|
// ── Création de job NATIVE OPS (#17) — Dispatch déprécié → tout depuis Planification. Réutilise UnifiedCreateModal (work-order). ──
|
||||||
|
// Job OPS-natif = PAS de legacy_ticket_id → coexiste avec les jobs synchronisés de F (la sync F ne touche que les jobs à legacy_ticket_id). « Sync F transitoire » = coexistence, pas de push F pour l'instant.
|
||||||
|
const createJobOpen = ref(false)
|
||||||
|
const createJobCtx = ref({})
|
||||||
|
const showSuggestSlots = ref(false)
|
||||||
|
function openCreateJob () { createJobCtx.value = { scheduled_date: '' }; createJobOpen.value = true } // pool (non planifié) par défaut
|
||||||
|
function onSlotSelected (slot) { // un créneau choisi → pré-remplit tech+date+heure+adresse+coords
|
||||||
|
createJobCtx.value = { assigned_tech: slot.tech_id, scheduled_date: slot.date, start_time: slot.start_time, address: slot._address || '', duration_h: slot._duration || 1, _latitude: slot._latitude, _longitude: slot._longitude }
|
||||||
|
createJobOpen.value = true
|
||||||
|
}
|
||||||
|
async function onJobCreated () { createJobOpen.value = false; await reloadPool(); await reloadOccupancy(); $q.notify({ type: 'positive', icon: 'add_task', message: 'Job créé — visible dans « À assigner »', timeout: 2600 }) }
|
||||||
// Drop sur la colonne « À assigner » = renvoyer au pool (désassigner, ERPNext only).
|
// Drop sur la colonne « À assigner » = renvoyer au pool (désassigner, ERPNext only).
|
||||||
async function onKanbanUnassign (ev) {
|
async function onKanbanUnassign (ev) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user