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>
206 lines
6.6 KiB
JavaScript
206 lines
6.6 KiB
JavaScript
/**
|
|
* useFlowEditor.js — Global reactive store for the Flow Editor dialog.
|
|
*
|
|
* Odoo-style "inline create from anywhere": any page can call openFlow()
|
|
* to pop the editor without having to route to Settings.
|
|
*
|
|
* The shared state (singleton module-level refs) is consumed by
|
|
* <FlowEditorDialog /> mounted in MainLayout. Pages simply call
|
|
* `const fe = useFlowEditor(); fe.openTemplate('FT-00001')`.
|
|
*
|
|
* API (all methods optimised to avoid redundant fetches):
|
|
* openTemplate(name, opts?) — load an existing FT by name and open editor
|
|
* openNew(opts?) — open editor with a blank draft
|
|
* close() — close + clear state
|
|
* isOpen Ref<boolean>
|
|
* template Ref<Object|null> — current FT doc being edited
|
|
* loading Ref<boolean>
|
|
* error Ref<string|null>
|
|
* dirty Ref<boolean> — unsaved changes flag
|
|
*
|
|
* Options accepted by openTemplate / openNew:
|
|
* { category, applies_to, kind } — preset fields for new templates
|
|
* { onSaved(tpl) } — callback after a successful save
|
|
* { context } — opaque host context (for telemetry)
|
|
*/
|
|
|
|
import { ref, reactive, readonly } from 'vue'
|
|
import {
|
|
getFlowTemplate,
|
|
createFlowTemplate,
|
|
updateFlowTemplate,
|
|
duplicateFlowTemplate,
|
|
deleteFlowTemplate,
|
|
} from 'src/api/flow-templates'
|
|
|
|
// ── Module-level shared state (singleton) ───────────────────────────────────
|
|
|
|
const isOpen = ref(false)
|
|
const template = ref(null) // the doc currently being edited
|
|
const templateName = ref(null) // non-null once saved once
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const error = ref(null)
|
|
const dirty = ref(false)
|
|
const onSavedCbs = ref([]) // callbacks from the caller
|
|
const mode = ref('edit') // 'new' | 'edit' | 'view'
|
|
|
|
/** Build a blank FT draft with sensible defaults. */
|
|
function blankTemplate (opts = {}) {
|
|
return {
|
|
template_name: '',
|
|
category: opts.category || 'other',
|
|
applies_to: opts.applies_to || null,
|
|
icon: opts.icon || 'account_tree',
|
|
is_active: 1,
|
|
is_system: 0,
|
|
version: 1,
|
|
description: '',
|
|
trigger_event: opts.trigger_event || null,
|
|
trigger_condition: '',
|
|
flow_definition: {
|
|
version: 1,
|
|
trigger: { type: opts.kind === 'agent' ? 'manual' : 'on_flow_start' },
|
|
variables: {},
|
|
steps: [],
|
|
},
|
|
tags: '',
|
|
notes: '',
|
|
}
|
|
}
|
|
|
|
// ── Actions ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Open the editor on an existing template.
|
|
* Fetches full body (incl. flow_definition) from the Hub.
|
|
*/
|
|
async function openTemplate (name, opts = {}) {
|
|
if (!name) return openNew(opts)
|
|
error.value = null
|
|
loading.value = true
|
|
isOpen.value = true
|
|
mode.value = 'edit'
|
|
templateName.value = name
|
|
onSavedCbs.value = opts.onSaved ? [opts.onSaved] : []
|
|
try {
|
|
const tpl = await getFlowTemplate(name)
|
|
// Ensure flow_definition is a live object (Hub returns parsed JSON)
|
|
if (!tpl.flow_definition || typeof tpl.flow_definition === 'string') {
|
|
try { tpl.flow_definition = JSON.parse(tpl.flow_definition || '{}') }
|
|
catch { tpl.flow_definition = { steps: [] } }
|
|
}
|
|
if (!tpl.flow_definition.steps) tpl.flow_definition.steps = []
|
|
template.value = tpl
|
|
dirty.value = false
|
|
} catch (e) {
|
|
error.value = e.message
|
|
isOpen.value = false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/** Open the editor on a fresh draft. Save-on-Enregistrer creates it. */
|
|
function openNew (opts = {}) {
|
|
error.value = null
|
|
isOpen.value = true
|
|
mode.value = 'new'
|
|
templateName.value = null
|
|
template.value = blankTemplate(opts)
|
|
dirty.value = false
|
|
onSavedCbs.value = opts.onSaved ? [opts.onSaved] : []
|
|
}
|
|
|
|
/** Close the editor. Caller is responsible for confirming unsaved changes. */
|
|
function close (force = false) {
|
|
if (dirty.value && !force) {
|
|
if (!window.confirm('Modifications non sauvegardées. Fermer quand même ?')) return
|
|
}
|
|
isOpen.value = false
|
|
template.value = null
|
|
templateName.value = null
|
|
dirty.value = false
|
|
error.value = null
|
|
}
|
|
|
|
/** Mark the editor state as dirty (called by child on any mutation). */
|
|
function markDirty () { dirty.value = true }
|
|
|
|
/**
|
|
* Save the current draft. Creates if new, else patches.
|
|
* Returns the saved template on success; throws on error.
|
|
*/
|
|
async function save () {
|
|
if (!template.value) return
|
|
saving.value = true
|
|
error.value = null
|
|
try {
|
|
let saved
|
|
const body = { ...template.value }
|
|
// flow_definition will be stringified server-side
|
|
if (mode.value === 'new') {
|
|
saved = await createFlowTemplate(body)
|
|
templateName.value = saved.name
|
|
mode.value = 'edit'
|
|
} else {
|
|
saved = await updateFlowTemplate(templateName.value, body)
|
|
}
|
|
template.value = { ...template.value, ...saved, flow_definition: template.value.flow_definition }
|
|
dirty.value = false
|
|
for (const cb of onSavedCbs.value) { try { cb(saved) } catch { /* noop */ } }
|
|
return saved
|
|
} catch (e) {
|
|
error.value = e.message
|
|
throw e
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
/** Duplicate the current template (only valid in edit mode). */
|
|
async function duplicate (newName) {
|
|
if (mode.value !== 'edit' || !templateName.value) return
|
|
const dup = await duplicateFlowTemplate(templateName.value, newName)
|
|
await openTemplate(dup.name)
|
|
return dup
|
|
}
|
|
|
|
/** Delete the current template (only valid in edit mode + non-system). */
|
|
async function remove () {
|
|
if (mode.value !== 'edit' || !templateName.value) return
|
|
if (template.value?.is_system) {
|
|
throw new Error('Impossible de supprimer un template système (utilisez "Dupliquer")')
|
|
}
|
|
await deleteFlowTemplate(templateName.value)
|
|
close(true)
|
|
}
|
|
|
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
export function useFlowEditor () {
|
|
return {
|
|
// state (read-only refs so consumers can't accidentally mutate)
|
|
isOpen: readonly(isOpen),
|
|
loading: readonly(loading),
|
|
saving: readonly(saving),
|
|
error: readonly(error),
|
|
dirty: readonly(dirty),
|
|
mode: readonly(mode),
|
|
templateName: readonly(templateName),
|
|
// mutable state (only the draft content, by design)
|
|
template,
|
|
// actions
|
|
openTemplate,
|
|
openNew,
|
|
close,
|
|
save,
|
|
duplicate,
|
|
remove,
|
|
markDirty,
|
|
}
|
|
}
|
|
|
|
// Named default for convenience
|
|
export default useFlowEditor
|