gigafibre-fsm/apps/ops/src/components/flow-editor/VariablePicker.vue
louispaulb 41d9b5f316 feat: flow editor, Gemini QR scanner with offline queue, dispatch planning v2
Major additions accumulated over 9 days — single commit per request.

Flow editor (new):
- Generic visual editor for step trees, usable by project wizard + agent flows
- PROJECT_KINDS / AGENT_KINDS catalogs decouple UI from domain
- Drag-and-drop reorder via vuedraggable with scope isolation per peer group
- Chain-aware depends_on rewrite on reorder (sequential only — DAGs preserved)
- Variable picker with per-applies_to catalog (Customer / Quotation /
  Service Contract / Issue / Subscription), insert + copy-clipboard modes
- trigger_condition helper with domain-specific JSONLogic examples
- Global FlowEditorDialog mounted once in MainLayout, Odoo inline pattern
- Server: targo-hub flow-runtime.js, flow-api.js, flow-templates.js
- ERPNext: Flow Template/Run doctypes, scheduler, 5 seeded system templates
- depends_on chips resolve to step labels instead of opaque "s4" ids

QR/OCR scanner (field app):
- Camera capture → Gemini Vision via targo-hub with 8s timeout
- IndexedDB offline queue retries photos when signal returns
- Watcher merges late-arriving scan results into the live UI

Dispatch:
- Planning mode (draft → publish) with offer pool for unassigned jobs
- Shared presets, recurrence selector, suggested-slots dialog
- PublishScheduleModal, unassign confirmation

Ops app:
- ClientDetailPage composables extraction (useClientData, useDeviceStatus,
  useWifiDiagnostic, useModemDiagnostic)
- Project wizard: shared detail sections, wizard catalog/publish composables
- Address pricing composable + pricing-mock data
- Settings redesign hosting flow templates

Targo-hub:
- Contract acceptance (JWT residential + DocuSeal commercial tracks)
- Referral system
- Modem-bridge diagnostic normalizer
- Device extractors consolidated

Migration scripts:
- Invoice/quote print format setup, Jinja rendering
- Additional import + fix scripts (reversals, dates, customers, payments)

Docs:
- Consolidated: old scattered MDs → HANDOFF, ARCHITECTURE, DATA_AND_FLOWS,
  FLOW_EDITOR_ARCHITECTURE, BILLING_AND_PAYMENTS, CPE_MANAGEMENT,
  APP_DESIGN_GUIDELINES
- Archived legacy wizard PHP for reference
- STATUS snapshots for 2026-04-18/19

Cleanup:
- Removed ~40 generated PDFs/HTMLs (invoice_preview*, rendered_jinja*)
- .gitignore now covers invoice preview output + nested .DS_Store

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 10:44:17 -04:00

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
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="indigo-6" 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="indigo-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>