This commit is contained in:
gsinghpal
2026-03-13 12:38:28 -04:00
parent db4b9aa278
commit fc3c966484
2975 changed files with 1614 additions and 498 deletions

View File

@@ -0,0 +1,415 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<!-- ══════════════════════════════════════════════════════════════════
FlowDesignerAction — Main layout
All colours use Bootstrap/Odoo CSS custom properties so
light + dark mode work automatically.
Node-type accent colours (green, red, amber…) are intentional
design tokens — they stay the same across themes.
══════════════════════════════════════════════════════════════════ -->
<t t-name="fusion_quotations.FlowDesignerAction">
<div class="fd-designer d-flex flex-column h-100">
<!-- ── Top Toolbar ── -->
<div class="fd-toolbar d-flex align-items-center gap-2 px-3 py-2">
<button class="btn btn-sm btn-outline-secondary" t-on-click="onBack">
<i class="fa fa-arrow-left me-1"/>Back
</button>
<div class="fd-toolbar-title fw-bold text-truncate ms-2" t-esc="state.flowName"/>
<span class="badge bg-secondary-subtle text-body-secondary ms-1" t-esc="state.equipmentType"/>
<div class="flex-grow-1"/>
<!-- Add Node Buttons -->
<div class="btn-group">
<button class="btn btn-sm btn-outline-success" data-node-type="start" t-on-click="onAddNode"
title="Add Start Node">
<i class="fa fa-play me-1"/>Start
</button>
<button class="btn btn-sm btn-outline-warning" data-node-type="decision" t-on-click="onAddNode"
title="Add Decision Node">
<i class="fa fa-code-fork me-1"/>Decision
</button>
<button class="btn btn-sm btn-outline-primary" data-node-type="option_group" t-on-click="onAddNode"
title="Add Option Group">
<i class="fa fa-list-ul me-1"/>Options
</button>
<button class="btn btn-sm btn-outline-info" data-node-type="action" t-on-click="onAddNode"
title="Add Action Node">
<i class="fa fa-bolt me-1"/>Action
</button>
<button class="btn btn-sm btn-outline-secondary fd-btn-measure"
data-node-type="measurement_check" t-on-click="onAddNode"
title="Add Measurement Check">
<i class="fa fa-tachometer me-1"/>Measure
</button>
<button class="btn btn-sm btn-outline-danger" data-node-type="end" t-on-click="onAddNode"
title="Add End Node">
<i class="fa fa-stop me-1"/>End
</button>
</div>
<div class="vr mx-1"/>
<!-- Zoom Controls -->
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" t-on-click="onZoomOut" title="Zoom Out">
<i class="fa fa-search-minus"/>
</button>
<button class="btn btn-outline-secondary" t-on-click="onZoomReset" title="Reset Zoom"
style="min-width:55px;">
<t t-esc="Math.round(state.zoom * 100)"/>%
</button>
<button class="btn btn-outline-secondary" t-on-click="onZoomIn" title="Zoom In">
<i class="fa fa-search-plus"/>
</button>
</div>
<div class="vr mx-1"/>
<button class="btn btn-sm btn-outline-danger" t-on-click="onDeleteSelected"
t-att-disabled="!state.selectedNodeId and !state.selectedConnectionId"
title="Delete Selected (Del)">
<i class="fa fa-trash"/>
</button>
<button class="btn btn-sm btn-primary" t-on-click="onSave"
t-att-disabled="state.saving or !state.dirty">
<i class="fa fa-save me-1"/>
<t t-if="state.saving">Saving...</t>
<t t-else="">Save</t>
</button>
<t t-if="state.dirty">
<span class="badge bg-warning text-dark ms-1">Unsaved</span>
</t>
</div>
<!-- ── Canvas + Panel ── -->
<div class="fd-canvas-wrapper d-flex flex-grow-1 overflow-hidden position-relative">
<!-- SVG Canvas -->
<svg class="fd-svg-canvas flex-grow-1" t-ref="svgCanvas" xmlns="http://www.w3.org/2000/svg">
<!-- Grid Pattern -->
<defs>
<pattern id="fd-grid" width="20" height="20" patternUnits="userSpaceOnUse"
t-att-x="state.viewX" t-att-y="state.viewY"
t-att-patternTransform="'scale(' + state.zoom + ')'">
<circle cx="10" cy="10" r="1" class="fd-grid-dot"/>
</pattern>
<!-- Arrow markers — fill set as attribute because CSS custom
properties don't cascade into SVG marker rendering contexts -->
<marker id="fd-arrow" viewBox="0 0 10 6" refX="10" refY="3"
markerWidth="8" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,3 L0,6 z" fill="#8b95a1"/>
</marker>
<marker id="fd-arrow-selected" viewBox="0 0 10 6" refX="10" refY="3"
markerWidth="8" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,3 L0,6 z" fill="var(--bs-primary, #3b82f6)"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="url(#fd-grid)"/>
<!-- Transform group for zoom/pan -->
<g t-ref="canvasGroup" class="fd-canvas-group" t-att-transform="transformStr">
<!-- Connections -->
<t t-foreach="connectionPaths" t-as="conn" t-key="conn.id">
<!-- Invisible fat hit area for click detection -->
<path t-att-d="conn.path" fill="none" stroke="transparent" stroke-width="14"
class="fd-connection-hit" t-att-data-conn-id="conn.id"
style="cursor:pointer;"/>
<!-- Visible bezier line — stroke set as SVG attr for guaranteed rendering -->
<path t-att-d="conn.path"
class="fd-connection-path"
t-att-stroke="conn.isSelected ? 'var(--bs-primary)' : 'var(--fd-conn-stroke, #8b95a1)'"
t-att-stroke-width="conn.isSelected ? '3.5' : '2.5'"
fill="none"
stroke-linecap="round"
t-att-marker-end="conn.isSelected ? 'url(#fd-arrow-selected)' : 'url(#fd-arrow)'"
t-att-data-conn-id="conn.id"
style="pointer-events:none;"/>
<!-- Connection label with background pill -->
<t t-if="conn.label">
<rect t-att-x="conn.midX - conn.labelW / 2"
t-att-y="conn.midY - 18"
t-att-width="conn.labelW" height="20" rx="10"
class="fd-conn-label-bg"/>
<text t-att-x="conn.midX" t-att-y="conn.midY - 5"
text-anchor="middle" font-size="11" font-weight="600"
class="fd-conn-label">
<t t-esc="conn.label"/>
</text>
</t>
</t>
<!-- Nodes -->
<t t-foreach="nodesWithPorts" t-as="node" t-key="node.id">
<g class="fd-node-group" t-att-data-node-id="node.id"
t-att-transform="'translate(' + node.pos_x + ',' + node.pos_y + ')'"
style="cursor:grab;">
<!-- Node body — accent color fill is a design token, stays fixed -->
<rect x="0" y="0" t-att-width="node.width" t-att-height="node.height"
t-att-rx="node.node_type === 'start' or node.node_type === 'end' ? node.height / 2 : 8"
t-att-fill="node.meta.color + '18'"
t-att-stroke="node.isSelected ? 'var(--bs-primary)' : node.meta.color"
t-att-stroke-width="node.isSelected ? '3' : '2'"
class="fd-node-rect"/>
<!-- Icon + Name — pointer-events:none so clicks pass through to ports/rect -->
<foreignObject x="0" y="0" t-att-width="node.width" t-att-height="node.height"
style="pointer-events:none;">
<div xmlns="http://www.w3.org/1999/xhtml" class="fd-node-content"
t-att-style="'display:flex;flex-direction:column;justify-content:center;height:' + node.height + 'px;padding:0 14px;overflow:hidden;pointer-events:none;'">
<div style="display:flex;align-items:center;gap:6px;">
<i t-att-class="'fa ' + (node.icon || 'fa-circle')"
t-att-style="'color:' + node.meta.color + ';font-size:14px;flex-shrink:0;'"/>
<span class="fd-node-name" t-esc="node.name"/>
</div>
<t t-if="node.node_type === 'decision' and node.decision_field">
<div class="fd-node-detail">
<t t-esc="node.decision_field"/>
<t t-if="node.decision_operator"> <t t-esc="node.decision_operator"/> </t>
<t t-if="node.decision_value"> <t t-esc="node.decision_value"/></t>
</div>
</t>
<t t-if="node.node_type === 'action' and node.action_type">
<div class="fd-node-detail">
<t t-esc="node.action_type"/>
</div>
</t>
<t t-if="node.node_type === 'measurement_check' and node.measurement_field">
<div class="fd-node-detail">
<t t-esc="node.measurement_field"/>
<t t-if="node.comparison"> <t t-esc="node.comparison"/> </t>
<t t-if="node.threshold_value"> <t t-esc="node.threshold_value"/></t>
</div>
</t>
<t t-if="node.section_name">
<div class="fd-node-detail">
<i class="fa fa-folder-o me-1"/><t t-esc="node.section_name"/>
</div>
</t>
</div>
</foreignObject>
<!-- Ports -->
<t t-foreach="node.ports" t-as="port" t-key="port.key">
<!-- Invisible hit area — detected by elementsFromPoint in JS -->
<circle t-att-cx="port.x" t-att-cy="port.y" r="15"
fill="transparent" stroke="none"
pointer-events="all"
class="fd-port-hit fd-port"
t-att-data-port-key="port.key"
t-att-data-port-type="port.type"
t-att-data-node-id="'' + node.id"
style="cursor:crosshair;"/>
<!-- Visible port circle — concrete fill for both types -->
<circle t-att-cx="port.x" t-att-cy="port.y" r="7"
t-att-fill="port.type !== 'input' ? node.meta.color : '#8b95a1'"
class="fd-port-visual"
style="pointer-events:none;"/>
<t t-if="port.label">
<text t-att-x="port.x + (port.type === 'output' ? 12 : -12)"
t-att-y="port.y + 4"
t-att-text-anchor="port.type === 'output' ? 'start' : 'end'"
class="fd-port-label"
fill="#8b95a1">
<t t-esc="port.label"/>
</text>
</t>
</t>
</g>
</t>
</g>
</svg>
<!-- ── Properties Panel (right side) ── -->
<t t-if="state.panelOpen and selectedNode">
<div class="fd-properties-panel">
<div class="fd-panel-header d-flex align-items-center justify-content-between px-3 py-2">
<div class="d-flex align-items-center gap-2">
<i t-att-class="'fa ' + (selectedNode.icon || 'fa-circle')"
t-att-style="'color:' + (selectedNode.color || '#3b82f6')"/>
<span class="fw-bold" t-esc="getNodeTypeLabel(selectedNode.node_type)"/>
</div>
<button class="btn btn-sm btn-link text-body-secondary p-0" t-on-click="onPanelClose">
<i class="fa fa-times"/>
</button>
</div>
<div class="fd-panel-body p-3">
<!-- Common: Name -->
<div class="mb-3">
<label class="form-label fd-label">Name</label>
<input type="text" class="form-control form-control-sm"
data-field="name" t-att-value="selectedNode.name"
t-on-change="onNodeFieldChange"/>
</div>
<!-- Decision Fields -->
<t t-if="selectedNode.node_type === 'decision'">
<div class="mb-3">
<label class="form-label fd-label">Decision Field</label>
<select class="form-select form-select-sm" data-field="decision_field"
t-on-change="onNodeFieldChange">
<option value="">-- Select --</option>
<option value="equipment_type" t-att-selected="selectedNode.decision_field === 'equipment_type'">Equipment Type</option>
<option value="wheelchair_type" t-att-selected="selectedNode.decision_field === 'wheelchair_type'">Wheelchair Category</option>
<option value="powerchair_type" t-att-selected="selectedNode.decision_field === 'powerchair_type'">Power Chair Category</option>
<option value="build_type" t-att-selected="selectedNode.decision_field === 'build_type'">Build Type</option>
<option value="client_type" t-att-selected="selectedNode.decision_field === 'client_type'">Client Type</option>
<option value="reason_for_application" t-att-selected="selectedNode.decision_field === 'reason_for_application'">Reason for Application</option>
<option value="seat_width" t-att-selected="selectedNode.decision_field === 'seat_width'">Seat Width</option>
<option value="seat_depth" t-att-selected="selectedNode.decision_field === 'seat_depth'">Seat Depth</option>
<option value="client_weight" t-att-selected="selectedNode.decision_field === 'client_weight'">Client Weight</option>
<option value="back_height" t-att-selected="selectedNode.decision_field === 'back_height'">Back Height</option>
<option value="seat_to_floor" t-att-selected="selectedNode.decision_field === 'seat_to_floor'">Seat to Floor</option>
<option value="leg_rest_length" t-att-selected="selectedNode.decision_field === 'leg_rest_length'">Leg Rest Length</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fd-label">Operator</label>
<select class="form-select form-select-sm" data-field="decision_operator"
t-on-change="onNodeFieldChange">
<option value="">-- Select --</option>
<option value="eq" t-att-selected="selectedNode.decision_operator === 'eq'">=</option>
<option value="neq" t-att-selected="selectedNode.decision_operator === 'neq'"></option>
<option value="gt" t-att-selected="selectedNode.decision_operator === 'gt'">&gt;</option>
<option value="gte" t-att-selected="selectedNode.decision_operator === 'gte'"></option>
<option value="lt" t-att-selected="selectedNode.decision_operator === 'lt'">&lt;</option>
<option value="lte" t-att-selected="selectedNode.decision_operator === 'lte'"></option>
<option value="in" t-att-selected="selectedNode.decision_operator === 'in'">In List</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fd-label">Expected Value</label>
<input type="text" class="form-control form-control-sm"
data-field="decision_value" t-att-value="selectedNode.decision_value"
t-on-change="onNodeFieldChange"
placeholder="For 'In List' use comma-separated"/>
</div>
</t>
<!-- Measurement Check Fields -->
<t t-if="selectedNode.node_type === 'measurement_check'">
<div class="mb-3">
<label class="form-label fd-label">Measurement</label>
<select class="form-select form-select-sm" data-field="measurement_field"
t-on-change="onNodeFieldChange">
<option value="">-- Select --</option>
<option value="seat_width" t-att-selected="selectedNode.measurement_field === 'seat_width'">Seat Width</option>
<option value="seat_depth" t-att-selected="selectedNode.measurement_field === 'seat_depth'">Seat Depth</option>
<option value="back_width" t-att-selected="selectedNode.measurement_field === 'back_width'">Backrest Width</option>
<option value="back_height" t-att-selected="selectedNode.measurement_field === 'back_height'">Back Height</option>
<option value="seat_to_floor" t-att-selected="selectedNode.measurement_field === 'seat_to_floor'">Seat to Floor</option>
<option value="leg_rest_length" t-att-selected="selectedNode.measurement_field === 'leg_rest_length'">Leg Rest Length</option>
<option value="client_weight" t-att-selected="selectedNode.measurement_field === 'client_weight'">Client Weight</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fd-label">Comparison</label>
<select class="form-select form-select-sm" data-field="comparison"
t-on-change="onNodeFieldChange">
<option value="">-- Select --</option>
<option value="gt" t-att-selected="selectedNode.comparison === 'gt'">Greater Than</option>
<option value="gte" t-att-selected="selectedNode.comparison === 'gte'">Greater Than or Equal</option>
<option value="lt" t-att-selected="selectedNode.comparison === 'lt'">Less Than</option>
<option value="eq" t-att-selected="selectedNode.comparison === 'eq'">Equal To</option>
<option value="neq" t-att-selected="selectedNode.comparison === 'neq'">Not Equal To</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fd-label">Threshold</label>
<input type="number" class="form-control form-control-sm"
data-field="threshold_value"
t-att-value="selectedNode.threshold_value"
t-on-change="onNodeNumberChange" step="0.1"/>
</div>
</t>
<!-- Action Fields -->
<t t-if="selectedNode.node_type === 'action'">
<div class="mb-3">
<label class="form-label fd-label">Action Type</label>
<select class="form-select form-select-sm" data-field="action_type"
t-on-change="onNodeFieldChange">
<option value="">-- Select --</option>
<option value="enable" t-att-selected="selectedNode.action_type === 'enable'">Enable Options</option>
<option value="disable" t-att-selected="selectedNode.action_type === 'disable'">Disable Options</option>
<option value="require" t-att-selected="selectedNode.action_type === 'require'">Require Options</option>
<option value="skip_step" t-att-selected="selectedNode.action_type === 'skip_step'">Skip Portal Step</option>
<option value="set_value" t-att-selected="selectedNode.action_type === 'set_value'">Set Field Value</option>
</select>
</div>
<t t-if="selectedNode.action_type === 'skip_step'">
<div class="mb-3">
<label class="form-label fd-label">Target Step</label>
<input type="number" class="form-control form-control-sm"
data-field="target_step"
t-att-value="selectedNode.target_step"
t-on-change="onNodeNumberChange" min="1" max="10"/>
</div>
</t>
</t>
<!-- Option Group — options list -->
<t t-if="selectedNode.node_type === 'option_group'">
<div class="mb-3">
<div class="d-flex align-items-center justify-content-between mb-2">
<label class="form-label fd-label mb-0">Options</label>
<button class="btn btn-sm btn-outline-primary" t-on-click="onAddNodeOption">
<i class="fa fa-plus me-1"/>Add
</button>
</div>
<t t-if="selectedNode.node_options and selectedNode.node_options.length">
<t t-foreach="selectedNode.node_options" t-as="opt" t-key="opt.id">
<div class="fd-option-row d-flex align-items-center gap-2 mb-2">
<span class="fd-option-bullet" t-att-style="'background:' + (selectedNode.color || '#3b82f6')"/>
<input type="text" class="form-control form-control-sm flex-grow-1"
t-att-value="opt.name"
t-att-data-index="opt_index"
t-on-change="onNodeOptionNameChange"/>
<button class="btn btn-sm btn-link text-danger p-0"
t-att-data-index="opt_index"
t-on-click="onRemoveNodeOption">
<i class="fa fa-trash-o"/>
</button>
</div>
</t>
</t>
<t t-else="">
<div class="text-body-secondary small fst-italic">No options yet. Click "Add" to create one.</div>
</t>
</div>
</t>
<!-- Node info footer -->
<div class="mt-4 pt-3 border-top">
<div class="text-body-secondary small">
<div><strong>ID:</strong> <t t-esc="selectedNode.id"/></div>
<div><strong>Position:</strong> (<t t-esc="Math.round(selectedNode.pos_x)"/>, <t t-esc="Math.round(selectedNode.pos_y)"/>)</div>
</div>
</div>
</div>
</div>
</t>
<!-- Loading overlay -->
<t t-if="state.loading">
<div class="fd-loading-overlay d-flex align-items-center justify-content-center">
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-primary mb-2"/>
<div class="text-body-secondary">Loading flow...</div>
</div>
</div>
</t>
</div>
</div>
</t>
</templates>