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>
115 lines
4.0 KiB
Vue
115 lines
4.0 KiB
Vue
<!--
|
|
VariablePicker.vue — Menu de variables `{{path}}` cliquables.
|
|
|
|
Un dropdown qui liste toutes les variables disponibles pour un `applies_to`
|
|
donné (Customer / Quotation / Service Contract / Issue / Subscription).
|
|
|
|
Deux modes d'utilisation :
|
|
- Mode "insert" (défaut) : émet @insert('{{path}}') pour qu'un parent
|
|
l'ajoute à la fin du champ qu'il contrôle (ex : trigger_condition).
|
|
- Mode "copy" : copie `{{path}}` dans le presse-papier et
|
|
notifie l'utilisateur — utile quand il n'y a pas de champ cible
|
|
capturé dans le même composant (ex : dans le StepEditorModal où
|
|
n'importe quel champ de payload peut en avoir besoin).
|
|
|
|
Props :
|
|
- appliesTo : 'Customer' | 'Quotation' | ...
|
|
- label : texte du bouton (défaut "+ Variable")
|
|
- mode : 'insert' | 'copy' (défaut 'insert')
|
|
|
|
Events :
|
|
- insert(text) : émet `{{path}}` à insérer (mode 'insert' uniquement)
|
|
|
|
Performance :
|
|
- La liste est computed une fois par applies_to (mémoisée par Vue).
|
|
- Les entrées sont triées par domaine puis alphabétique pour rester stable.
|
|
-->
|
|
<template>
|
|
<q-btn-dropdown flat dense no-caps size="sm" :icon="icon" :label="label"
|
|
class="variable-picker" color="primary" content-class="vp-menu">
|
|
<q-list dense class="vp-list">
|
|
<q-item-label header class="vp-header">
|
|
Variables disponibles
|
|
<span v-if="appliesTo" class="text-caption text-grey-6">
|
|
· {{ appliesTo }}
|
|
</span>
|
|
</q-item-label>
|
|
|
|
<q-item v-if="!appliesTo" class="vp-empty">
|
|
<q-item-section>
|
|
<q-item-label class="text-caption text-grey-7">
|
|
Sélectionnez « Applique à » pour voir les variables du domaine.
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item v-for="v in variables" :key="v.path" clickable v-close-popup
|
|
@click="onPick(v)" class="vp-item">
|
|
<q-item-section>
|
|
<q-item-label class="text-caption text-weight-medium">{{ v.label }}</q-item-label>
|
|
<q-item-label caption>
|
|
<code class="vp-code">{{ formatted(v.path) }}</code>
|
|
</q-item-label>
|
|
<q-item-label v-if="v.hint" caption class="text-grey-6 vp-hint">
|
|
{{ v.hint }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-icon :name="mode === 'copy' ? 'content_copy' : 'add_circle_outline'" size="16px" color="green-5" />
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-btn-dropdown>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import { Notify, copyToClipboard } from 'quasar'
|
|
import { getVariables } from './variables'
|
|
|
|
const props = defineProps({
|
|
appliesTo: { type: String, default: null },
|
|
label: { type: String, default: '+ Variable' },
|
|
mode: { type: String, default: 'insert' }, // 'insert' | 'copy'
|
|
icon: { type: String, default: 'data_object' },
|
|
})
|
|
|
|
const emit = defineEmits(['insert'])
|
|
|
|
const variables = computed(() => getVariables(props.appliesTo))
|
|
|
|
function formatted (path) { return `{{${path}}}` }
|
|
|
|
async function onPick (v) {
|
|
const text = formatted(v.path)
|
|
if (props.mode === 'copy') {
|
|
try {
|
|
await copyToClipboard(text)
|
|
Notify.create({ type: 'positive', message: `Copié : ${text}`, timeout: 1200, position: 'top' })
|
|
} catch {
|
|
Notify.create({ type: 'negative', message: 'Copie impossible', timeout: 1500 })
|
|
}
|
|
return
|
|
}
|
|
emit('insert', text)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.variable-picker :deep(.q-btn__content) { font-size: 0.72rem; }
|
|
.vp-list { min-width: 320px; max-width: 380px; max-height: 420px; overflow-y: auto; }
|
|
.vp-header { font-weight: 600; font-size: 0.72rem; color: #475569; }
|
|
.vp-item { padding: 6px 12px; border-bottom: 1px solid #f1f5f9; }
|
|
.vp-item:last-child { border-bottom: none; }
|
|
.vp-code {
|
|
background: #f1f5f9;
|
|
color: #1e293b;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
font-size: 0.72rem;
|
|
font-family: ui-monospace, Menlo, monospace;
|
|
}
|
|
.vp-hint { font-size: 0.7rem; margin-top: 2px; }
|
|
.vp-empty { padding: 10px 14px; }
|
|
</style>
|