ticket: contrôle Statut/Reporter partagé (v1 sur le panneau ticket)
Nouveau TicketStatusControl (doctype-conscient Issue/Dispatch Job) : Reporter +1j/+7j/date · En attente d'un autre ticket #X · Fermer/Rouvrir. Reporter = On Hold + note datée (job = replanifie scheduled_date). Posé sur IssueDetail (page ticket + fiche client) en remplacement des boutons Fermer/Rouvrir épars. Prochaine étape (séquencée) : câbler sur le panneau job (Planification) + convergence des 3 vues (#27). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8a655f9933
commit
fcfd63c2e9
67
apps/ops/src/components/shared/TicketStatusControl.vue
Normal file
67
apps/ops/src/components/shared/TicketStatusControl.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<!-- Contrôle STATUT / REPORTER partagé (page ticket · fiche client · Planification).
|
||||
Doctype-conscient : Issue (support) ou Dispatch Job (terrain). -->
|
||||
<q-btn-dropdown :label="label" dense no-caps unelevated :color="color" text-color="white" icon="flag" :loading="busy">
|
||||
<q-list dense style="min-width:250px">
|
||||
<q-item-label header class="q-py-xs">⏰ Reporter</q-item-label>
|
||||
<q-item clickable v-close-popup @click="postpone(1)"><q-item-section avatar><q-icon name="snooze" color="indigo" /></q-item-section><q-item-section>À demain (+1 jour)</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="postpone(7)"><q-item-section avatar><q-icon name="snooze" color="indigo" /></q-item-section><q-item-section>Dans 7 jours</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="postponeUntil"><q-item-section avatar><q-icon name="event" color="indigo" /></q-item-section><q-item-section>À une date précise…</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="waitTicket"><q-item-section avatar><q-icon name="hourglass_top" color="brown" /></q-item-section><q-item-section>En attente d'un autre ticket…</q-item-section></q-item>
|
||||
<q-separator class="q-my-xs" />
|
||||
<q-item v-if="!isClosed" clickable v-close-popup @click="close"><q-item-section avatar><q-icon name="check_circle" color="green-7" /></q-item-section><q-item-section>Fermer</q-item-section></q-item>
|
||||
<q-item v-else clickable v-close-popup @click="reopen"><q-item-section avatar><q-icon name="refresh" color="warning" /></q-item-section><q-item-section>Rouvrir</q-item-section></q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { Dialog, Notify } from 'quasar'
|
||||
import { updateDoc, createDoc } from 'src/api/erp'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: { type: String, required: true }, // 'Issue' | 'Dispatch Job'
|
||||
docname: { type: String, required: true },
|
||||
status: { type: String, default: '' },
|
||||
scheduledDate: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['changed'])
|
||||
|
||||
const busy = ref(false)
|
||||
const isJob = computed(() => props.doctype === 'Dispatch Job')
|
||||
const isClosed = computed(() => ['Closed', 'Resolved', 'Completed', 'Cancelled'].includes(props.status))
|
||||
const isHold = computed(() => props.status === 'On Hold')
|
||||
const label = computed(() => isClosed.value ? 'Fermé' : (isHold.value ? 'En attente' : 'Statut'))
|
||||
const color = computed(() => isClosed.value ? 'grey-6' : (isHold.value ? 'brown' : 'primary'))
|
||||
|
||||
function todayPlus (days) { const d = new Date(); d.setDate(d.getDate() + Number(days || 0)); return d.toLocaleDateString('en-CA', { timeZone: 'America/Toronto' }) }
|
||||
|
||||
async function apply (patch, note) {
|
||||
busy.value = true
|
||||
try {
|
||||
await updateDoc(props.doctype, props.docname, patch)
|
||||
if (note) { try { await createDoc('Comment', { comment_type: 'Comment', reference_doctype: props.doctype, reference_name: props.docname, content: note }) } catch { /* note best-effort */ } }
|
||||
emit('changed', { ...patch, note })
|
||||
Notify.create({ type: 'positive', message: note || 'Statut mis à jour', timeout: 2200 })
|
||||
} catch (e) { Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 }) }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
|
||||
// Reporter : job → replanifie à la date de réveil (+ On Hold) ; ticket → On Hold + note datée.
|
||||
async function postpone (days) {
|
||||
const wake = todayPlus(days)
|
||||
if (isJob.value) apply({ scheduled_date: wake, status: 'On Hold' }, `⏰ Reporté au ${wake}`)
|
||||
else apply({ status: 'On Hold' }, `⏰ Reporté au ${wake}`)
|
||||
}
|
||||
function postponeUntil () {
|
||||
Dialog.create({ title: 'Reporter à…', prompt: { model: todayPlus(1), type: 'date' }, cancel: true, ok: { label: 'Reporter', color: 'primary' } })
|
||||
.onOk(v => { if (v) { if (isJob.value) apply({ scheduled_date: v, status: 'On Hold' }, `⏰ Reporté au ${v}`); else apply({ status: 'On Hold' }, `⏰ Reporté au ${v}`) } })
|
||||
}
|
||||
function waitTicket () {
|
||||
Dialog.create({ title: 'En attente d\'un autre ticket', message: 'N° du ticket bloquant (ex. 253907)', prompt: { model: '', type: 'text' }, cancel: true, ok: { label: 'Mettre en attente', color: 'brown' } })
|
||||
.onOk(v => { const num = String(v || '').trim(); if (num) apply({ status: 'On Hold' }, `⏳ En attente du ticket #${num}`) })
|
||||
}
|
||||
function close () { apply({ status: isJob.value ? 'Completed' : 'Closed' }, null) }
|
||||
function reopen () { apply({ status: isJob.value ? 'open' : 'Open' }, null) }
|
||||
</script>
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
<template>
|
||||
<!-- Quick actions -->
|
||||
<div class="issue-actions q-mb-sm">
|
||||
<q-btn v-if="doc.status !== 'Closed' && doc.status !== 'Resolved'" dense unelevated no-caps
|
||||
icon="check_circle" label="Fermer le ticket" color="green-7" size="sm"
|
||||
@click="closeTicket" :loading="closingTicket" />
|
||||
<q-btn v-else dense unelevated no-caps
|
||||
icon="refresh" label="Réouvrir" color="warning" size="sm"
|
||||
@click="reopenTicket" :loading="closingTicket" />
|
||||
<TicketStatusControl doctype="Issue" :docname="docName" :status="doc.status"
|
||||
@changed="p => { if (p && p.status) doc.status = p.status; $emit('reply-sent', docName) }" />
|
||||
<FlowQuickButton flat dense size="sm" icon="account_tree" label="Flow"
|
||||
tooltip="Lancer / modifier un flow pour ce ticket"
|
||||
category="incident" applies-to="Issue" trigger-event="on_issue_opened" />
|
||||
|
|
@ -319,6 +315,7 @@ import { fetchTags, updateTag, renameTag, deleteTag as deleteTagApi } from 'src/
|
|||
import InlineField from 'src/components/shared/InlineField.vue'
|
||||
import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue'
|
||||
import FlowQuickButton from 'src/components/flow-editor/FlowQuickButton.vue'
|
||||
import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue'
|
||||
import ProjectWizard from 'src/components/shared/ProjectWizard.vue'
|
||||
import TaskNode from 'src/components/shared/TaskNode.vue'
|
||||
|
||||
|
|
@ -700,31 +697,7 @@ function onJobUpdated (jobName, data) {
|
|||
emit('dispatch-updated', jobName, data)
|
||||
}
|
||||
|
||||
// ── Close / Reopen ──
|
||||
|
||||
const closingTicket = ref(false)
|
||||
|
||||
async function closeTicket () {
|
||||
closingTicket.value = true
|
||||
try {
|
||||
await updateDoc('Issue', props.docName, { status: 'Closed' })
|
||||
props.doc.status = 'Closed'
|
||||
Notify.create({ type: 'positive', message: 'Ticket fermé' })
|
||||
} catch (e) {
|
||||
Notify.create({ type: 'negative', message: `Erreur: ${e.message}` })
|
||||
} finally { closingTicket.value = false }
|
||||
}
|
||||
|
||||
async function reopenTicket () {
|
||||
closingTicket.value = true
|
||||
try {
|
||||
await updateDoc('Issue', props.docName, { status: 'Open' })
|
||||
props.doc.status = 'Open'
|
||||
Notify.create({ type: 'positive', message: 'Ticket réouvert' })
|
||||
} catch (e) {
|
||||
Notify.create({ type: 'negative', message: `Erreur: ${e.message}` })
|
||||
} finally { closingTicket.value = false }
|
||||
}
|
||||
// Fermer/Rouvrir/Reporter : gérés par le composant partagé TicketStatusControl.
|
||||
|
||||
// ── Reply ──
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user