From 349f9af2da314cca114e1d126f24283db78a0d3d Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 23 Apr 2026 11:21:41 -0400 Subject: [PATCH] feat(ops/client): edit/delete/reorder subscriptions + rebate nesting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InlineField on monthly row price (dblclick) + annual row monthly base price. Saves via Service Subscription.monthly_price → mirrored back into the UI row's actual_price; drops an audit line on the customer timeline. - Delete button (confirm dialog, v-if=can('delete_records')) on both monthly + annual rows. Uses deleteDoc + local splice + invalidates location + section caches. - display_order custom Int field on Service Subscription, persisted in 10-step increments on drag reorder (so manual inserts have room to squeeze between without a full re-number pass). loadSubscriptions sorts by display_order first so the dispatcher-controlled order survives a page reload and can drive invoice print ordering later. - Rebate rows nested visually: 32px indent + arrow glyph + lighter red background + smaller type + inherited red color on the inline price input. Matches the invoice PDF grouping dispatchers expect. --- apps/ops/.quasar/client-entry.js | 2 + apps/ops/src/composables/useClientData.js | 8 +- .../src/composables/useSubscriptionActions.js | 31 +++++- apps/ops/src/pages/ClientDetailPage.vue | 100 +++++++++++++++++- 4 files changed, 132 insertions(+), 9 deletions(-) diff --git a/apps/ops/.quasar/client-entry.js b/apps/ops/.quasar/client-entry.js index de61936..8c0244d 100644 --- a/apps/ops/.quasar/client-entry.js +++ b/apps/ops/.quasar/client-entry.js @@ -33,6 +33,8 @@ import 'quasar/dist/quasar.css' import 'src/css/app.scss' +import 'src/css/tech.scss' + import createQuasarApp from './app.js' import quasarUserOptions from './quasar-user-options.js' diff --git a/apps/ops/src/composables/useClientData.js b/apps/ops/src/composables/useClientData.js index da8f017..cc09770 100644 --- a/apps/ops/src/composables/useClientData.js +++ b/apps/ops/src/composables/useClientData.js @@ -70,8 +70,11 @@ export function useClientData (deps) { fields: ['name', 'plan_name', 'service_category', 'monthly_price', 'billing_cycle', 'service_location', 'status', 'start_date', 'end_date', 'cancellation_date', 'contract_duration', 'product_sku', 'speed_down', 'speed_up', - 'radius_user', 'device'], - limit: 100, orderBy: 'start_date desc, creation desc', + 'radius_user', 'device', 'display_order'], + limit: 100, + // display_order first (dispatcher-controlled), then by start date so + // newer subs float up when display_order hasn't been set yet (= 0). + orderBy: 'display_order asc, start_date desc, creation desc', }) // Map Service Subscription → UI row shape (matches what useSubscriptionGroups @@ -106,6 +109,7 @@ export function useClientData (deps) { radius_user: doc.radius_user || '', radius_pwd: '', device: doc.device || '', + display_order: Number(doc.display_order || 0), qty: 1, })) } diff --git a/apps/ops/src/composables/useSubscriptionActions.js b/apps/ops/src/composables/useSubscriptionActions.js index c153772..a18638d 100644 --- a/apps/ops/src/composables/useSubscriptionActions.js +++ b/apps/ops/src/composables/useSubscriptionActions.js @@ -188,8 +188,35 @@ export function useSubscriptionActions (subscriptions, customer, comments, inval } catch {} } - function onSubDragChange (evt, locName) { - if (evt.added || evt.removed) invalidateCache(locName) + // vuedraggable mutates the bound list in-place, so by the time this + // handler fires the `items` array (passed by the template) already + // reflects the new order. We persist display_order in 10-step increments + // — leaves room for manual inserts (frappe Link-sort-by-number ugh) — + // and only PUT the rows whose position actually changed to avoid + // N writes on every drag. + async function onSubDragChange (evt, locName, items = null) { + // vuedraggable emits an object with {added|removed|moved}; old signature + // (evt, locName) with no items still works for cache-only invalidation. + if (evt.added || evt.removed || evt.moved) invalidateCache(locName) + if (!items || !items.length) return + + const writes = [] + for (let i = 0; i < items.length; i++) { + const want = (i + 1) * 10 // 10, 20, 30… (keeps manual gaps) + const item = items[i] + if (!item || item.display_order === want) continue + item.display_order = want // optimistic local update + writes.push( + authFetch(BASE_URL + '/api/resource/Service%20Subscription/' + encodeURIComponent(item.name), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ display_order: want }), + }).catch(() => null) + ) + } + if (writes.length) { + try { await Promise.all(writes) } catch {} + } } return { diff --git a/apps/ops/src/pages/ClientDetailPage.vue b/apps/ops/src/pages/ClientDetailPage.vue index ef21f61..54f23d3 100644 --- a/apps/ops/src/pages/ClientDetailPage.vue +++ b/apps/ops/src/pages/ClientDetailPage.vue @@ -202,7 +202,7 @@
+ @change="onSubDragChange($event, loc.name, section.items)">