feat: add pagination, individual price sync arrows, and bulk price sync

- Pagination with page nav for mapped, unmatched Odoo, and unmatched WC tabs
- Per-product arrow buttons to push price in either direction
- Bulk price sync buttons: All Prices Odoo→WC and All Prices WC→Odoo
- Server-side offset/limit with total count in search endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-03-31 22:51:07 -04:00
parent 80b7d3d620
commit f9fcb6612b
6 changed files with 424 additions and 61 deletions

View File

@@ -40,12 +40,21 @@ export class ProductMapping extends Component {
// Mapped tab
mappedProducts: [],
selectedMapped: [],
mappedPage: 1,
mappedTotal: 0,
// Unmatched tab
odooProducts: [],
wooProducts: [],
selectedOdooId: false,
selectedWooId: false,
unmatchedOdooPage: 1,
unmatchedOdooTotal: 0,
unmatchedWooPage: 1,
unmatchedWooTotal: 0,
// Pagination
pageSize: 50,
// Conflicts tab
conflicts: [],
@@ -95,12 +104,17 @@ export class ProductMapping extends Component {
async _loadMapped(query = "") {
try {
const params = { query, limit: 50 };
const params = {
query,
limit: this.state.pageSize,
offset: (this.state.mappedPage - 1) * this.state.pageSize,
};
if (this.state.instanceId) {
params.instance_id = this.state.instanceId;
}
const result = await rpc("/woo/search/mapped", params);
this.state.mappedProducts = result || [];
this.state.mappedProducts = (result && result.results) || [];
this.state.mappedTotal = (result && result.total) || 0;
this.state.selectedMapped = [];
} catch (err) {
console.error("[ProductMapping] _loadMapped error:", err);
@@ -109,12 +123,17 @@ export class ProductMapping extends Component {
async _loadOdooProducts(query = "") {
try {
const params = { query, limit: 50 };
const params = {
query,
limit: this.state.pageSize,
offset: (this.state.unmatchedOdooPage - 1) * this.state.pageSize,
};
if (this.state.instanceId) {
params.instance_id = this.state.instanceId;
}
const result = await rpc("/woo/search/odoo_products", params);
this.state.odooProducts = result || [];
this.state.odooProducts = (result && result.results) || [];
this.state.unmatchedOdooTotal = (result && result.total) || 0;
} catch (err) {
console.error("[ProductMapping] _loadOdooProducts error:", err);
}
@@ -122,12 +141,17 @@ export class ProductMapping extends Component {
async _loadWooProducts(query = "") {
try {
const params = { query, limit: 50 };
const params = {
query,
limit: this.state.pageSize,
offset: (this.state.unmatchedWooPage - 1) * this.state.pageSize,
};
if (this.state.instanceId) {
params.instance_id = this.state.instanceId;
}
const result = await rpc("/woo/search/woo_products", params);
this.state.wooProducts = result || [];
this.state.wooProducts = (result && result.results) || [];
this.state.unmatchedWooTotal = (result && result.total) || 0;
} catch (err) {
console.error("[ProductMapping] _loadWooProducts error:", err);
}
@@ -213,6 +237,9 @@ export class ProductMapping extends Component {
async onInstanceChange(ev) {
const val = ev.target.value;
this.state.instanceId = val ? parseInt(val, 10) : false;
this.state.mappedPage = 1;
this.state.unmatchedOdooPage = 1;
this.state.unmatchedWooPage = 1;
await this._refreshAll();
}
@@ -221,7 +248,12 @@ export class ProductMapping extends Component {
// -------------------------------------------------------------------------
onMappedResults(results) {
this.state.mappedProducts = results;
if (results && results.results) {
this.state.mappedProducts = results.results;
this.state.mappedTotal = results.total || 0;
} else {
this.state.mappedProducts = results || [];
}
}
toggleSelectMapped(id) {
@@ -300,15 +332,25 @@ export class ProductMapping extends Component {
// -------------------------------------------------------------------------
onOdooResults(results) {
this.state.odooProducts = results;
if (!results.find((r) => r.id === this.state.selectedOdooId)) {
let items = results;
if (results && results.results) {
items = results.results;
this.state.unmatchedOdooTotal = results.total || 0;
}
this.state.odooProducts = items || [];
if (!this.state.odooProducts.find((r) => r.id === this.state.selectedOdooId)) {
this.state.selectedOdooId = false;
}
}
onWooResults(results) {
this.state.wooProducts = results;
if (!results.find((r) => r.id === this.state.selectedWooId)) {
let items = results;
if (results && results.results) {
items = results.results;
this.state.unmatchedWooTotal = results.total || 0;
}
this.state.wooProducts = items || [];
if (!this.state.wooProducts.find((r) => r.id === this.state.selectedWooId)) {
this.state.selectedWooId = false;
}
}
@@ -410,6 +452,162 @@ export class ProductMapping extends Component {
}
}
// -------------------------------------------------------------------------
// Pagination — Mapped
// -------------------------------------------------------------------------
_calcTotalPages(total) {
return Math.max(1, Math.ceil(total / this.state.pageSize));
}
get mappedTotalPages() {
return this._calcTotalPages(this.state.mappedTotal);
}
async mappedNextPage() {
if (this.state.mappedPage < this.mappedTotalPages) {
this.state.mappedPage++;
await this._loadMapped();
}
}
async mappedPrevPage() {
if (this.state.mappedPage > 1) {
this.state.mappedPage--;
await this._loadMapped();
}
}
// -------------------------------------------------------------------------
// Pagination — Unmatched Odoo
// -------------------------------------------------------------------------
get unmatchedOdooTotalPages() {
return this._calcTotalPages(this.state.unmatchedOdooTotal);
}
async unmatchedOdooNextPage() {
if (this.state.unmatchedOdooPage < this.unmatchedOdooTotalPages) {
this.state.unmatchedOdooPage++;
await this._loadOdooProducts("");
}
}
async unmatchedOdooPrevPage() {
if (this.state.unmatchedOdooPage > 1) {
this.state.unmatchedOdooPage--;
await this._loadOdooProducts("");
}
}
// -------------------------------------------------------------------------
// Pagination — Unmatched WC
// -------------------------------------------------------------------------
get unmatchedWooTotalPages() {
return this._calcTotalPages(this.state.unmatchedWooTotal);
}
async unmatchedWooNextPage() {
if (this.state.unmatchedWooPage < this.unmatchedWooTotalPages) {
this.state.unmatchedWooPage++;
await this._loadWooProducts("");
}
}
async unmatchedWooPrevPage() {
if (this.state.unmatchedWooPage > 1) {
this.state.unmatchedWooPage--;
await this._loadWooProducts("");
}
}
// -------------------------------------------------------------------------
// Individual price sync
// -------------------------------------------------------------------------
async pushPriceToOdoo(mapId) {
try {
await rpc("/web/dataset/call_kw", {
model: "woo.product.map",
method: "action_push_price_to_odoo",
args: [[mapId]],
kwargs: {},
});
this.notification.add("WC price pushed to Odoo.", { type: "success" });
await this._loadMapped();
} catch (err) {
console.error("[ProductMapping] pushPriceToOdoo error:", err);
this.notification.add("Failed to push price to Odoo.", { type: "danger" });
}
}
async pushPriceToWC(mapId) {
try {
await rpc("/web/dataset/call_kw", {
model: "woo.product.map",
method: "action_push_price_to_wc",
args: [[mapId]],
kwargs: {},
});
this.notification.add("Odoo price pushed to WC.", { type: "success" });
await this._loadMapped();
} catch (err) {
console.error("[ProductMapping] pushPriceToWC error:", err);
this.notification.add("Failed to push price to WC.", { type: "danger" });
}
}
// -------------------------------------------------------------------------
// Bulk price sync
// -------------------------------------------------------------------------
async bulkPriceOdooToWC() {
if (!this.state.instanceId) {
this.notification.add("Select an instance.", { type: "warning" });
return;
}
this.state.loading = true;
try {
await rpc("/web/dataset/call_kw", {
model: "woo.instance",
method: "action_bulk_price_odoo_to_wc",
args: [[this.state.instanceId]],
kwargs: {},
});
this.notification.add("All Odoo prices pushed to WooCommerce.", { type: "success" });
await this._refreshAll();
} catch (err) {
console.error("[ProductMapping] bulkPriceOdooToWC error:", err);
this.notification.add("Bulk price sync failed.", { type: "danger" });
} finally {
this.state.loading = false;
}
}
async bulkPriceWCToOdoo() {
if (!this.state.instanceId) {
this.notification.add("Select an instance.", { type: "warning" });
return;
}
this.state.loading = true;
try {
await rpc("/web/dataset/call_kw", {
model: "woo.instance",
method: "action_bulk_price_wc_to_odoo",
args: [[this.state.instanceId]],
kwargs: {},
});
this.notification.add("All WC prices pulled to Odoo.", { type: "success" });
await this._refreshAll();
} catch (err) {
console.error("[ProductMapping] bulkPriceWCToOdoo error:", err);
this.notification.add("Bulk price sync failed.", { type: "danger" });
} finally {
this.state.loading = false;
}
}
// -------------------------------------------------------------------------
// Top bar actions
// -------------------------------------------------------------------------