feat: add OWL product mapping UI with live AJAX search
- AjaxSearch component: debounced 300ms, calls /woo/search/* endpoints via rpc() - ProductMapping client action: 3 tabs (Mapped, Unmatched, Conflicts) - Mapped tab: live search, bulk unmap/sync, per-row price/inventory sync toggles - Unmatched tab: split Odoo|WC panels, click-to-select, Map/Create/Ignore actions - Conflicts tab: Use Odoo / Use WC per-row and bulk resolve - Top bar: instance selector, Fetch Products, Sync Now, live stats - woo_dashboard.xml updated with ir.actions.client records - woo_menus.xml pointed at new client action - CSS: full layout styles, badges, split view, progress bar, buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- ===================================================================
|
||||
AjaxSearch
|
||||
=================================================================== -->
|
||||
<t t-name="fusion_woocommerce.AjaxSearch">
|
||||
<div class="woo-search-wrap">
|
||||
<span class="woo-search-icon fa fa-search"/>
|
||||
<input
|
||||
type="text"
|
||||
class="woo-search-input"
|
||||
t-att-placeholder="props.placeholder or 'Search…'"
|
||||
t-att-value="state.query"
|
||||
t-on-input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ===================================================================
|
||||
ProductMapping — main client action
|
||||
=================================================================== -->
|
||||
<t t-name="fusion_woocommerce.ProductMapping">
|
||||
<div class="o_action o_client_action">
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="woo-topbar">
|
||||
<!-- Instance selector -->
|
||||
<select t-on-change="onInstanceChange">
|
||||
<option value="">All Instances</option>
|
||||
<t t-foreach="state.instances" t-as="inst" t-key="inst.id">
|
||||
<option t-att-value="inst.id" t-att-selected="state.instanceId === inst.id">
|
||||
<t t-esc="inst.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
|
||||
<!-- Fetch / Sync buttons -->
|
||||
<button class="woo-btn woo-btn-secondary" t-on-click="fetchProducts"
|
||||
t-att-disabled="state.loading">
|
||||
<i class="fa fa-download me-1"/> Fetch Products
|
||||
</button>
|
||||
<button class="woo-btn woo-btn-primary" t-on-click="syncNow"
|
||||
t-att-disabled="state.loading">
|
||||
<i class="fa fa-refresh me-1"/> Sync Now
|
||||
</button>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="flex-grow-1"/>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="woo-stat">
|
||||
<span class="woo-stat-value" t-esc="state.mappedCount"/>
|
||||
<span class="woo-stat-label">Mapped</span>
|
||||
</div>
|
||||
<div class="woo-stat">
|
||||
<span class="woo-stat-value" t-esc="state.unmappedCount"/>
|
||||
<span class="woo-stat-label">Unmapped</span>
|
||||
</div>
|
||||
<div class="woo-stat">
|
||||
<span class="woo-stat-value" t-esc="state.conflictCount"/>
|
||||
<span class="woo-stat-label">Conflicts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<t t-if="state.loading">
|
||||
<div class="woo-loading">
|
||||
<div class="woo-spinner"/>
|
||||
Loading…
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-else="">
|
||||
<div class="p-3">
|
||||
<!-- Tab navigation -->
|
||||
<div class="woo-tabs">
|
||||
<button class="woo-tab" t-att-class="state.activeTab === 'mapped' ? 'active' : ''"
|
||||
t-on-click="() => this.setTab('mapped')">
|
||||
Mapped Products
|
||||
<span class="ms-1 badge bg-secondary" t-esc="state.mappedProducts.length"/>
|
||||
</button>
|
||||
<button class="woo-tab" t-att-class="state.activeTab === 'unmatched' ? 'active' : ''"
|
||||
t-on-click="() => this.setTab('unmatched')">
|
||||
Unmatched Products
|
||||
<span class="ms-1 badge bg-warning text-dark" t-esc="state.wooProducts.length"/>
|
||||
</button>
|
||||
<button class="woo-tab" t-att-class="state.activeTab === 'conflicts' ? 'active' : ''"
|
||||
t-on-click="() => this.setTab('conflicts')">
|
||||
Conflicts
|
||||
<span class="ms-1 badge bg-danger" t-esc="state.conflictCount"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
TAB: Mapped Products
|
||||
===================================================== -->
|
||||
<t t-if="state.activeTab === 'mapped'">
|
||||
<div class="woo-map-actions">
|
||||
<AjaxSearch
|
||||
endpoint="'/woo/search/mapped'"
|
||||
t-props="{ instanceId: state.instanceId, onResults: onMappedResults.bind(this), placeholder: 'Search mapped products…' }"
|
||||
/>
|
||||
<button class="woo-btn woo-btn-danger woo-btn-sm"
|
||||
t-on-click="unmapSelected"
|
||||
t-att-disabled="!state.selectedMapped.length">
|
||||
<i class="fa fa-unlink me-1"/> Unmap Selected
|
||||
</button>
|
||||
<button class="woo-btn woo-btn-success woo-btn-sm"
|
||||
t-on-click="syncSelected"
|
||||
t-att-disabled="!state.selectedMapped.length">
|
||||
<i class="fa fa-refresh me-1"/> Sync Selected
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<t t-if="!state.mappedProducts.length">
|
||||
<div class="woo-empty">
|
||||
<div class="woo-empty-icon">🔗</div>
|
||||
<div class="woo-empty-text">No mapped products found.</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="woo-table-wrap">
|
||||
<table class="woo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="woo-check-col">
|
||||
<input type="checkbox" t-on-change="toggleSelectAllMapped"/>
|
||||
</th>
|
||||
<th>WooCommerce Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Odoo Product</th>
|
||||
<th>Instance</th>
|
||||
<th>Price Sync</th>
|
||||
<th>Inventory Sync</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="state.mappedProducts" t-as="p" t-key="p.id">
|
||||
<tr t-att-class="isMappedSelected(p.id) ? 'selected' : ''">
|
||||
<td class="woo-check-col">
|
||||
<input type="checkbox"
|
||||
t-att-checked="isMappedSelected(p.id)"
|
||||
t-on-change="() => this.toggleSelectMapped(p.id)"/>
|
||||
</td>
|
||||
<td><t t-esc="p.woo_product_name"/></td>
|
||||
<td><code><t t-esc="p.woo_sku"/></code></td>
|
||||
<td><t t-esc="p.odoo_product_name"/></td>
|
||||
<td><t t-esc="p.instance_name"/></td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
t-att-checked="p.sync_price"
|
||||
t-on-change="() => this.togglePriceSync(p.id, p.sync_price)"/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
t-att-checked="p.sync_inventory"
|
||||
t-on-change="() => this.toggleInventorySync(p.id, p.sync_inventory)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- =====================================================
|
||||
TAB: Unmatched Products
|
||||
===================================================== -->
|
||||
<t t-if="state.activeTab === 'unmatched'">
|
||||
<!-- Map action bar -->
|
||||
<div class="woo-map-actions">
|
||||
<button class="woo-btn woo-btn-primary"
|
||||
t-on-click="mapSelected"
|
||||
t-att-disabled="!canMap">
|
||||
<i class="fa fa-link me-1"/> Map Selected
|
||||
</button>
|
||||
<t t-if="!canMap">
|
||||
<small class="text-muted align-self-center">
|
||||
Select one Odoo product and one WooCommerce product to map them.
|
||||
</small>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="woo-split">
|
||||
<!-- Odoo products panel -->
|
||||
<div class="woo-split-panel">
|
||||
<div class="woo-split-panel-header">
|
||||
<span>Odoo Products</span>
|
||||
<AjaxSearch
|
||||
endpoint="'/woo/search/odoo_products'"
|
||||
t-props="{ instanceId: state.instanceId, onResults: onOdooResults.bind(this), placeholder: 'Search Odoo…' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="woo-split-list">
|
||||
<t t-if="!state.odooProducts.length">
|
||||
<div class="woo-empty">
|
||||
<div class="woo-empty-text">No Odoo products found.</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="state.odooProducts" t-as="op" t-key="op.id">
|
||||
<div class="woo-split-item"
|
||||
t-att-class="state.selectedOdooId === op.id ? 'selected' : ''"
|
||||
t-on-click="() => this.selectOdoo(op.id)">
|
||||
<div class="woo-split-item-name"><t t-esc="op.name"/></div>
|
||||
<div class="woo-split-item-sub">
|
||||
<t t-if="op.default_code">SKU: <t t-esc="op.default_code"/> · </t>
|
||||
$<t t-esc="op.list_price.toFixed(2)"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="woo-split-divider">
|
||||
<i class="fa fa-exchange text-muted"/>
|
||||
</div>
|
||||
|
||||
<!-- WooCommerce products panel -->
|
||||
<div class="woo-split-panel">
|
||||
<div class="woo-split-panel-header">
|
||||
<span>WooCommerce Products</span>
|
||||
<AjaxSearch
|
||||
endpoint="'/woo/search/woo_products'"
|
||||
t-props="{ instanceId: state.instanceId, onResults: onWooResults.bind(this), placeholder: 'Search WooCommerce…' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="woo-split-list">
|
||||
<t t-if="!state.wooProducts.length">
|
||||
<div class="woo-empty">
|
||||
<div class="woo-empty-text">No unmatched WooCommerce products.</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="state.wooProducts" t-as="wp" t-key="wp.id">
|
||||
<div class="woo-split-item"
|
||||
t-att-class="state.selectedWooId === wp.id ? 'selected' : ''"
|
||||
t-on-click="() => this.selectWoo(wp.id)">
|
||||
<div class="woo-split-item-name"><t t-esc="wp.woo_product_name"/></div>
|
||||
<div class="woo-split-item-sub">
|
||||
<t t-if="wp.woo_sku">SKU: <t t-esc="wp.woo_sku"/> · </t>
|
||||
<t t-esc="wp.woo_product_type"/>
|
||||
</div>
|
||||
<!-- Per-item actions -->
|
||||
<div class="mt-1 d-flex gap-1">
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click.stop="() => this.createInOdoo(wp.id)">
|
||||
Create in Odoo
|
||||
</button>
|
||||
<button class="woo-btn woo-btn-danger woo-btn-sm"
|
||||
t-on-click.stop="() => this.ignoreWoo(wp.id)">
|
||||
Ignore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Odoo-only actions (create in WC) shown below list -->
|
||||
<t t-if="state.selectedOdooId">
|
||||
<div class="mt-2">
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click="() => this.createInWC(state.selectedOdooId)">
|
||||
<i class="fa fa-cloud-upload me-1"/> Create Selected in WooCommerce
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- =====================================================
|
||||
TAB: Conflicts
|
||||
===================================================== -->
|
||||
<t t-if="state.activeTab === 'conflicts'">
|
||||
<t t-if="!state.conflicts.length">
|
||||
<div class="woo-empty">
|
||||
<div class="woo-empty-icon">✅</div>
|
||||
<div class="woo-empty-text">No pending conflicts. Everything is in sync!</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="woo-map-actions">
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click="() => this.resolveAllConflicts('use_odoo')">
|
||||
Use Odoo for All
|
||||
</button>
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click="() => this.resolveAllConflicts('use_woo')">
|
||||
Use WooCommerce for All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="woo-table-wrap">
|
||||
<table class="woo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Field</th>
|
||||
<th>Odoo Value</th>
|
||||
<th>WooCommerce Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="state.conflicts" t-as="c" t-key="c.id">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="woo-badge woo-badge-conflict">
|
||||
<t t-esc="c.conflict_type"/>
|
||||
</span>
|
||||
</td>
|
||||
<td><t t-esc="c.field_name"/></td>
|
||||
<td><t t-esc="c.odoo_value"/></td>
|
||||
<td><t t-esc="c.woo_value"/></td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click="() => this.resolveConflict(c.id, 'use_odoo')">
|
||||
Use Odoo
|
||||
</button>
|
||||
<button class="woo-btn woo-btn-secondary woo-btn-sm"
|
||||
t-on-click="() => this.resolveConflict(c.id, 'use_woo')">
|
||||
Use WooCommerce
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user