diff --git a/fusion_repairs/__manifest__.py b/fusion_repairs/__manifest__.py
index c772b91c..f8c2cc6b 100644
--- a/fusion_repairs/__manifest__.py
+++ b/fusion_repairs/__manifest__.py
@@ -4,7 +4,7 @@
{
'name': 'Fusion Repairs',
- 'version': '19.0.2.1.0',
+ 'version': '19.0.2.2.0',
'category': 'Inventory/Repairs',
'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal',
'description': """
@@ -56,6 +56,7 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'website',
'sale_management',
'stock',
+ 'purchase',
'repair',
'maintenance',
'fusion_tasks',
@@ -78,6 +79,8 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'data/emergency_charge_data.xml',
'data/callout_rate_data.xml',
'data/delivery_charge_data.xml',
+ 'data/symptom_class_data.xml',
+ 'data/flowchart_stairlift_not_moving_data.xml',
# Views
'views/repair_product_category_views.xml',
'views/intake_template_views.xml',
@@ -93,6 +96,8 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'views/repair_order_views.xml',
'views/repair_part_order_views.xml',
'views/repair_service_plan_views.xml',
+ 'views/repair_symptom_class_views.xml',
+ 'views/repair_flowchart_views.xml',
'views/sale_order_views.xml',
'views/technician_task_views.xml',
'views/res_partner_views.xml',
@@ -119,6 +124,15 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'fusion_repairs/static/src/scss/dashboard.scss',
'fusion_repairs/static/src/components/dashboard/dashboard.js',
'fusion_repairs/static/src/components/dashboard/dashboard.xml',
+ # Bundle 11: Drawflow flowchart designer + runner. CSS first.
+ 'fusion_repairs/static/src/lib/drawflow/drawflow.min.css',
+ 'fusion_repairs/static/src/lib/drawflow/drawflow.min.js',
+ 'fusion_repairs/static/src/components/flowchart_designer/flowchart_designer.scss',
+ 'fusion_repairs/static/src/components/flowchart_designer/flowchart_designer.js',
+ 'fusion_repairs/static/src/components/flowchart_designer/flowchart_designer.xml',
+ 'fusion_repairs/static/src/components/flowchart_runner/flowchart_runner.scss',
+ 'fusion_repairs/static/src/components/flowchart_runner/flowchart_runner.js',
+ 'fusion_repairs/static/src/components/flowchart_runner/flowchart_runner.xml',
],
'web.assets_frontend': [
'fusion_repairs/static/src/scss/portal_repair_mobile.scss',
diff --git a/fusion_repairs/data/flowchart_stairlift_not_moving_data.xml b/fusion_repairs/data/flowchart_stairlift_not_moving_data.xml
new file mode 100644
index 00000000..50963abc
--- /dev/null
+++ b/fusion_repairs/data/flowchart_stairlift_not_moving_data.xml
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+ Stairlift - Not Moving
+
+
+
+ Standard diagnostic walk-through for a stairlift that is not responding
+ to its controls. Most common root causes (power, seatbelt, swivel lock)
+ are checked first; if all clear, dispatch a technician.
+ ]]>
+
+
+
+
+
+ Is the power on?
+ question
+
+ 10
+ 100
+ 100
+ Ask the client if the stairlift has power. Check:
+
+
Is the unit plugged into the wall outlet?
+
Is the master key switch (usually on the carriage) turned ON?
+
Is the battery indicator LED lit?
+
+ ]]>
+
+
+
+
+ No power - check breaker
+ suggestion
+ 20
+ 450
+ 20
+ Ask the client to:
+
+
Test the wall outlet with a phone charger or lamp.
+
Check the home's electrical panel for a tripped breaker.
+
If the breaker is tripped, flip it OFF then ON.
+
+ ]]>
+
+
+
+
+ Is the seatbelt fastened?
+ question
+ 30
+ 450
+ 200
+ Most stairlifts will not move unless the seatbelt safety
+ switch is engaged. Confirm the belt is buckled.
+ ]]>
+
+
+
+
+ Fasten seatbelt and retry
+ suggestion
+ 40
+ 800
+ 120
+ Have the client buckle the seatbelt and try the controls again.
+ ]]>
+
+
+
+
+ Is the seat rotated to the forward (travel) position?
+ question
+ 50
+ 800
+ 300
+ The swivel lock must be engaged in the forward position. If
+ the seat is partially turned, the stairlift will refuse to move.
+ ]]>
+
+
+
+
+ Rotate seat to forward and retry
+ suggestion
+ 60
+ 1150
+ 220
+ Ask the client to rotate the seat back to the forward
+ travel position until the swivel lock clicks. Then try again.
+ ]]>
+
+
+
+
+ Working - resolved on call
+ outcome
+ resolved
+ 100
+ 1500
+ 150
+ Great - the stairlift is moving again. Confirm with the client,
+ close the repair, and offer to book an annual safety inspection
+ if they have not had one recently.
+ ]]>
+
+
+
+
+ Still not moving - dispatch technician
+ outcome
+ escalate
+ 110
+ 1500
+ 350
+ Common fixes are exhausted. Likely root causes for a tech to investigate:
+
+
Control board fault (most common - $300-$500 part)
+
Motor brushes worn
+
Track end-stop safety switch
+
+
Escalate and the transcript will be visible to the tech before they arrive.
+ ]]>
+
+
+
+
+
+
+
+ No - no power
+ 10
+
+
+
+
+
+ Yes - power is on
+ 20
+
+
+
+
+
+
+ Power restored - works now
+ 10
+
+
+
+
+
+ Still no power
+ 20
+
+
+
+
+
+
+ No
+ 10
+
+
+
+
+
+ Yes
+ 20
+
+
+
+
+
+
+ Works now
+ 10
+
+
+
+
+
+ Still not moving - try next check
+ 20
+
+
+
+
+
+
+ No - seat is turned
+ 10
+
+
+
+
+
+ Yes - seat is forward
+ 20
+
+
+
+
+
+
+ Works now
+ 10
+
+
+
+
+
+ Still not moving
+ 20
+
+
+
+
diff --git a/fusion_repairs/data/mail_template_data.xml b/fusion_repairs/data/mail_template_data.xml
index 562fad8c..b6bbe0d1 100644
--- a/fusion_repairs/data/mail_template_data.xml
+++ b/fusion_repairs/data/mail_template_data.xml
@@ -242,26 +242,48 @@
Repair: Parts Ordered (Client)
- Parts ordered for your {{ object.repair_order_id.x_fc_repair_category_id.name or 'equipment' }} - {{ object.repair_order_id.name }}
+ Parts ordered for your {{ object.repair_order_id.x_fc_repair_category_id.name or 'equipment' }} - expected {{ object.expected_date }}{{ (object.company_id.email_formatted or user.email_formatted) }}{{ object.partner_id.id }}
-
Parts ordered
-
- We've placed an order for the parts your
- needs. Expected arrival: .
+
Your part has been ordered
+
+
+
+
+ Expected arrival
+
+
+
+
+
+
+
+ We have placed the order for the part your
+
+ needs to get you up and running again.
+
Part
-
-
Manufacturer
-
-
Ref
+
Repair
+
Reference
-
We'll email again as soon as the parts arrive at our warehouse.
+
+
+ There is nothing for you to do right now.
+ Our office will call you as soon as the part arrives at our
+ warehouse to confirm a return visit time. If you have any
+ questions about pricing or scheduling, please reach out to
+ our office directly.
+
+
+
+ We will email you again the moment the part arrives.
+
diff --git a/fusion_repairs/data/symptom_class_data.xml b/fusion_repairs/data/symptom_class_data.xml
new file mode 100644
index 00000000..b4106475
--- /dev/null
+++ b/fusion_repairs/data/symptom_class_data.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+ Not Moving
+ not_moving
+
+ 10
+ fa-pause-circle
+ Stairlift unresponsive to controls; will not travel up or down.
+
+
+
+ Beeps / Alarm
+ beeps_alarm
+
+ 20
+ fa-bell
+
+
+
+ Stops Midway
+ stops_midway
+
+ 30
+ fa-hand-paper-o
+
+
+
+ Remote / Call Station Issue
+ remote_issue
+
+ 40
+ fa-mobile
+
+
+
+ Swivel Won't Rotate
+ swivel_stuck
+
+ 50
+ fa-refresh
+
+
+
+ Slow / Sluggish
+ slow_sluggish
+
+ 60
+ fa-tachometer
+
+
+
+ Track Issue (noise / grinding)
+ track_issue
+
+ 70
+ fa-bars
+
+
+
+
+ Not Moving
+ not_moving
+
+ 10
+ fa-pause-circle
+
+
+ Remote / Pendant Issue
+ remote_issue
+
+ 20
+ fa-mobile
+
+
+
+
+ Not Moving
+ not_moving
+
+ 10
+ fa-pause-circle
+
+
+
+
+ Will Not Rise / Recline
+ not_moving
+
+ 10
+ fa-pause-circle
+
+
+
+
diff --git a/fusion_repairs/migrations/19.0.2.2.0/post-migration.py b/fusion_repairs/migrations/19.0.2.2.0/post-migration.py
new file mode 100644
index 00000000..e4dd5710
--- /dev/null
+++ b/fusion_repairs/migrations/19.0.2.2.0/post-migration.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""Post-migration for 19.0.2.2.0 - Bundle 11.
+
+Back-fills the new x_fc_symptom_class_id M2O on repair.order from the legacy
+x_fc_issue_category Char so that existing repairs can immediately be used
+with troubleshooting flowcharts. For each distinct (category, issue_category)
+text we encounter, we either match an existing symptom.class by lowercase
+name OR create a placeholder symptom row tagged 'auto-imported' so a manager
+can clean it up later.
+"""
+
+import logging
+import re
+
+_logger = logging.getLogger(__name__)
+
+
+def _slug(text):
+ return re.sub(r'[^a-z0-9_]+', '_', (text or '').strip().lower()).strip('_') or 'unknown'
+
+
+def migrate(cr, version):
+ if not version:
+ return # fresh install, nothing to back-fill
+
+ # Find every (category_id, issue_category) pair on existing repairs that
+ # is not yet linked to a symptom class.
+ cr.execute("""
+ SELECT DISTINCT x_fc_repair_category_id, x_fc_issue_category
+ FROM repair_order
+ WHERE x_fc_issue_category IS NOT NULL
+ AND x_fc_issue_category <> ''
+ AND x_fc_symptom_class_id IS NULL
+ """)
+ pairs = cr.fetchall()
+ if not pairs:
+ return
+
+ _logger.info(
+ 'Bundle 11 migration: back-filling symptom_class for %d distinct '
+ '(category, issue_category) pairs', len(pairs),
+ )
+
+ for cat_id, issue_text in pairs:
+ if not cat_id:
+ continue
+ code = _slug(issue_text)
+ # Try to reuse an existing symptom class by code-or-name on this category.
+ cr.execute("""
+ SELECT id FROM fusion_repair_symptom_class
+ WHERE category_id = %s
+ AND (code = %s OR LOWER(name->>'en_US') = LOWER(%s))
+ LIMIT 1
+ """, (cat_id, code, issue_text))
+ row = cr.fetchone()
+ if row:
+ sym_id = row[0]
+ else:
+ # Create a placeholder so the M2O isn't lost. Manager renames later.
+ display = (issue_text or 'Imported').strip()[:80]
+ cr.execute("""
+ INSERT INTO fusion_repair_symptom_class
+ (name, code, category_id, sequence, active,
+ description, icon, create_date, write_date, create_uid,
+ write_uid)
+ VALUES (%s, %s, %s, %s, %s, %s, %s,
+ NOW(), NOW(), 1, 1)
+ RETURNING id
+ """, (
+ {'en_US': display},
+ code,
+ cat_id,
+ 90,
+ True,
+ 'Auto-imported by Bundle 11 migration from legacy '
+ 'x_fc_issue_category. Review and rename if needed.',
+ 'fa-exclamation-circle',
+ ))
+ sym_id = cr.fetchone()[0]
+ # Back-fill the M2O on every repair with this pair.
+ cr.execute("""
+ UPDATE repair_order
+ SET x_fc_symptom_class_id = %s
+ WHERE x_fc_repair_category_id = %s
+ AND x_fc_issue_category = %s
+ AND x_fc_symptom_class_id IS NULL
+ """, (sym_id, cat_id, issue_text))
+
+ _logger.info('Bundle 11 migration: back-fill complete.')
diff --git a/fusion_repairs/models/__init__.py b/fusion_repairs/models/__init__.py
index 5810aaa9..7ffe12f7 100644
--- a/fusion_repairs/models/__init__.py
+++ b/fusion_repairs/models/__init__.py
@@ -19,6 +19,9 @@ from . import repair_part_order
from . import repair_callout_rate
from . import repair_labor_warranty
from . import repair_delivery_charge
+from . import repair_symptom_class
+from . import repair_flowchart
+from . import repair_flowchart_run
from . import product_template
from . import res_partner
from . import res_users
diff --git a/fusion_repairs/models/intake_service.py b/fusion_repairs/models/intake_service.py
index 2d79bf7f..d52baeb2 100644
--- a/fusion_repairs/models/intake_service.py
+++ b/fusion_repairs/models/intake_service.py
@@ -130,6 +130,7 @@ class FusionRepairIntakeService(models.AbstractModel):
'x_fc_third_party_equipment': bool(item.get('third_party')),
'x_fc_urgency': item.get('urgency') or 'normal',
'x_fc_issue_category': item.get('issue_category') or False,
+ 'x_fc_symptom_class_id': item.get('symptom_class_id') or False,
'x_fc_is_quote_only': bool(quote_only),
'x_fc_rush_requested': bool(rush_requested),
'x_fc_rush_tier': rush_tier or False,
diff --git a/fusion_repairs/models/repair_flowchart.py b/fusion_repairs/models/repair_flowchart.py
new file mode 100644
index 00000000..839ffc72
--- /dev/null
+++ b/fusion_repairs/models/repair_flowchart.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024-2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+
+"""Troubleshooting flowchart designer-time models.
+
+Three models cooperate:
+
+ fusion.repair.flowchart one chart per (category, symptom)
+ -> fusion.repair.flowchart.node one row per visual node
+ -> fusion.repair.flowchart.edge directed edges between nodes
+
+The full visual layout (positions, drawflow internal state) is also serialised
+to flowchart.canvas_layout as JSON so the Drawflow designer round-trips
+without needing per-node x/y stored separately. The structured node/edge
+records exist for queries (e.g. "show me all flowcharts that mention 'control
+board' in any node"), reporting, runtime traversal, and so that the runner can
+operate without parsing the canvas JSON.
+"""
+
+import json
+import logging
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError, ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+NODE_TYPES = [
+ ('question', 'Question'),
+ ('suggestion', 'Suggestion'),
+ ('info', 'Info'),
+ ('outcome', 'Outcome'),
+]
+
+OUTCOME_KINDS = [
+ ('resolved', 'Resolved on Call'),
+ ('escalate', 'Escalate to Technician'),
+ ('order_part', 'Order Part'),
+]
+
+
+class FusionRepairFlowchart(models.Model):
+ _name = 'fusion.repair.flowchart'
+ _inherit = ['mail.thread']
+ _description = 'Repair Troubleshooting Flowchart'
+ _order = 'category_id, symptom_class_id, name'
+
+ name = fields.Char(required=True, translate=True, tracking=True)
+ category_id = fields.Many2one(
+ 'fusion.repair.product.category',
+ string='Equipment Category',
+ required=True,
+ index=True,
+ ondelete='cascade',
+ tracking=True,
+ )
+ symptom_class_id = fields.Many2one(
+ 'fusion.repair.symptom.class',
+ string='Symptom',
+ required=True,
+ index=True,
+ ondelete='cascade',
+ domain="[('category_id', '=', category_id)]",
+ tracking=True,
+ )
+ description = fields.Html(translate=True)
+ active = fields.Boolean(default=True, tracking=True)
+ published = fields.Boolean(
+ default=False,
+ tracking=True,
+ help='Only published flowcharts are offered to CS during intake. Draft '
+ 'charts are visible to admins for editing but never run.',
+ )
+ version = fields.Integer(
+ default=1,
+ readonly=True,
+ copy=False,
+ help='Bumped on every save in the designer; lets us snapshot which '
+ 'version a flowchart.run was based on.',
+ )
+
+ canvas_layout = fields.Text(
+ string='Designer State',
+ help='Drawflow JSON for the visual designer canvas - positions, zoom, '
+ 'connection curves. Round-tripped by the designer UI.',
+ )
+
+ node_ids = fields.One2many('fusion.repair.flowchart.node', 'flowchart_id')
+ edge_ids = fields.One2many('fusion.repair.flowchart.edge', 'flowchart_id')
+ node_count = fields.Integer(compute='_compute_counts')
+ edge_count = fields.Integer(compute='_compute_counts')
+
+ start_node_id = fields.Many2one(
+ 'fusion.repair.flowchart.node',
+ string='Start Node',
+ compute='_compute_start_node',
+ store=True,
+ help='Auto-computed - whichever node has is_start=True. Validation '
+ 'enforces exactly one.',
+ )
+
+ company_id = fields.Many2one(
+ 'res.company',
+ default=lambda self: self.env.company,
+ )
+
+ _active_per_pair_unique = models.Constraint(
+ # NULLs treated as distinct so multiple draft (active=False) charts
+ # can coexist; only one active+published chart per pair is the
+ # business rule we care about - enforced via Python below.
+ 'unique(category_id, symptom_class_id, company_id, published)',
+ 'Only one published flowchart per (category, symptom) per company. '
+ 'Unpublish or duplicate before publishing another.',
+ )
+
+ @api.depends('node_ids', 'edge_ids')
+ def _compute_counts(self):
+ for r in self:
+ r.node_count = len(r.node_ids)
+ r.edge_count = len(r.edge_ids)
+
+ @api.depends('node_ids.is_start')
+ def _compute_start_node(self):
+ for r in self:
+ r.start_node_id = r.node_ids.filtered(lambda n: n.is_start)[:1]
+
+ # ------------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------------
+ def action_open_designer(self):
+ """Open the OWL Drawflow designer as a client action."""
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'fusion_repair_flowchart_designer',
+ 'name': _('Flowchart Designer - %s') % self.name,
+ 'params': {'flowchart_id': self.id},
+ }
+
+ def action_publish(self):
+ for r in self:
+ r._validate_publishable()
+ r.published = True
+ r.message_post(body=_('Flowchart published. CS will now see it for '
+ 'new intakes on this (category, symptom).'))
+
+ def action_unpublish(self):
+ for r in self:
+ r.published = False
+
+ def action_duplicate_as_draft(self):
+ self.ensure_one()
+ copy = self.copy({
+ 'name': _('%s (copy)') % self.name,
+ 'published': False,
+ 'version': 1,
+ })
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': copy.name,
+ 'res_model': self._name,
+ 'view_mode': 'form',
+ 'res_id': copy.id,
+ }
+
+ # ------------------------------------------------------------------
+ # VALIDATION
+ # ------------------------------------------------------------------
+ def _validate_publishable(self):
+ for r in self:
+ starts = r.node_ids.filtered(lambda n: n.is_start)
+ if len(starts) != 1:
+ raise ValidationError(_(
+ 'Flowchart "%s" must have exactly one start node '
+ '(found %d). Edit in the designer and try again.'
+ ) % (r.name, len(starts)))
+ # Every non-outcome node should have at least one outgoing edge.
+ for n in r.node_ids:
+ if n.node_type != 'outcome' and not r.edge_ids.filtered(
+ lambda e: e.source_node_id == n
+ ):
+ raise ValidationError(_(
+ 'Node "%s" is not an outcome but has no outgoing edges. '
+ 'Connect it to the next step or change its type to '
+ 'Outcome.'
+ ) % n.name)
+
+ # ------------------------------------------------------------------
+ # DESIGNER RPC API (called from the OWL component)
+ # ------------------------------------------------------------------
+ @api.model
+ def designer_load(self, flowchart_id):
+ """Return everything the designer needs to render this chart."""
+ chart = self.browse(flowchart_id).exists()
+ if not chart:
+ raise UserError(_('Flowchart not found.'))
+ return {
+ 'id': chart.id,
+ 'name': chart.name,
+ 'category_id': chart.category_id.id,
+ 'category_name': chart.category_id.name,
+ 'symptom_class_id': chart.symptom_class_id.id,
+ 'symptom_name': chart.symptom_class_id.name,
+ 'published': chart.published,
+ 'version': chart.version,
+ 'canvas_layout': chart.canvas_layout or '',
+ 'nodes': [n._designer_dict() for n in chart.node_ids],
+ 'edges': [e._designer_dict() for e in chart.edge_ids],
+ }
+
+ def designer_save(self, payload):
+ """Replace the chart's nodes + edges with the designer's snapshot.
+
+ payload = {
+ 'canvas_layout': '',
+ 'nodes': [{'client_id': str, 'name': ..., 'node_type': ...,
+ 'content_html': ..., 'media_ids': [int],
+ 'is_start': bool, 'outcome_kind': ...,
+ 'canvas_x': int, 'canvas_y': int}, ...],
+ 'edges': [{'source_client_id': str, 'target_client_id': str,
+ 'label': str, 'sequence': int}, ...],
+ }
+
+ Returns the saved chart (designer_load shape).
+ """
+ self.ensure_one()
+ # Atomic replace - simplest and safest. Edges depend on nodes so
+ # delete edges first.
+ self.edge_ids.unlink()
+ self.node_ids.unlink()
+
+ client_to_db = {}
+ Node = self.env['fusion.repair.flowchart.node'].sudo()
+ for nd in payload.get('nodes', []):
+ rec = Node.create({
+ 'flowchart_id': self.id,
+ 'name': nd.get('name') or _('Untitled'),
+ 'node_type': nd.get('node_type') or 'question',
+ 'content_html': nd.get('content_html') or False,
+ 'is_start': bool(nd.get('is_start')),
+ 'outcome_kind': nd.get('outcome_kind') or False,
+ 'canvas_x': int(nd.get('canvas_x') or 0),
+ 'canvas_y': int(nd.get('canvas_y') or 0),
+ 'media_ids': [(6, 0, nd.get('media_ids') or [])],
+ })
+ client_to_db[nd.get('client_id')] = rec.id
+
+ Edge = self.env['fusion.repair.flowchart.edge'].sudo()
+ for ed in payload.get('edges', []):
+ src = client_to_db.get(ed.get('source_client_id'))
+ tgt = client_to_db.get(ed.get('target_client_id'))
+ if not src or not tgt:
+ _logger.warning(
+ 'Skipping edge with unknown client ids: %s -> %s',
+ ed.get('source_client_id'), ed.get('target_client_id'),
+ )
+ continue
+ Edge.create({
+ 'flowchart_id': self.id,
+ 'source_node_id': src,
+ 'target_node_id': tgt,
+ 'label': ed.get('label') or '',
+ 'sequence': int(ed.get('sequence') or 10),
+ })
+
+ self.write({
+ 'canvas_layout': payload.get('canvas_layout') or '',
+ 'version': self.version + 1,
+ })
+ # Soft-validate (don't raise) - the designer can publish later.
+ try:
+ self._validate_publishable()
+ except ValidationError as e:
+ _logger.info('Saved chart %s but it is not yet publishable: %s',
+ self.name, e)
+ return self.designer_load(self.id)
+
+
+class FusionRepairFlowchartNode(models.Model):
+ _name = 'fusion.repair.flowchart.node'
+ _description = 'Flowchart Node'
+ _order = 'flowchart_id, sequence, id'
+
+ flowchart_id = fields.Many2one(
+ 'fusion.repair.flowchart',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ )
+ name = fields.Char(required=True, translate=True)
+ sequence = fields.Integer(default=10)
+ node_type = fields.Selection(
+ NODE_TYPES, default='question', required=True,
+ )
+ content_html = fields.Html(
+ translate=True,
+ sanitize=True,
+ sanitize_overridable=True,
+ help='Rich text shown to CS when this node is the current step. '
+ 'Photos / videos go in media_ids instead.',
+ )
+ media_ids = fields.Many2many(
+ 'ir.attachment',
+ 'fusion_repair_flowchart_node_media_rel',
+ 'node_id', 'attachment_id',
+ string='Media',
+ help='Photos and short videos that illustrate this step.',
+ )
+ is_start = fields.Boolean(
+ string='Start',
+ help='Exactly one node per flowchart must be the start.',
+ )
+ outcome_kind = fields.Selection(
+ OUTCOME_KINDS,
+ string='Outcome Kind',
+ help='Required when node_type is Outcome - determines what happens '
+ 'when the run reaches this node.',
+ )
+
+ # Designer-only - persisted for round-trip but not used by the runtime.
+ canvas_x = fields.Integer(default=0)
+ canvas_y = fields.Integer(default=0)
+
+ outgoing_edge_ids = fields.One2many(
+ 'fusion.repair.flowchart.edge', 'source_node_id',
+ string='Outgoing Edges',
+ )
+ incoming_edge_ids = fields.One2many(
+ 'fusion.repair.flowchart.edge', 'target_node_id',
+ string='Incoming Edges',
+ )
+
+ def _designer_dict(self):
+ self.ensure_one()
+ return {
+ 'id': self.id,
+ 'name': self.name,
+ 'node_type': self.node_type,
+ 'content_html': self.content_html or '',
+ 'is_start': self.is_start,
+ 'outcome_kind': self.outcome_kind or '',
+ 'canvas_x': self.canvas_x,
+ 'canvas_y': self.canvas_y,
+ 'media_ids': self.media_ids.ids,
+ 'media': [{
+ 'id': a.id,
+ 'name': a.name,
+ 'mimetype': a.mimetype,
+ 'url': f'/web/image/{a.id}',
+ } for a in self.media_ids],
+ }
+
+
+class FusionRepairFlowchartEdge(models.Model):
+ _name = 'fusion.repair.flowchart.edge'
+ _description = 'Flowchart Edge'
+ _order = 'source_node_id, sequence, id'
+
+ flowchart_id = fields.Many2one(
+ 'fusion.repair.flowchart',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ )
+ source_node_id = fields.Many2one(
+ 'fusion.repair.flowchart.node',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ )
+ target_node_id = fields.Many2one(
+ 'fusion.repair.flowchart.node',
+ required=True,
+ ondelete='cascade',
+ index=True,
+ )
+ label = fields.Char(
+ translate=True,
+ help='Shown on the button in the runner ("Yes", "No", '
+ '"Wheel turns freely"...).',
+ )
+ sequence = fields.Integer(default=10)
+
+ def _designer_dict(self):
+ self.ensure_one()
+ return {
+ 'id': self.id,
+ 'source_node_id': self.source_node_id.id,
+ 'target_node_id': self.target_node_id.id,
+ 'label': self.label or '',
+ 'sequence': self.sequence,
+ }
diff --git a/fusion_repairs/models/repair_flowchart_run.py b/fusion_repairs/models/repair_flowchart_run.py
new file mode 100644
index 00000000..5ad27d4b
--- /dev/null
+++ b/fusion_repairs/models/repair_flowchart_run.py
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024-2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+
+"""Flowchart runtime - one fusion.repair.flowchart.run per CS troubleshooting
+session, with one fusion.repair.flowchart.run.step per node the rep visited.
+
+The transcript is the audit + handoff artefact: when CS resolves on-call we
+post it to chatter; when they escalate we copy it to internal_notes so the
+dispatched tech can see what was tried BEFORE arriving at the client.
+"""
+
+import logging
+
+from markupsafe import Markup
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+
+RUN_OUTCOMES = [
+ ('in_progress', 'In Progress'),
+ ('resolved', 'Resolved on Call'),
+ ('escalated', 'Escalated to Technician'),
+ ('order_part', 'Captured Part Order'),
+ ('abandoned', 'Abandoned'),
+]
+
+
+class FusionRepairFlowchartRun(models.Model):
+ _name = 'fusion.repair.flowchart.run'
+ _inherit = ['mail.thread']
+ _description = 'Flowchart Troubleshooting Run'
+ _order = 'started_at desc, id desc'
+
+ name = fields.Char(
+ compute='_compute_name',
+ store=True,
+ )
+ repair_order_id = fields.Many2one(
+ 'repair.order',
+ required=True,
+ index=True,
+ ondelete='cascade',
+ )
+ flowchart_id = fields.Many2one(
+ 'fusion.repair.flowchart',
+ required=True,
+ index=True,
+ ondelete='restrict',
+ )
+ flowchart_version = fields.Integer(
+ string='Chart Version Snapshot',
+ help='Version of the flowchart at the time the run was started; '
+ 'so the transcript stays valid even if the chart is later edited.',
+ )
+ partner_id = fields.Many2one(
+ related='repair_order_id.partner_id', store=True, readonly=True,
+ )
+ started_at = fields.Datetime(default=fields.Datetime.now, required=True)
+ started_by_id = fields.Many2one(
+ 'res.users', default=lambda self: self.env.user, required=True,
+ )
+ completed_at = fields.Datetime()
+ completed_by_id = fields.Many2one('res.users')
+ outcome = fields.Selection(
+ RUN_OUTCOMES,
+ default='in_progress',
+ required=True,
+ tracking=True,
+ )
+ step_ids = fields.One2many(
+ 'fusion.repair.flowchart.run.step', 'run_id',
+ string='Steps',
+ )
+ step_count = fields.Integer(compute='_compute_step_count')
+ current_node_id = fields.Many2one(
+ 'fusion.repair.flowchart.node',
+ compute='_compute_current_node',
+ store=True,
+ help='Where the runner is right now - last step\'s node if no edge '
+ 'was chosen yet, otherwise the edge\'s target node.',
+ )
+ transcript_html = fields.Html(
+ compute='_compute_transcript',
+ sanitize=False, # we control the markup
+ )
+ company_id = fields.Many2one(
+ 'res.company',
+ default=lambda self: self.env.company,
+ )
+
+ @api.depends('repair_order_id.name', 'flowchart_id.name')
+ def _compute_name(self):
+ for r in self:
+ r.name = _('%s on %s') % (
+ r.flowchart_id.name or '?', r.repair_order_id.name or '?',
+ )
+
+ @api.depends('step_ids')
+ def _compute_step_count(self):
+ for r in self:
+ r.step_count = len(r.step_ids)
+
+ @api.depends('step_ids.chosen_edge_id', 'step_ids.node_id',
+ 'flowchart_id.start_node_id')
+ def _compute_current_node(self):
+ for r in self:
+ if not r.step_ids:
+ r.current_node_id = r.flowchart_id.start_node_id
+ continue
+ last = r.step_ids[-1]
+ # If they chose an edge on the last step, we should already be
+ # showing the target. If not (just landed on a card), it's the
+ # current node itself.
+ if last.chosen_edge_id:
+ r.current_node_id = last.chosen_edge_id.target_node_id
+ else:
+ r.current_node_id = last.node_id
+
+ @api.depends('step_ids.node_name_snapshot', 'step_ids.chosen_label_snapshot',
+ 'step_ids.cs_note', 'outcome')
+ def _compute_transcript(self):
+ for r in self:
+ if not r.step_ids:
+ r.transcript_html = '
No steps recorded yet.
'
+ continue
+ lines = ['']
+ for s in r.step_ids:
+ line = f'
{s.node_name_snapshot or "?"}'
+ if s.chosen_label_snapshot:
+ line += f' → {s.chosen_label_snapshot}'
+ if s.cs_note:
+ line += f' (note: {s.cs_note})'
+ line += '
')
+ r.transcript_html = ''.join(lines)
+
+ # ------------------------------------------------------------------
+ # RUNTIME RPC API (called from the OWL runner component)
+ # ------------------------------------------------------------------
+ @api.model
+ def runtime_start_or_resume(self, repair_id):
+ """Open or resume a troubleshooting run for the given repair.
+
+ - If an in-progress run exists, return it.
+ - Otherwise resolve the flowchart from the repair's (category, symptom)
+ and create a fresh run starting at the chart's start node.
+
+ Returns the dict shape that the OWL runner consumes.
+ """
+ repair = self.env['repair.order'].browse(repair_id).exists()
+ if not repair:
+ raise UserError(_('Repair order not found.'))
+ existing = self.search([
+ ('repair_order_id', '=', repair.id),
+ ('outcome', '=', 'in_progress'),
+ ], limit=1)
+ if existing:
+ return existing._runtime_dict()
+ if not repair.x_fc_symptom_class_id:
+ raise UserError(_('Pick a Symptom on the repair before starting '
+ 'troubleshooting.'))
+ chart = self.env['fusion.repair.flowchart'].sudo().search([
+ ('category_id', '=', repair.x_fc_repair_category_id.id),
+ ('symptom_class_id', '=', repair.x_fc_symptom_class_id.id),
+ ('published', '=', True),
+ ('active', '=', True),
+ ], limit=1)
+ if not chart:
+ raise UserError(_(
+ 'No published troubleshooting flowchart found for '
+ '%(cat)s + %(sym)s. Ask a manager to publish one in '
+ 'Configuration > Troubleshooting Flowcharts.'
+ ) % {
+ 'cat': repair.x_fc_repair_category_id.name or '?',
+ 'sym': repair.x_fc_symptom_class_id.name or '?',
+ })
+ run = self.sudo().create({
+ 'repair_order_id': repair.id,
+ 'flowchart_id': chart.id,
+ 'flowchart_version': chart.version,
+ })
+ return run._runtime_dict()
+
+ def runtime_choose(self, edge_id, cs_note=''):
+ """CS clicked an answer button on the current card.
+
+ Records a step (current_node + chosen_edge + note) and returns the
+ updated run dict (which the OWL component uses to render the next card).
+ """
+ self.ensure_one()
+ if self.outcome != 'in_progress':
+ raise UserError(_('This run has already been completed.'))
+ edge = self.env['fusion.repair.flowchart.edge'].browse(edge_id).exists()
+ if not edge:
+ raise UserError(_('Chosen answer not found.'))
+ if edge.source_node_id != self.current_node_id:
+ raise UserError(_(
+ 'That answer does not belong to the current step. The chart '
+ 'may have been edited - please reload the runner.'
+ ))
+ self.env['fusion.repair.flowchart.run.step'].sudo().create({
+ 'run_id': self.id,
+ 'sequence': len(self.step_ids) + 1,
+ 'node_id': self.current_node_id.id,
+ 'node_name_snapshot': self.current_node_id.name,
+ 'chosen_edge_id': edge.id,
+ 'chosen_label_snapshot': edge.label,
+ 'cs_note': cs_note or False,
+ })
+ # current_node_id auto-recomputes via depends
+ self.invalidate_recordset(['current_node_id'])
+ return self._runtime_dict()
+
+ def runtime_complete(self, outcome, cs_note=''):
+ """CS hit a terminal outcome (resolved / escalate / order_part) OR
+ manually abandoned. Closes the run and triggers downstream effects."""
+ self.ensure_one()
+ valid = {'resolved', 'escalated', 'order_part', 'abandoned'}
+ if outcome not in valid:
+ raise UserError(_('Unknown outcome %s.') % outcome)
+ if self.outcome != 'in_progress':
+ return self._runtime_dict()
+ # Snapshot the final node as a step so the transcript shows it.
+ node = self.current_node_id
+ if node and (not self.step_ids or self.step_ids[-1].node_id != node):
+ self.env['fusion.repair.flowchart.run.step'].sudo().create({
+ 'run_id': self.id,
+ 'sequence': len(self.step_ids) + 1,
+ 'node_id': node.id,
+ 'node_name_snapshot': node.name,
+ 'cs_note': cs_note or False,
+ })
+ self.write({
+ 'outcome': outcome,
+ 'completed_at': fields.Datetime.now(),
+ 'completed_by_id': self.env.uid,
+ })
+ # Dispatch the side-effects on the repair (see _apply_outcome).
+ self._apply_outcome()
+ return self._runtime_dict()
+
+ def _apply_outcome(self):
+ """Run the side-effects on the parent repair depending on outcome."""
+ for r in self:
+ repair = r.repair_order_id
+ transcript = Markup(r.transcript_html or '')
+ if r.outcome == 'resolved':
+ repair.x_fc_resolved_on_call = True
+ repair.message_post(body=Markup(_(
+ '
Resolved on call via troubleshooting flowchart '
+ '%(name)s:
%(transcript)s'
+ )) % {'name': r.flowchart_id.name, 'transcript': transcript})
+ # Close the repair through the Odoo state machine. Reuse the
+ # visit-report wizard helper pattern.
+ try:
+ if repair.state == 'draft':
+ try:
+ repair.action_validate()
+ except Exception:
+ repair._action_repair_confirm()
+ if repair.state == 'confirmed':
+ repair.action_repair_start()
+ if repair.state == 'under_repair':
+ repair.action_repair_end()
+ except Exception as e:
+ _logger.warning(
+ 'Could not auto-close repair %s after on-call '
+ 'resolution: %s', repair.name, e,
+ )
+ elif r.outcome == 'escalated':
+ # Prepend the transcript to internal_notes so the tech sees
+ # what was tried at the top of the form.
+ existing = repair.internal_notes or ''
+ header = Markup(
+ '
+
+ Tree view shows the whole flowchart. The yellow-highlighted
+ node is the current step. Visited nodes are green. Click
+ Hide Tree to return to the card view and
+ keep answering.
+
+
+
+
+
+
+
+
+
+
diff --git a/fusion_repairs/static/src/lib/drawflow/drawflow.min.css b/fusion_repairs/static/src/lib/drawflow/drawflow.min.css
new file mode 100644
index 00000000..c9c94424
--- /dev/null
+++ b/fusion_repairs/static/src/lib/drawflow/drawflow.min.css
@@ -0,0 +1 @@
+.drawflow,.drawflow .parent-node{position:relative}.parent-drawflow{display:flex;overflow:hidden;touch-action:none;outline:0}.drawflow{width:100%;height:100%;user-select:none;perspective:0}.drawflow .drawflow-node{display:flex;align-items:center;position:absolute;background:#0ff;width:160px;min-height:40px;border-radius:4px;border:2px solid #000;color:#000;z-index:2;padding:15px}.drawflow .drawflow-node.selected{background:red}.drawflow .drawflow-node:hover{cursor:move}.drawflow .drawflow-node .inputs,.drawflow .drawflow-node .outputs{width:0}.drawflow .drawflow-node .drawflow_content_node{width:100%;display:block}.drawflow .drawflow-node .input,.drawflow .drawflow-node .output{position:relative;width:20px;height:20px;background:#fff;border-radius:50%;border:2px solid #000;cursor:crosshair;z-index:1;margin-bottom:5px}.drawflow .drawflow-node .input{left:-27px;top:2px;background:#ff0}.drawflow .drawflow-node .output{right:-3px;top:2px}.drawflow svg{z-index:0;position:absolute;overflow:visible!important}.drawflow .connection{position:absolute;pointer-events:none;aspect-ratio:1/1}.drawflow .connection .main-path{fill:none;stroke-width:5px;stroke:#4682b4;pointer-events:all}.drawflow .connection .main-path:hover{stroke:#1266ab;cursor:pointer}.drawflow .connection .main-path.selected{stroke:#43b993}.drawflow .connection .point{cursor:move;stroke:#000;stroke-width:2;fill:#fff;pointer-events:all}.drawflow .connection .point.selected,.drawflow .connection .point:hover{fill:#1266ab}.drawflow .main-path{fill:none;stroke-width:5px;stroke:#4682b4}.drawflow-delete{position:absolute;display:block;width:30px;height:30px;background:#000;color:#fff;z-index:4;border:2px solid #fff;line-height:30px;font-weight:700;text-align:center;border-radius:50%;font-family:monospace;cursor:pointer}.drawflow>.drawflow-delete{margin-left:-15px;margin-top:15px}.parent-node .drawflow-delete{right:-15px;top:-15px}
\ No newline at end of file
diff --git a/fusion_repairs/static/src/lib/drawflow/drawflow.min.js b/fusion_repairs/static/src/lib/drawflow/drawflow.min.js
new file mode 100644
index 00000000..012df79f
--- /dev/null
+++ b/fusion_repairs/static/src/lib/drawflow/drawflow.min.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Drawflow=t():e.Drawflow=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(i,s,function(t){return e[t]}.bind(null,s));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return i}));class i{constructor(e,t=null,n=null){this.events={},this.container=e,this.precanvas=null,this.nodeId=1,this.ele_selected=null,this.node_selected=null,this.drag=!1,this.reroute=!1,this.reroute_fix_curvature=!1,this.curvature=.5,this.reroute_curvature_start_end=.5,this.reroute_curvature=.5,this.reroute_width=6,this.drag_point=!1,this.editor_selected=!1,this.connection=!1,this.connection_ele=null,this.connection_selected=null,this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_x_start=0,this.pos_y=0,this.pos_y_start=0,this.mouse_x=0,this.mouse_y=0,this.line_path=5,this.first_click=null,this.force_first_input=!1,this.draggable_inputs=!0,this.useuuid=!1,this.parent=n,this.noderegister={},this.render=t,this.drawflow={drawflow:{Home:{data:{}}}},this.module="Home",this.editor_mode="edit",this.zoom=1,this.zoom_max=1.6,this.zoom_min=.5,this.zoom_value=.1,this.zoom_last_value=1,this.evCache=new Array,this.prevDiff=-1}start(){this.container.classList.add("parent-drawflow"),this.container.tabIndex=0,this.precanvas=document.createElement("div"),this.precanvas.classList.add("drawflow"),this.container.appendChild(this.precanvas),this.container.addEventListener("mouseup",this.dragEnd.bind(this)),this.container.addEventListener("mousemove",this.position.bind(this)),this.container.addEventListener("mousedown",this.click.bind(this)),this.container.addEventListener("touchend",this.dragEnd.bind(this)),this.container.addEventListener("touchmove",this.position.bind(this)),this.container.addEventListener("touchstart",this.click.bind(this)),this.container.addEventListener("contextmenu",this.contextmenu.bind(this)),this.container.addEventListener("keydown",this.key.bind(this)),this.container.addEventListener("wheel",this.zoom_enter.bind(this)),this.container.addEventListener("input",this.updateNodeValue.bind(this)),this.container.addEventListener("dblclick",this.dblclick.bind(this)),this.container.onpointerdown=this.pointerdown_handler.bind(this),this.container.onpointermove=this.pointermove_handler.bind(this),this.container.onpointerup=this.pointerup_handler.bind(this),this.container.onpointercancel=this.pointerup_handler.bind(this),this.container.onpointerout=this.pointerup_handler.bind(this),this.container.onpointerleave=this.pointerup_handler.bind(this),this.load()}pointerdown_handler(e){this.evCache.push(e)}pointermove_handler(e){for(var t=0;t100&&(n>this.prevDiff&&this.zoom_in(),n=n&&(n=parseInt(e)+1)}))})),this.nodeId=n}removeReouteConnectionSelected(){this.dispatch("connectionUnselected",!0),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})}click(e){if(this.dispatch("click",e),"fixed"===this.editor_mode){if(e.preventDefault(),"parent-drawflow"!==e.target.classList[0]&&"drawflow"!==e.target.classList[0])return!1;this.ele_selected=e.target.closest(".parent-drawflow")}else"view"===this.editor_mode?(null!=e.target.closest(".drawflow")||e.target.matches(".parent-drawflow"))&&(this.ele_selected=e.target.closest(".parent-drawflow"),e.preventDefault()):(this.first_click=e.target,this.ele_selected=e.target,0===e.button&&this.contextmenuDel(),null!=e.target.closest(".drawflow_content_node")&&(this.ele_selected=e.target.closest(".drawflow_content_node").parentElement));switch(this.ele_selected.classList[0]){case"drawflow-node":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected!=this.ele_selected&&this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.node_selected!=this.ele_selected&&this.dispatch("nodeSelected",this.ele_selected.id.slice(5)),this.node_selected=this.ele_selected,this.node_selected.classList.add("selected"),this.draggable_inputs?"SELECT"!==e.target.tagName&&(this.drag=!0):"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&"SELECT"!==e.target.tagName&&!0!==e.target.hasAttribute("contenteditable")&&(this.drag=!0);break;case"output":this.connection=!0,null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.drawConnection(e.target);break;case"parent-drawflow":case"drawflow":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.editor_selected=!0;break;case"main-path":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.connection_selected=this.ele_selected,this.connection_selected.classList.add("selected");const t=this.connection_selected.parentElement.classList;t.length>1&&(this.dispatch("connectionSelected",{output_id:t[2].slice(14),input_id:t[1].slice(13),output_class:t[3],input_class:t[4]}),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.add("selected")}));break;case"point":this.drag_point=!0,this.ele_selected.classList.add("selected");break;case"drawflow-delete":this.node_selected&&this.removeNodeId(this.node_selected.id),this.connection_selected&&this.removeConnection(),null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null)}"touchstart"===e.type?(this.pos_x=e.touches[0].clientX,this.pos_x_start=e.touches[0].clientX,this.pos_y=e.touches[0].clientY,this.pos_y_start=e.touches[0].clientY,this.mouse_x=e.touches[0].clientX,this.mouse_y=e.touches[0].clientY):(this.pos_x=e.clientX,this.pos_x_start=e.clientX,this.pos_y=e.clientY,this.pos_y_start=e.clientY),["input","output","main-path"].includes(this.ele_selected.classList[0])&&e.preventDefault(),this.dispatch("clickEnd",e)}position(e){if("touchmove"===e.type)var t=e.touches[0].clientX,n=e.touches[0].clientY;else t=e.clientX,n=e.clientY;if(this.connection&&this.updateConnection(t,n),this.editor_selected&&(i=this.canvas_x+-(this.pos_x-t),s=this.canvas_y+-(this.pos_y-n),this.dispatch("translate",{x:i,y:s}),this.precanvas.style.transform="translate("+i+"px, "+s+"px) scale("+this.zoom+")"),this.drag){e.preventDefault();var i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n,this.ele_selected.style.top=this.ele_selected.offsetTop-s+"px",this.ele_selected.style.left=this.ele_selected.offsetLeft-i+"px",this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_x=this.ele_selected.offsetLeft-i,this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_y=this.ele_selected.offsetTop-s,this.updateConnectionNodes(this.ele_selected.id)}if(this.drag_point){i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n;var o=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),l=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));this.ele_selected.setAttributeNS(null,"cx",o),this.ele_selected.setAttributeNS(null,"cy",l);const e=this.ele_selected.parentElement.classList[2].slice(9),c=this.ele_selected.parentElement.classList[1].slice(13),d=this.ele_selected.parentElement.classList[3],a=this.ele_selected.parentElement.classList[4];let r=Array.from(this.ele_selected.parentElement.children).indexOf(this.ele_selected)-1;if(this.reroute_fix_curvature){r-=this.ele_selected.parentElement.querySelectorAll(".main-path").length-1,r<0&&(r=0)}const h=e.slice(5),u=this.drawflow.drawflow[this.module].data[h].outputs[d].connections.findIndex((function(e,t){return e.node===c&&e.output===a}));this.drawflow.drawflow[this.module].data[h].outputs[d].connections[u].points[r]={pos_x:o,pos_y:l};const p=this.ele_selected.parentElement.classList[2].slice(9);this.updateConnectionNodes(p)}"touchmove"===e.type&&(this.mouse_x=t,this.mouse_y=n),this.dispatch("mouseMove",{x:t,y:n})}dragEnd(e){if("touchend"===e.type)var t=this.mouse_x,n=this.mouse_y,i=document.elementFromPoint(t,n);else t=e.clientX,n=e.clientY,i=e.target;if(this.drag&&(this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("nodeMoved",this.ele_selected.id.slice(5))),this.drag_point&&(this.ele_selected.classList.remove("selected"),this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("rerouteMoved",this.ele_selected.parentElement.classList[2].slice(14))),this.editor_selected&&(this.canvas_x=this.canvas_x+-(this.pos_x-t),this.canvas_y=this.canvas_y+-(this.pos_y-n),this.editor_selected=!1),!0===this.connection)if("input"===i.classList[0]||this.force_first_input&&(null!=i.closest(".drawflow_content_node")||"drawflow-node"===i.classList[0])){if(!this.force_first_input||null==i.closest(".drawflow_content_node")&&"drawflow-node"!==i.classList[0])s=i.parentElement.parentElement.id,o=i.classList[1];else{if(null!=i.closest(".drawflow_content_node"))var s=i.closest(".drawflow_content_node").parentElement.id;else var s=i.id;if(0===Object.keys(this.getNodeFromId(s.slice(5)).inputs).length)var o=!1;else var o="input_1"}var l=this.ele_selected.parentElement.parentElement.id,c=this.ele_selected.classList[1];if(l!==s&&!1!==o){if(0===this.container.querySelectorAll(".connection.node_in_"+s+".node_out_"+l+"."+c+"."+o).length){this.connection_ele.classList.add("node_in_"+s),this.connection_ele.classList.add("node_out_"+l),this.connection_ele.classList.add(c),this.connection_ele.classList.add(o);var d=s.slice(5),a=l.slice(5);this.drawflow.drawflow[this.module].data[a].outputs[c].connections.push({node:d,output:o}),this.drawflow.drawflow[this.module].data[d].inputs[o].connections.push({node:a,input:c}),this.updateConnectionNodes("node-"+a),this.updateConnectionNodes("node-"+d),this.dispatch("connectionCreated",{output_id:a,input_id:d,output_class:c,input_class:o})}else this.dispatch("connectionCancel",!0),this.connection_ele.remove();this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null;this.drag=!1,this.drag_point=!1,this.connection=!1,this.ele_selected=null,this.editor_selected=!1,this.dispatch("mouseUp",e)}contextmenu(e){if(this.dispatch("contextmenu",e),e.preventDefault(),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;if(this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove(),this.node_selected||this.connection_selected){var t=document.createElement("div");t.classList.add("drawflow-delete"),t.innerHTML="x",this.node_selected&&this.node_selected.appendChild(t),this.connection_selected&&this.connection_selected.parentElement.classList.length>1&&(t.style.top=e.clientY*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))+"px",t.style.left=e.clientX*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))+"px",this.precanvas.appendChild(t))}}contextmenuDel(){this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove()}key(e){if(this.dispatch("keydown",e),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;("Delete"===e.key||"Backspace"===e.key&&e.metaKey)&&(null!=this.node_selected&&"INPUT"!==this.first_click.tagName&&"TEXTAREA"!==this.first_click.tagName&&!0!==this.first_click.hasAttribute("contenteditable")&&this.removeNodeId(this.node_selected.id),null!=this.connection_selected&&this.removeConnection())}zoom_enter(e,t){e.ctrlKey&&(e.preventDefault(),e.deltaY>0?this.zoom_out():this.zoom_in())}zoom_refresh(){this.dispatch("zoom",this.zoom),this.canvas_x=this.canvas_x/this.zoom_last_value*this.zoom,this.canvas_y=this.canvas_y/this.zoom_last_value*this.zoom,this.zoom_last_value=this.zoom,this.precanvas.style.transform="translate("+this.canvas_x+"px, "+this.canvas_y+"px) scale("+this.zoom+")"}zoom_in(){this.zoomthis.zoom_min&&(this.zoom-=this.zoom_value,this.zoom_refresh())}zoom_reset(){1!=this.zoom&&(this.zoom=1,this.zoom_refresh())}createCurvature(e,t,n,i,s,o){var l=e,c=t,d=n,a=i,r=s;switch(o){case"open":if(e>=n)var h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"close":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*r;else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"other":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;default:return" M "+l+" "+c+" C "+(h=l+Math.abs(d-l)*r)+" "+c+" "+(u=d-Math.abs(d-l)*r)+" "+a+" "+d+" "+a}}drawConnection(e){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");this.connection_ele=t;var n=document.createElementNS("http://www.w3.org/2000/svg","path");n.classList.add("main-path"),n.setAttributeNS(null,"d",""),t.classList.add("connection"),t.appendChild(n),this.precanvas.appendChild(t);var i=e.parentElement.parentElement.id.slice(5),s=e.classList[1];this.dispatch("connectionStart",{output_id:i,output_class:s})}updateConnection(e,t){const n=this.precanvas,i=this.zoom;let s=n.clientWidth/(n.clientWidth*i);s=s||0;let o=n.clientHeight/(n.clientHeight*i);o=o||0;var l=this.connection_ele.children[0],c=this.ele_selected.offsetWidth/2+(this.ele_selected.getBoundingClientRect().x-n.getBoundingClientRect().x)*s,d=this.ele_selected.offsetHeight/2+(this.ele_selected.getBoundingClientRect().y-n.getBoundingClientRect().y)*o,a=e*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),r=t*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom)),h=this.curvature,u=this.createCurvature(c,d,a,r,h,"openclose");l.setAttributeNS(null,"d",u)}addConnection(e,t,n,i){var s=this.getModuleFromNodeId(e);if(s===this.getModuleFromNodeId(t)){var o=this.getNodeFromId(e),l=!1;for(var c in o.outputs[n].connections){var d=o.outputs[n].connections[c];d.node==t&&d.output==i&&(l=!0)}if(!1===l){if(this.drawflow.drawflow[s].data[e].outputs[n].connections.push({node:t.toString(),output:i}),this.drawflow.drawflow[s].data[t].inputs[i].connections.push({node:e.toString(),input:n}),this.module===s){var a=document.createElementNS("http://www.w3.org/2000/svg","svg"),r=document.createElementNS("http://www.w3.org/2000/svg","path");r.classList.add("main-path"),r.setAttributeNS(null,"d",""),a.classList.add("connection"),a.classList.add("node_in_node-"+t),a.classList.add("node_out_node-"+e),a.classList.add(n),a.classList.add(i),a.appendChild(r),this.precanvas.appendChild(a),this.updateConnectionNodes("node-"+e),this.updateConnectionNodes("node-"+t)}this.dispatch("connectionCreated",{output_id:e,input_id:t,output_class:n,input_class:i})}}}updateConnectionNodes(e){const t="node_in_"+e,n="node_out_"+e;this.line_path;const i=this.container,s=this.precanvas,o=this.curvature,l=this.createCurvature,c=this.reroute_curvature,d=this.reroute_curvature_start_end,a=this.reroute_fix_curvature,r=this.reroute_width,h=this.zoom;let u=s.clientWidth/(s.clientWidth*h);u=u||0;let p=s.clientHeight/(s.clientHeight*h);p=p||0;const f=i.querySelectorAll("."+n);Object.keys(f).map((function(t,n){if(null===f[t].querySelector(".point")){var m=i.querySelector("#"+e),g=f[t].classList[1].replace("node_in_",""),_=i.querySelector("#"+g).querySelectorAll("."+f[t].classList[4])[0],w=_.offsetWidth/2+(_.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=_.offsetHeight/2+(_.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=m.querySelectorAll("."+f[t].classList[3])[0],C=y.offsetWidth/2+(y.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,x=y.offsetHeight/2+(y.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(C,x,w,v,o,"openclose");f[t].children[0].setAttributeNS(null,"d",n)}else{const n=f[t].querySelectorAll(".point");let o="";const m=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),g=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,g,_,d,"open");o+=y,m.push(y);f=t;var C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0];g=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,g,_,d,"close");o+=y,m.push(y)}else if(0===a){var L;f=i.querySelector("#"+e),g=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,g,_,d,"open");o+=y,m.push(y);f=t,g=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,g,_,c,"other");o+=y,m.push(y)}else if(a===n.length-1){var E,R;f=t,C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0],g=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,g,_,d,"close");o+=y,m.push(y)}else{f=t,g=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,g,_,c,"other");o+=y,m.push(y)}}),a?m.forEach((e,n)=>{f[t].children[n].setAttributeNS(null,"d",e)}):f[t].children[0].setAttributeNS(null,"d",o)}}));const m=i.querySelectorAll("."+t);Object.keys(m).map((function(t,n){if(null===m[t].querySelector(".point")){var h=i.querySelector("#"+e),f=m[t].classList[2].replace("node_out_",""),g=i.querySelector("#"+f).querySelectorAll("."+m[t].classList[3])[0],_=g.offsetWidth/2+(g.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=g.offsetHeight/2+(g.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=(h=h.querySelectorAll("."+m[t].classList[4])[0]).offsetWidth/2+(h.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,y=h.offsetHeight/2+(h.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(_,w,v,y,o,"openclose");m[t].children[0].setAttributeNS(null,"d",n)}else{const n=m[t].querySelectorAll(".point");let o="";const h=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),m=((C=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,_=(E=f.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=l(m,g,_,w,d,"close");o+=v,h.push(v);f=t;var y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0];m=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,g=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"open");o+=v,h.push(v)}else if(0===a){var x;f=t,y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0],m=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,g=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"open");o+=v,h.push(v);f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,c,"other");o+=v,h.push(v)}else if(a===n.length-1){var L,E;f=t,y=t.parentElement.classList[1].replace("node_in_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[4])[0],_=(E=L.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"close");o+=v,h.push(v)}else{f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,c,"other");o+=v,h.push(v)}}),a?h.forEach((e,n)=>{m[t].children[n].setAttributeNS(null,"d",e)}):m[t].children[0].setAttributeNS(null,"d",o)}}))}dblclick(e){null!=this.connection_selected&&this.reroute&&this.createReroutePoint(this.connection_selected),"point"===e.target.classList[0]&&this.removeReroutePoint(e.target)}createReroutePoint(e){this.connection_selected.classList.remove("selected");const t=this.connection_selected.parentElement.classList[2].slice(9),n=this.connection_selected.parentElement.classList[1].slice(13),i=this.connection_selected.parentElement.classList[3],s=this.connection_selected.parentElement.classList[4];this.connection_selected=null;const o=document.createElementNS("http://www.w3.org/2000/svg","circle");o.classList.add("point");var l=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),c=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));o.setAttributeNS(null,"cx",l),o.setAttributeNS(null,"cy",c),o.setAttributeNS(null,"r",this.reroute_width);let d=0;if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;var a=document.createElementNS("http://www.w3.org/2000/svg","path");if(a.classList.add("main-path"),a.setAttributeNS(null,"d",""),e.parentElement.insertBefore(a,e.parentElement.children[t]),1===t)e.parentElement.appendChild(o);else{const n=Array.from(e.parentElement.children).indexOf(e);d=n,e.parentElement.insertBefore(o,e.parentElement.children[n+t+1])}}else e.parentElement.appendChild(o);const r=t.slice(5),h=this.drawflow.drawflow[this.module].data[r].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));void 0===this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points&&(this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points=[]),this.reroute_fix_curvature?(d>0||this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points!==[]?this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.splice(d,0,{pos_x:l,pos_y:c}):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),e.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),this.dispatch("addReroute",r),this.updateConnectionNodes(t)}removeReroutePoint(e){const t=e.parentElement.classList[2].slice(9),n=e.parentElement.classList[1].slice(13),i=e.parentElement.classList[3],s=e.parentElement.classList[4];let o=Array.from(e.parentElement.children).indexOf(e);const l=t.slice(5),c=this.drawflow.drawflow[this.module].data[l].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;e.parentElement.children[t-1].remove(),o-=t,o<0&&(o=0)}else o--;this.drawflow.drawflow[this.module].data[l].outputs[i].connections[c].points.splice(o,1),e.remove(),this.dispatch("removeReroute",l),this.updateConnectionNodes(t)}registerNode(e,t,n=null,i=null){this.noderegister[e]={html:t,props:n,options:i}}getNodeFromId(e){var t=this.getModuleFromNodeId(e);return JSON.parse(JSON.stringify(this.drawflow.drawflow[t].data[e]))}getNodesFromName(e){var t=[];const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){for(var o in n[i].data)n[i].data[o].name==e&&t.push(n[i].data[o].id)})),t}addNode(e,t,n,i,s,o,l,c,d=!1){if(this.useuuid)var a=this.getUuid();else a=this.nodeId;const r=document.createElement("div");r.classList.add("parent-node");const h=document.createElement("div");h.innerHTML="",h.setAttribute("id","node-"+a),h.classList.add("drawflow-node"),""!=o&&h.classList.add(...o.split(" "));const u=document.createElement("div");u.classList.add("inputs");const p=document.createElement("div");p.classList.add("outputs");const f={};for(var m=0;me(this.noderegister[c].html,{props:this.noderegister[c].props}),...this.noderegister[c].options}).$mount();_.appendChild(e.$el)}Object.entries(l).forEach((function(e,t){if("object"==typeof e[1])!function e(t,n,i){if(null===t)t=l[n];else t=t[n];null!==t&&Object.entries(t).forEach((function(n,s){if("object"==typeof n[1])e(t,n[0],i+"-"+n[0]);else for(var o=_.querySelectorAll("[df-"+i+"-"+n[0]+"]"),l=0;lt(this.noderegister[e.html].html,{props:this.noderegister[e.html].props}),...this.noderegister[e.html].options}).$mount();c.appendChild(t.$el)}Object.entries(e.data).forEach((function(t,n){if("object"==typeof t[1])!function t(n,i,s){if(null===n)n=e.data[i];else n=n[i];null!==n&&Object.entries(n).forEach((function(e,i){if("object"==typeof e[1])t(n,e[0],s+"-"+e[0]);else for(var o=c.querySelectorAll("[df-"+s+"-"+e[0]+"]"),l=0;l{const a=e.outputs[s].connections[o].node,r=e.outputs[s].connections[o].output,h=i.querySelector(".connection.node_in_node-"+a+".node_out_node-"+e.id+"."+s+"."+r);if(n&&0===d)for(var u=0;u{this.removeSingleConnection(e.id_output,e.id,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].inputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].inputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].inputs={};const c=t.slice(6);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push(e)}),this.drawflow.drawflow[n].data[e].inputs["input_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .inputs .input").forEach((e,t)=>{const n=e.classList[1].slice(6);parseInt(c){this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections.forEach((i,s)=>{if(i.node==e){const o=i.output.slice(6);if(parseInt(c){this.removeSingleConnection(e.id,e.id_input,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].outputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].outputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].outputs={};const c=t.slice(7);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push({node:e.node,output:e.output})}),this.drawflow.drawflow[n].data[e].outputs["output_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .outputs .output").forEach((e,t)=>{const n=e.classList[1].slice(7);parseInt(c){this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections.forEach((i,s)=>{if(i.node==e){const o=i.input.slice(7);if(parseInt(c)-1){this.module===s&&this.container.querySelector(".connection.node_in_node-"+t+".node_out_node-"+e+"."+n+"."+i).remove();var o=this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function(e,n){return e.node==t&&e.output===i}));this.drawflow.drawflow[s].data[e].outputs[n].connections.splice(o,1);var l=this.drawflow.drawflow[s].data[t].inputs[i].connections.findIndex((function(t,i){return t.node==e&&t.input===n}));return this.drawflow.drawflow[s].data[t].inputs[i].connections.splice(l,1),this.dispatch("connectionRemoved",{output_id:e,input_id:t,output_class:n,input_class:i}),!0}return!1}return!1}removeConnectionNodeId(e){const t="node_in_"+e,n="node_out_"+e,i=this.container.querySelectorAll("."+n);for(var s=i.length-1;s>=0;s--){var o=i[s].classList,l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1);var c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1),i[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}const d=this.container.querySelectorAll("."+t);for(s=d.length-1;s>=0;s--){o=d[s].classList,c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1);l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1),d[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}}getModuleFromNodeId(e){var t;const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){Object.keys(n[i].data).map((function(n,s){n==e&&(t=i)}))})),t}addModule(e){this.drawflow.drawflow[e]={data:{}},this.dispatch("moduleCreated",e)}changeModule(e){this.dispatch("moduleChanged",e),this.module=e,this.precanvas.innerHTML="",this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_y=0,this.mouse_x=0,this.mouse_y=0,this.zoom=1,this.zoom_last_value=1,this.precanvas.style.transform="",this.import(this.drawflow,!1)}removeModule(e){this.module===e&&this.changeModule("Home"),delete this.drawflow.drawflow[e],this.dispatch("moduleRemoved",e)}clearModuleSelected(){this.precanvas.innerHTML="",this.drawflow.drawflow[this.module]={data:{}}}clear(){this.precanvas.innerHTML="",this.drawflow={drawflow:{Home:{data:{}}}}}export(){const e=JSON.parse(JSON.stringify(this.drawflow));return this.dispatch("export",e),e}import(e,t=!0){this.clear(),this.drawflow=JSON.parse(JSON.stringify(e)),this.load(),t&&this.dispatch("import","import")}on(e,t){return"function"!=typeof t?(console.error("The listener callback must be a function, the given type is "+typeof t),!1):"string"!=typeof e?(console.error("The event name must be a string, the given type is "+typeof e),!1):(void 0===this.events[e]&&(this.events[e]={listeners:[]}),void this.events[e].listeners.push(t))}removeListener(e,t){if(!this.events[e])return!1;const n=this.events[e].listeners,i=n.indexOf(t);i>-1&&n.splice(i,1)}dispatch(e,t){if(void 0===this.events[e])return!1;this.events[e].listeners.forEach(e=>{e(t)})}getUuid(){for(var e=[],t=0;t<36;t++)e[t]="0123456789abcdef".substr(Math.floor(16*Math.random()),1);return e[14]="4",e[19]="0123456789abcdef".substr(3&e[19]|8,1),e[8]=e[13]=e[18]=e[23]="-",e.join("")}}}]).default}));
\ No newline at end of file
diff --git a/fusion_repairs/views/menus.xml b/fusion_repairs/views/menus.xml
index 949b7d08..61e33661 100644
--- a/fusion_repairs/views/menus.xml
+++ b/fusion_repairs/views/menus.xml
@@ -75,6 +75,25 @@
action="action_repair_delivery_charge"
sequence="67"/>
+
+
+
+
+
+
+