feat: add WooCommerce sync dashboard widget

- WooDashboard OWL client action registered as fusion_woocommerce.woo_dashboard
- Cards: pending orders (clickable), last sync (relative time), errors 24h (clickable),
  products mapped % with progress bar
- Quick actions: Sync Now, View Conflicts, Open Mapping, View Orders
- Instances table showing name, connection state, last sync datetime
- Auto-refreshes every 60 s while mounted
- All data fetched via standalone rpc() per Odoo 19 rules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-03-31 20:53:50 -04:00
parent de14a28112
commit 2dd1a2be6c
2 changed files with 363 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
/** @odoo-module **/
import { Component, useState, onWillStart, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { rpc } from "@web/core/network/rpc";
/**
* WooDashboard — OWL client action for the WooCommerce sync dashboard.
*
* Shows:
* - Orders pending sync
* - Last sync time (relative)
* - Errors in last 24 h
* - Products mapped / unmapped (progress bar)
* - Quick actions: Sync Now, View Conflicts, Open Mapping
*/
export class WooDashboard extends Component {
static template = "fusion_woocommerce.Dashboard";
static props = ["action", "*"];
setup() {
this.actionService = useService("action");
this.notification = useService("notification");
this.state = useState({
loading: true,
// Stats
pendingOrders: 0,
lastSync: null,
errors24h: 0,
mappedCount: 0,
totalProducts: 0,
// Instances
instances: [],
});
onWillStart(async () => {
await this._loadDashboard();
});
// Refresh every 60 s while mounted
this._refreshInterval = null;
onMounted(() => {
this._refreshInterval = setInterval(() => {
this._loadDashboard();
}, 60000);
});
}
// -------------------------------------------------------------------------
// Data
// -------------------------------------------------------------------------
async _loadDashboard() {
try {
await Promise.all([
this._loadInstances(),
this._loadPendingOrders(),
this._loadErrors(),
this._loadProductStats(),
this._loadLastSync(),
]);
} catch (err) {
console.error("[WooDashboard] _loadDashboard error:", err);
} finally {
this.state.loading = false;
}
}
async _loadInstances() {
const result = await rpc("/web/dataset/call_kw", {
model: "woo.instance",
method: "search_read",
args: [[]],
kwargs: { fields: ["id", "name", "state", "last_sync"], limit: 20 },
});
this.state.instances = result || [];
}
async _loadPendingOrders() {
const count = await rpc("/web/dataset/call_kw", {
model: "woo.order",
method: "search_count",
args: [[["state", "in", ["new", "confirmed"]]]],
kwargs: {},
});
this.state.pendingOrders = count || 0;
}
async _loadErrors() {
// Errors from woo.sync.log in the last 24 h
const since = new Date(Date.now() - 24 * 3600 * 1000);
const sinceStr = since.toISOString().replace("T", " ").substring(0, 19);
const count = await rpc("/web/dataset/call_kw", {
model: "woo.sync.log",
method: "search_count",
args: [[
["state", "=", "failed"],
["create_date", ">=", sinceStr],
]],
kwargs: {},
});
this.state.errors24h = count || 0;
}
async _loadProductStats() {
const [mapped, total] = await Promise.all([
rpc("/web/dataset/call_kw", {
model: "woo.product.map",
method: "search_count",
args: [[["state", "=", "mapped"]]],
kwargs: {},
}),
rpc("/web/dataset/call_kw", {
model: "woo.product.map",
method: "search_count",
args: [[]],
kwargs: {},
}),
]);
this.state.mappedCount = mapped || 0;
this.state.totalProducts = total || 0;
}
async _loadLastSync() {
// Get the most recent successful sync log entry
const result = await rpc("/web/dataset/call_kw", {
model: "woo.sync.log",
method: "search_read",
args: [[["state", "=", "success"]]],
kwargs: {
fields: ["create_date"],
limit: 1,
order: "create_date desc",
},
});
if (result && result.length) {
this.state.lastSync = result[0].create_date;
}
}
// -------------------------------------------------------------------------
// Computed / helpers
// -------------------------------------------------------------------------
get mappedPercent() {
if (!this.state.totalProducts) return 0;
return Math.round((this.state.mappedCount / this.state.totalProducts) * 100);
}
get lastSyncRelative() {
if (!this.state.lastSync) return "Never";
const past = new Date(this.state.lastSync.replace(" ", "T") + "Z");
const diffMs = Date.now() - past.getTime();
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return "Just now";
if (diffMin < 60) return `${diffMin} minute${diffMin > 1 ? "s" : ""} ago`;
const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return `${diffHr} hour${diffHr > 1 ? "s" : ""} ago`;
const diffDay = Math.floor(diffHr / 24);
return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`;
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
async syncNow() {
try {
const instanceIds = this.state.instances.map((i) => i.id);
if (!instanceIds.length) {
this.notification.add("No WooCommerce instances configured.", { type: "warning" });
return;
}
await rpc("/web/dataset/call_kw", {
model: "woo.instance",
method: "action_sync",
args: [instanceIds],
kwargs: {},
});
this.notification.add("Sync started for all instances.", { type: "success" });
} catch (err) {
console.error("[WooDashboard] syncNow error:", err);
this.notification.add("Failed to start sync.", { type: "danger" });
}
}
openOrders() {
this.actionService.doAction("fusion_woocommerce.action_woo_order");
}
openSyncLogs() {
this.actionService.doAction({
type: "ir.actions.act_window",
name: "Sync Errors (Last 24 h)",
res_model: "woo.sync.log",
view_mode: "tree,form",
domain: [["state", "=", "failed"]],
target: "current",
});
}
openConflicts() {
this.actionService.doAction("fusion_woocommerce.action_woo_conflict");
}
openMapping() {
this.actionService.doAction({
type: "ir.actions.client",
tag: "fusion_woocommerce.product_mapping",
name: "Product Mapping",
});
}
}
registry.category("actions").add("fusion_woocommerce.woo_dashboard", WooDashboard);