/** * 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, } }