gigafibre-fsm/apps/ops/src/components/flow-editor/kind-catalogs.js
louispaulb 5ab14cac44 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
Accumulated work on the dispatch/legacy-writeback branch:
- Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard,
  PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs;
  hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js.
- Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js,
  sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental
  migration scripts; LegacySyncPage, SupplierInvoices + negative-billing /
  terminated-active reports.
- Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit,
  olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers,
  legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets,
  ClientDetail updates.
- docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:12:12 -04:00

260 lines
9.8 KiB
JavaScript

/**
* kind-catalogs.js — Pluggable definitions for flow step kinds.
*
* A "catalog" tells the FlowEditor which step kinds are allowed and how
* to render their editor form. One editor, many domains (project, agent,
* customer onboarding, etc.) — same UI, different catalogs.
*
* Field descriptor shape:
* {
* name: 'subject', // flattens into step.payload[name]
* type: 'text'|'textarea'|'number'|'select'|'datetime'|'webhook',
* label: 'Sujet',
* required: true|false,
* options: [...], // for select
* placeholder: '...',
* default: 'value',
* help: 'tooltip text',
* }
*/
// -----------------------------------------------------------------------------
// Trigger types (when a step executes) — shared across all catalogs
// -----------------------------------------------------------------------------
export const TRIGGER_TYPES = {
on_flow_start: {
label: 'Au démarrage du flow',
help: 'Cette étape s\'exécute dès que le flow démarre',
fields: [],
},
on_prev_complete: {
label: 'Après les dépendances (depends_on)',
help: 'Cette étape attend que les étapes listées dans depends_on soient complétées',
fields: [],
},
after_delay: {
label: 'Après un délai',
help: 'Cette étape attend X heures/jours après que ses dépendances soient complétées',
fields: [
{ name: 'delay_hours', type: 'number', label: 'Heures', placeholder: '24' },
{ name: 'delay_days', type: 'number', label: 'Jours', placeholder: '7' },
],
},
on_date: {
label: 'À une date précise',
help: 'Déclenchement à une date/heure fixe',
fields: [
{ name: 'at', type: 'datetime', label: 'Date/heure' },
],
},
on_webhook: {
label: 'Webhook externe reçu',
help: 'POST sur /flow/trigger/:run_id/:step_id depuis n8n/autre',
fields: [],
},
manual: {
label: 'Déclenchement manuel (bouton)',
help: 'L\'utilisateur clique un bouton pour déclencher',
fields: [],
},
}
// -----------------------------------------------------------------------------
// Project kinds — for the project wizard + service orchestration flows
// -----------------------------------------------------------------------------
const JOB_TYPES = ['Installation', 'Réparation', 'Maintenance', 'Retrait', 'Dépannage', 'Autre']
const PRIORITIES = ['low', 'medium', 'high']
const GROUPS = ['Admin', 'Tech Targo', 'Support', 'NOC', 'Facturation']
export const PROJECT_KINDS = {
dispatch_job: {
label: 'Tâche dispatch',
icon: 'build',
color: '#6366f1',
fields: [
{ name: 'subject', type: 'text', label: 'Sujet', required: true },
{ name: 'job_type', type: 'select', label: 'Type', options: JOB_TYPES, default: 'Autre' },
{ name: 'priority', type: 'select', label: 'Priorité', options: PRIORITIES, default: 'medium' },
{ name: 'duration_h', type: 'number', label: 'Durée (h)', default: 1 },
{ name: 'assigned_group', type: 'select', label: 'Groupe', options: GROUPS, default: 'Tech Targo' },
{ name: 'on_open_webhook', type: 'text', label: 'Webhook à l\'ouverture (n8n)', placeholder: 'https://n8n.gigafibre.ca/webhook/...' },
{ name: 'on_close_webhook',type: 'text', label: 'Webhook à la fermeture (n8n)' },
{ name: 'merge_key', type: 'text', label: 'Merge key (optionnel)', help: 'Étapes avec le même merge_key fusionnent en une seule visite' },
],
},
issue: {
label: 'Ticket',
icon: 'confirmation_number',
color: '#f59e0b',
help: 'Crée un ticket (ERPNext Issue) quand le flow atteint cette étape — c\'est l\'action « créer un ticket ».',
fields: [
{ name: 'subject', type: 'text', label: 'Sujet', required: true },
{ name: 'description', type: 'textarea', label: 'Description' },
{ name: 'priority', type: 'select', label: 'Priorité', options: ['Low', 'Medium', 'High', 'Urgent'], default: 'Medium' },
{ name: 'issue_type', type: 'text', label: 'Type (texte libre)', placeholder: 'Suivi' },
],
},
notify: {
label: 'Notification (SMS/email)',
icon: 'send',
color: '#3b82f6',
fields: [
{ name: 'channel', type: 'select', label: 'Canal', options: ['sms', 'email'], default: 'sms' },
{ name: 'to', type: 'text', label: 'Destinataire (template)', placeholder: '{{customer.primary_phone}}', help: 'Supporte les templates {{customer.field}}' },
{ name: 'template_id', type: 'text', label: 'Template ID (depuis email-templates.js)', placeholder: 'welcome_residential' },
{ name: 'subject', type: 'text', label: 'Sujet (email uniquement)' },
{ name: 'body', type: 'textarea', label: 'Corps (si pas de template_id)' },
],
},
webhook: {
label: 'Webhook externe',
icon: 'webhook',
color: '#8b5cf6',
fields: [
{ name: 'url', type: 'text', label: 'URL', required: true, placeholder: 'https://n8n.gigafibre.ca/webhook/xxx' },
{ name: 'method', type: 'select', label: 'Méthode', options: ['POST', 'GET', 'PUT', 'DELETE'], default: 'POST' },
{ name: 'body_template', type: 'textarea', label: 'Body (JSON template)', placeholder: '{"customer": "{{customer.name}}", "contract": "{{contract.name}}"}' },
],
},
erp_update: {
label: 'Mise à jour ERPNext',
icon: 'edit_note',
color: '#10b981',
fields: [
{ name: 'doctype', type: 'text', label: 'DocType', required: true, placeholder: 'Customer' },
{ name: 'docname_ref', type: 'text', label: 'Nom du doc (template)', placeholder: '{{customer.name}}' },
{ name: 'fields_json', type: 'textarea', label: 'Champs à mettre à jour (JSON)', placeholder: '{"customer_group": "Active"}' },
],
},
wait: {
label: 'Attendre',
icon: 'hourglass_empty',
color: '#94a3b8',
help: 'Utilise le trigger « Après un délai » pour contrôler la durée',
fields: [],
},
condition: {
label: 'Condition (si / sinon)',
icon: 'fork_right',
color: '#eab308',
hasBranches: true,
branchLabels: { yes: 'Oui', no: 'Non' },
fields: [
{ name: 'field', type: 'text', label: 'Champ (chemin JSON)', placeholder: 'customer.primary_phone', required: true },
{ name: 'op', type: 'select', label: 'Opérateur',
options: ['==', '!=', '<', '>', '<=', '>=', 'in', 'not_in', 'empty', 'not_empty'], default: '==' },
{ name: 'value', type: 'text', label: 'Valeur', placeholder: 'Actif' },
],
},
subscription_activate: {
label: 'Activer l\'abonnement',
icon: 'autorenew',
color: '#ec4899',
fields: [
{ name: 'subscription_ref', type: 'text', label: 'Référence abonnement (template)', placeholder: '{{contract.subscription}}' },
],
},
}
// -----------------------------------------------------------------------------
// Agent kinds — for the conversational agent flows (AgentFlowsPage)
// -----------------------------------------------------------------------------
// Preserved from the current AgentFlowsPage stepTypeLabels structure so the
// extracted FlowEditor can replace it as-is in a later refactor.
export const AGENT_KINDS = {
tool: {
label: 'Appel outil',
icon: 'settings',
color: '#6366f1',
fields: [
{ name: 'tool', type: 'text', label: 'Outil', placeholder: 'get_equipment' },
{ name: 'note', type: 'text', label: 'Note' },
],
},
condition: {
label: 'Condition',
icon: 'help',
color: '#eab308',
hasBranches: true,
branchLabels: { yes: 'Oui', no: 'Non' },
fields: [
{ name: 'field', type: 'text', label: 'Champ', placeholder: 'device.online' },
{ name: 'op', type: 'select', label: 'Opérateur', options: ['==', '!=', '<', '>', '<=', '>='] },
{ name: 'value', type: 'text', label: 'Valeur' },
],
},
switch: {
label: 'Switch',
icon: 'call_split',
color: '#f59e0b',
hasBranches: 'dynamic',
fields: [
{ name: 'field', type: 'text', label: 'Champ switch', placeholder: 'onu.alarm_type' },
],
},
respond: {
label: 'Réponse',
icon: 'chat',
color: '#10b981',
fields: [
{ name: 'message', type: 'textarea', label: 'Message', rows: 4 },
{ name: 'note', type: 'text', label: 'Note interne' },
],
},
action: {
label: 'Action',
icon: 'bolt',
color: '#8b5cf6',
fields: [
{ name: 'action', type: 'text', label: 'Action', placeholder: 'create_dispatch_job' },
{ name: 'params', type: 'json', label: 'Paramètres (JSON)' },
{ name: 'message', type: 'textarea', label: 'Message au client', rows: 2 },
],
},
goto: {
label: 'Aller à',
icon: 'arrow_forward',
color: '#64748b',
fields: [
{ name: 'target', type: 'text', label: 'Cible (intent ID)' },
],
},
}
// -----------------------------------------------------------------------------
// Utilities
// -----------------------------------------------------------------------------
export function getKind (catalog, kindName) {
return catalog[kindName] || { label: kindName, icon: 'circle', color: '#94a3b8', fields: [] }
}
export function getTrigger (typeName) {
return TRIGGER_TYPES[typeName] || { label: typeName || 'Inconnu', fields: [] }
}
/**
* Build an empty step skeleton for a given kind.
* Applies defaults from field descriptors.
*/
export function buildEmptyStep (kindName, catalog) {
const kind = getKind(catalog, kindName)
const payload = {}
for (const f of kind.fields || []) {
if (f.default !== undefined) payload[f.name] = f.default
}
return {
id: 'step_' + Math.random().toString(36).slice(2, 9),
kind: kindName,
label: kind.label,
parent_id: null,
branch: null,
depends_on: [],
trigger: { type: 'on_prev_complete' },
payload,
}
}