Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré / juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts, sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle week-end + placeholder « en attente du quart », clustering + lasso + filtre-date sur la carte, accès rapide « À assigner » (badge). Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive + timeout fetch global + authFetch durci → fin des rechargements manuels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
6.7 KiB
Vue
124 lines
6.7 KiB
Vue
<template>
|
|
<!-- RÉUTILISABLE : ajoute le fil/objet courant (conversation, ticket, projet…) à une TÂCHE ERPNext —
|
|
existante ou nouvelle. Passé un `source` = { type, name, title, customer, url }. -->
|
|
<q-dialog v-model="model" @hide="reset">
|
|
<q-card style="min-width:440px;max-width:92vw">
|
|
<q-card-section class="row items-center q-pb-none">
|
|
<q-icon name="add_task" color="primary" size="22px" class="q-mr-sm" />
|
|
<span class="text-subtitle1 text-weight-bold">Ajouter à une tâche</span>
|
|
<q-space /><q-btn flat round dense icon="close" v-close-popup />
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<div class="text-caption text-grey-7 q-mb-sm">
|
|
Lier <b>{{ source.title || source.name || 'cet élément' }}</b>
|
|
<span v-if="source.type" class="text-grey-5">({{ srcLabel }})</span> à une tâche.
|
|
</div>
|
|
<q-btn-toggle v-model="tab" spread no-caps dense unelevated toggle-color="primary" color="grey-2" text-color="grey-8" class="q-mb-md"
|
|
:options="[{ label: 'Tâche existante', value: 'existing' }, { label: 'Nouvelle tâche', value: 'new' }]" />
|
|
|
|
<!-- Tâche existante -->
|
|
<template v-if="tab === 'existing'">
|
|
<q-select dense outlined v-model="picked" use-input input-debounce="300" :options="taskOptions" @filter="searchTasks"
|
|
option-label="label" option-value="value" emit-value map-options clearable :loading="searching"
|
|
label="Chercher une tâche (sujet ou #)">
|
|
<template #no-option><q-item><q-item-section class="text-grey-6 text-caption">Tape pour chercher — ou crée une nouvelle tâche.</q-item-section></q-item></template>
|
|
</q-select>
|
|
</template>
|
|
|
|
<!-- Nouvelle tâche -->
|
|
<template v-else>
|
|
<q-input dense outlined v-model="newTask.subject" label="Sujet de la tâche" autofocus />
|
|
<div class="row q-col-gutter-sm q-mt-xs">
|
|
<q-select class="col-6 col-sm" dense outlined v-model="newTask.priority" :options="['Low', 'Medium', 'High', 'Urgent']" label="Priorité" emit-value map-options />
|
|
<q-input class="col-6 col-sm" dense outlined v-model="newTask.exp_end_date" type="date" label="Échéance" />
|
|
</div>
|
|
<q-input dense outlined v-model="newTask.description" label="Détails (optionnel)" type="textarea" autogrow class="q-mt-xs" />
|
|
</template>
|
|
</q-card-section>
|
|
<q-card-actions align="right">
|
|
<q-btn flat no-caps label="Annuler" v-close-popup />
|
|
<q-btn unelevated no-caps color="primary" :icon="tab === 'new' ? 'add_task' : 'link'"
|
|
:label="tab === 'new' ? 'Créer + lier' : 'Lier à la tâche'" :loading="busy"
|
|
:disable="tab === 'existing' ? !picked : !newTask.subject.trim()" @click="confirm" />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, reactive, watch } from 'vue'
|
|
import { useQuasar } from 'quasar'
|
|
import { listDocs, getDoc, createDoc, updateDoc } from 'src/api/erp'
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: Boolean, default: false },
|
|
// Objet/fil courant à lier : { type:'Issue'|'Conversation'|'Customer'|'Project'…, name, title, customer, url }
|
|
source: { type: Object, default: () => ({}) },
|
|
})
|
|
const emit = defineEmits(['update:modelValue', 'linked'])
|
|
const $q = useQuasar()
|
|
const model = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
|
|
|
|
const SRC_LABEL = { Issue: 'ticket', Conversation: 'conversation', Customer: 'client', Project: 'projet', Task: 'tâche', 'Dispatch Job': 'travail' }
|
|
const srcLabel = computed(() => SRC_LABEL[props.source.type] || props.source.type)
|
|
|
|
const tab = ref('existing')
|
|
const busy = ref(false)
|
|
const searching = ref(false)
|
|
const taskOptions = ref([])
|
|
const picked = ref(null)
|
|
const newTask = reactive({ subject: '', priority: 'Medium', exp_end_date: '', description: '' })
|
|
|
|
// Pré-remplit le sujet de la nouvelle tâche depuis le fil courant
|
|
watch(model, (open) => { if (open) { newTask.subject = props.source.title || ''; loadRecentTasks() } })
|
|
|
|
// Ligne de référence ajoutée à la tâche (lien retour vers le fil) — réutilisable multi-objets : chaque ajout empile une ligne.
|
|
function refLine () {
|
|
const s = props.source
|
|
const bits = ['↪ ' + (s.title || s.name || 'élément')]
|
|
if (s.type && s.name) bits.push('[' + (srcLabel.value || s.type) + ' ' + s.name + ']')
|
|
if (s.customer) bits.push('· client ' + s.customer)
|
|
if (s.url) bits.push('· ' + s.url)
|
|
return bits.join(' ')
|
|
}
|
|
|
|
async function loadRecentTasks () {
|
|
searching.value = true
|
|
try {
|
|
const rows = await listDocs('Task', { filters: { status: ['not in', ['Completed', 'Cancelled']] }, fields: ['name', 'subject', 'status'], limit: 15, orderBy: 'modified desc' })
|
|
taskOptions.value = rows.map(t => ({ label: (t.subject || t.name) + ' · ' + t.status, value: t.name }))
|
|
} catch { taskOptions.value = [] } finally { searching.value = false }
|
|
}
|
|
function searchTasks (query, done) {
|
|
const q = (query || '').trim()
|
|
if (!q) { loadRecentTasks().then(() => done(() => {})); return }
|
|
searching.value = true
|
|
listDocs('Task', { or_filters: { subject: ['like', '%' + q + '%'], name: ['like', '%' + q + '%'] }, fields: ['name', 'subject', 'status'], limit: 15, orderBy: 'modified desc' })
|
|
.then(rows => { done(() => { taskOptions.value = rows.map(t => ({ label: (t.subject || t.name) + ' · ' + t.status, value: t.name })) }) })
|
|
.catch(() => done(() => { taskOptions.value = [] }))
|
|
.finally(() => { searching.value = false })
|
|
}
|
|
|
|
async function confirm () {
|
|
busy.value = true
|
|
try {
|
|
if (tab.value === 'new') {
|
|
const data = { subject: newTask.subject.trim(), status: 'Open', priority: newTask.priority, description: [newTask.description, refLine()].filter(Boolean).join('\n\n') }
|
|
if (newTask.exp_end_date) data.exp_end_date = newTask.exp_end_date
|
|
const r = await createDoc('Task', data)
|
|
$q.notify({ type: 'positive', icon: 'add_task', message: 'Tâche créée et liée' })
|
|
emit('linked', { task: r?.name, created: true })
|
|
} else {
|
|
const cur = await getDoc('Task', picked.value)
|
|
const desc = [(cur && cur.description) || '', refLine()].filter(Boolean).join('\n\n')
|
|
await updateDoc('Task', picked.value, { description: desc })
|
|
$q.notify({ type: 'positive', icon: 'link', message: 'Lié à la tâche ' + picked.value })
|
|
emit('linked', { task: picked.value, created: false })
|
|
}
|
|
model.value = false
|
|
} catch (e) { $q.notify({ type: 'negative', message: 'Échec : ' + (e.message || e) }) } finally { busy.value = false }
|
|
}
|
|
|
|
function reset () { tab.value = 'existing'; picked.value = null; newTask.subject = ''; newTask.priority = 'Medium'; newTask.exp_end_date = ''; newTask.description = '' }
|
|
</script>
|