From c6ced906d9b70a654af8b522bff4118ee61fb039 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Mon, 8 Dec 2025 17:34:28 +0800 Subject: [PATCH] Add view column options to inventory screen Put options into local storage --- src/web/assets/inventory/InventoryAsset.php | 5 + .../assets/inventory/dist/css/inventory.css | 2 +- .../inventory/dist/css/inventory.css.map | 2 +- src/web/assets/inventory/dist/inventory.js | 2 +- .../assets/inventory/dist/inventory.js.map | 2 +- .../assets/inventory/src/css/inventory.scss | 5 + .../src/js/InventoryLevelsManager.js | 252 +++++++++++++++++- 7 files changed, 264 insertions(+), 6 deletions(-) diff --git a/src/web/assets/inventory/InventoryAsset.php b/src/web/assets/inventory/InventoryAsset.php index 6cde1b2bf6..b94a875e40 100644 --- a/src/web/assets/inventory/InventoryAsset.php +++ b/src/web/assets/inventory/InventoryAsset.php @@ -62,6 +62,11 @@ public function registerAssetFiles($view): void 'Available', 'On Hand', 'Incoming', + 'View', + 'View settings', + 'Table Columns', + 'Purchasable', + 'SKU', ]); } } diff --git a/src/web/assets/inventory/dist/css/inventory.css b/src/web/assets/inventory/dist/css/inventory.css index 0028513110..9fe2c164ab 100644 --- a/src/web/assets/inventory/dist/css/inventory.css +++ b/src/web/assets/inventory/dist/css/inventory.css @@ -1,2 +1,2 @@ -.ltr .inventory-headers{-webkit-justify-content:flex-end;justify-content:flex-end}.rtl .inventory-headers{-webkit-justify-content:flex-start;justify-content:flex-start}.ltr .inventory-cell{text-align:right}.ltr .inventory-cell .action-btn{right:calc(var(--touch-target-size)*-1/1.5)}.rtl .inventory-cell{text-align:left}.rtl .inventory-cell .action-btn{left:calc(var(--touch-target-size)*-1/1.5)}#inventory-levels .inventory-cell{position:relative}#inventory-levels .inventory-cell .action-btn{opacity:0;position:absolute;z-index:99}#inventory-levels .inventory-cell .action-btn[aria-expanded=true],#inventory-levels .vue-admin-table tbody tr:hover .inventory-cell .action-btn{opacity:1} +.inventory-levels-toolbar{margin-bottom:var(--m)}.ltr .inventory-headers{-webkit-justify-content:flex-end;justify-content:flex-end}.rtl .inventory-headers{-webkit-justify-content:flex-start;justify-content:flex-start}.ltr .inventory-cell{text-align:right}.ltr .inventory-cell .action-btn{right:calc(var(--touch-target-size)*-1/1.5)}.rtl .inventory-cell{text-align:left}.rtl .inventory-cell .action-btn{left:calc(var(--touch-target-size)*-1/1.5)}#inventory-levels .inventory-cell{position:relative}#inventory-levels .inventory-cell .action-btn{opacity:0;position:absolute;z-index:99}#inventory-levels .inventory-cell .action-btn[aria-expanded=true],#inventory-levels .vue-admin-table tbody tr:hover .inventory-cell .action-btn{opacity:1} /*# sourceMappingURL=inventory.css.map*/ \ No newline at end of file diff --git a/src/web/assets/inventory/dist/css/inventory.css.map b/src/web/assets/inventory/dist/css/inventory.css.map index 77f1e96a94..1204b0dcfe 100644 --- a/src/web/assets/inventory/dist/css/inventory.css.map +++ b/src/web/assets/inventory/dist/css/inventory.css.map @@ -1 +1 @@ -{"version":3,"file":"css/inventory.css","mappings":"AACE,wBACE,0DAGF,wBACE,8DAeF,qBACE,iBAGF,iCACE,4CAKF,qBACE,gBAGF,iCACE,2CAKF,kCACE,kBAGF,8CACE,UACA,kBACA,WAQA,gJACE","sources":["webpack:///./css/inventory.scss"],"sourcesContent":[".inventory-headers {\n .ltr & {\n justify-content: flex-end;\n }\n\n .rtl & {\n justify-content: flex-start;\n }\n}\n\n.inventory-cell {\n .ltr & {\n text-align: right;\n }\n\n .rtl & {\n text-align: left;\n }\n}\n\n.ltr {\n .inventory-cell {\n text-align: right;\n }\n\n .inventory-cell .action-btn {\n right: calc(var(--touch-target-size) * -1 / 1.5);\n }\n}\n\n.rtl {\n .inventory-cell {\n text-align: left;\n }\n\n .inventory-cell .action-btn {\n left: calc(var(--touch-target-size) * -1 / 1.5);\n }\n}\n\n#inventory-levels {\n .inventory-cell {\n position: relative;\n }\n\n .inventory-cell .action-btn {\n opacity: 0;\n position: absolute;\n z-index: 99;\n }\n\n .inventory-cell .action-btn[aria-expanded='true'] {\n opacity: 1;\n }\n\n .vue-admin-table tbody tr:hover {\n .inventory-cell .action-btn {\n opacity: 1;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"css/inventory.css","mappings":"AACA,0BACE,uBAIA,wBACE,0DAGF,wBACE,8DAeF,qBACE,iBAGF,iCACE,4CAKF,qBACE,gBAGF,iCACE,2CAKF,kCACE,kBAGF,8CACE,UACA,kBACA,WAQA,gJACE","sources":["webpack:///./css/inventory.scss"],"sourcesContent":["// Inventory levels toolbar (contains the View button)\n.inventory-levels-toolbar {\n margin-bottom: var(--m);\n}\n\n.inventory-headers {\n .ltr & {\n justify-content: flex-end;\n }\n\n .rtl & {\n justify-content: flex-start;\n }\n}\n\n.inventory-cell {\n .ltr & {\n text-align: right;\n }\n\n .rtl & {\n text-align: left;\n }\n}\n\n.ltr {\n .inventory-cell {\n text-align: right;\n }\n\n .inventory-cell .action-btn {\n right: calc(var(--touch-target-size) * -1 / 1.5);\n }\n}\n\n.rtl {\n .inventory-cell {\n text-align: left;\n }\n\n .inventory-cell .action-btn {\n left: calc(var(--touch-target-size) * -1 / 1.5);\n }\n}\n\n#inventory-levels {\n .inventory-cell {\n position: relative;\n }\n\n .inventory-cell .action-btn {\n opacity: 0;\n position: absolute;\n z-index: 99;\n }\n\n .inventory-cell .action-btn[aria-expanded='true'] {\n opacity: 1;\n }\n\n .vue-admin-table tbody tr:hover {\n .inventory-cell .action-btn {\n opacity: 1;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/src/web/assets/inventory/dist/inventory.js b/src/web/assets/inventory/dist/inventory.js index 9726a2bb70..654d2b65bb 100644 --- a/src/web/assets/inventory/dist/inventory.js +++ b/src/web/assets/inventory/dist/inventory.js @@ -1,2 +1,2 @@ -!function(){var e={576:function(){function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}"undefined"===e(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.InventoryLevelsManager=Garnish.Base.extend({settings:null,containerId:null,$container:null,adminTableId:null,init:function(e,t){this.containerId=e,this.setSettings(t,Craft.Commerce.InventoryLevelsManager.defaults),this.$container=$(this.containerId),this.$container.data("inventoryLevelsManager")&&(console.warn("Double-instantiating an Inventory Levels Manager on an element."),this.$container.data("inventoryLevelsManager").destroy()),this.$container.data("inventoryLevelsManager",this),this.adminTableId="inventory-admin-table-"+Math.random().toString(36).substring(7),this.$adminTable=$('
').appendTo(this.$container),this.initAdminTable()},initAdminTable:function(){var e=this;this.columns=[{name:"purchasable",sortField:"item",title:Craft.t("commerce","Purchasable")},{name:"sku",sortField:"sku",title:Craft.t("commerce","SKU")},{name:"reserved",sortField:"reservedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Reserved")},{name:"damaged",sortField:"damagedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Damaged")},{name:"safety",sortField:"safetyTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Safety")},{name:"qualityControl",sortField:"qualityControlTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Quality Control")},{name:"committed",sortField:"committedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Committed")},{name:"available",sortField:"availableTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Available")},{name:"onHand",sortField:"onHandTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","On Hand")},{name:"incoming",sortField:"incomingTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Incoming")}],this.adminTable=new Craft.VueAdminTable({columns:this.columns,container:"#"+this.adminTableId,checkboxes:!1,allowMultipleSelections:!0,fullPane:!1,perPage:25,tableDataEndpoint:"commerce/inventory/inventory-levels-table-data",onQueryParams:function(t){return t.inventoryLocationId=e.settings.inventoryLocationId,e.settings.inventoryItemId&&(t.inventoryItemId=e.settings.inventoryItemId),t.containerId=e.containerId,t},search:!0,searchPlaceholder:Craft.t("commerce","Search inventory"),emptyMessage:Craft.t("commerce","No inventory found."),padded:!0})},defaultSettings:{inventoryLocationId:null,inventoryItemId:null}})},476:function(){function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}"undefined"===e(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.InventoryMovementModal=Craft.CpModal.extend({$quantityInput:null,$toInventoryMovementTypeInput:null,init:function(e){this.base("commerce/inventory/edit-movement-modal",e),this.debouncedRefresh=this.debounce(this.refresh,500),this.on("load",this.afterLoad.bind(this))},afterLoad:function(){var e=Craft.namespaceId("inventoryMovement-quantity",this.namespace);this.$quantityInput=this.$container.find("#"+e),this.addListener(this.$quantityInput,"keyup",this.debouncedRefresh);var t=Craft.namespaceId("inventoryMovement-toInventoryTransactionType",this.namespace);this.$toInventoryMovementTypeInput=this.$container.find("#"+t),this.addListener(this.$toInventoryMovementTypeInput,"change",this.refresh)},refresh:function(){var e=this,t=Garnish.getPostData(this.$container),n={data:Craft.expandPostArray(t),headers:{"X-Craft-Namespace":this.namespace}};Craft.sendActionRequest("POST",this.action,n).then((function(t){e.showLoadSpinner(),e.update(t.data).then((function(){e.$quantityInput.trigger("focus"),e.updateSizeAndPosition()})).finally((function(){e.hideLoadSpinner()}))}))},debounce:function(e,t){var n,a=this;return function(){for(var r=arguments.length,i=new Array(r),o=0;on.parts.length&&(a.parts.length=n.parts.length)}else{var o=[];for(r=0;r').appendTo(this.$container),this.initAdminTable()},getStorageKey:function(){return"Craft-"+Craft.siteUid+".Commerce.InventoryLevels.viewPreferences."+Craft.userId},loadViewPreferences:function(){var e=this.getStorageKey(),t=localStorage.getItem(e);if(t)try{this.viewPreferences=JSON.parse(t)}catch(e){this.viewPreferences=this.getDefaultViewPreferences()}else this.viewPreferences=this.getDefaultViewPreferences()},getDefaultViewPreferences:function(){var e={};return this.optionalColumns.forEach((function(t){e[t]=!0})),{visibleColumns:e}},saveViewPreferences:function(){var e=this.getStorageKey();localStorage.setItem(e,JSON.stringify(this.viewPreferences))},getAllColumns:function(){return[{name:"purchasable",sortField:"item",title:Craft.t("commerce","Purchasable")},{name:"sku",sortField:"sku",title:Craft.t("commerce","SKU")},{name:"reserved",sortField:"reservedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Reserved")},{name:"damaged",sortField:"damagedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Damaged")},{name:"safety",sortField:"safetyTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Safety")},{name:"qualityControl",sortField:"qualityControlTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Quality Control")},{name:"committed",sortField:"committedTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Committed")},{name:"available",sortField:"availableTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Available")},{name:"onHand",sortField:"onHandTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","On Hand")},{name:"incoming",sortField:"incomingTotal",titleClass:"inventory-headers",dataClass:"inventory-cell",title:Craft.t("commerce","Incoming")}]},getVisibleColumns:function(){var e=this;return this.getAllColumns().filter((function(t){return-1!==e.requiredColumns.indexOf(t.name)||!1!==e.viewPreferences.visibleColumns[t.name]}))},initAdminTable:function(){var e=this;this.columns=this.getVisibleColumns(),this.adminTable=new Craft.VueAdminTable({columns:this.columns,container:"#"+this.adminTableId,checkboxes:!1,allowMultipleSelections:!0,fullPane:!1,perPage:25,tableDataEndpoint:"commerce/inventory/inventory-levels-table-data",onQueryParams:function(t){return t.inventoryLocationId=e.settings.inventoryLocationId,e.settings.inventoryItemId&&(t.inventoryItemId=e.settings.inventoryItemId),t.containerId=e.containerId,t},search:!0,searchPlaceholder:Craft.t("commerce","Search inventory"),emptyMessage:Craft.t("commerce","No inventory found."),padded:!0})},initViewMenu:function(){var e=this,t=this.getAllColumns();this.viewMenuId="inventory-view-menu-"+Math.random().toString(36).substring(7),this.$viewToolbar=$("
",{class:"flex inventory-levels-toolbar"}).appendTo(this.$container),$('
').appendTo(this.$viewToolbar),this.$viewBtn=$("
').appendTo(\n this.$container\n );\n\n this.initAdminTable();\n },\n\n initAdminTable: function () {\n this.columns = [\n {\n name: 'purchasable',\n sortField: 'item',\n title: Craft.t('commerce', 'Purchasable'),\n },\n {name: 'sku', sortField: 'sku', title: Craft.t('commerce', 'SKU')},\n {\n name: 'reserved',\n sortField: 'reservedTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Reserved'),\n },\n {\n name: 'damaged',\n sortField: 'damagedTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Damaged'),\n },\n {\n name: 'safety',\n sortField: 'safetyTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Safety'),\n },\n {\n name: 'qualityControl',\n sortField: 'qualityControlTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Quality Control'),\n },\n {\n name: 'committed',\n sortField: 'committedTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Committed'),\n },\n {\n name: 'available',\n sortField: 'availableTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Available'),\n },\n {\n name: 'onHand',\n sortField: 'onHandTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'On Hand'),\n },\n {\n name: 'incoming',\n sortField: 'incomingTotal',\n titleClass: 'inventory-headers',\n dataClass: 'inventory-cell',\n title: Craft.t('commerce', 'Incoming'),\n },\n ];\n\n this.adminTable = new Craft.VueAdminTable({\n columns: this.columns,\n container: '#' + this.adminTableId,\n checkboxes: false,\n allowMultipleSelections: true,\n fullPane: false,\n perPage: 25,\n tableDataEndpoint: 'commerce/inventory/inventory-levels-table-data',\n onQueryParams: (params) => {\n // Arrow function to maintain 'this' context\n params.inventoryLocationId = this.settings.inventoryLocationId;\n if (this.settings.inventoryItemId) {\n params.inventoryItemId = this.settings.inventoryItemId;\n }\n params.containerId = this.containerId;\n return params;\n },\n search: true,\n searchPlaceholder: Craft.t('commerce', 'Search inventory'),\n emptyMessage: Craft.t('commerce', 'No inventory found.'),\n padded: true,\n });\n },\n\n defaultSettings: {\n inventoryLocationId: null,\n inventoryItemId: null,\n },\n});\n","/* jshint esversion: 6, strict: false */\n/* globals Craft, Garnish, $ */\nif (typeof Craft.Commerce === typeof undefined) {\n Craft.Commerce = {};\n}\n\nCraft.Commerce.InventoryMovementModal = Craft.CpModal.extend({\n $quantityInput: null,\n $toInventoryMovementTypeInput: null,\n\n init: function (settings) {\n this.base('commerce/inventory/edit-movement-modal', settings);\n\n this.debouncedRefresh = this.debounce(this.refresh, 500);\n\n // after load event is triggered on this\n this.on('load', this.afterLoad.bind(this));\n },\n afterLoad: function () {\n const quantityId = Craft.namespaceId(\n 'inventoryMovement-quantity',\n this.namespace\n );\n this.$quantityInput = this.$container.find('#' + quantityId);\n this.addListener(this.$quantityInput, 'keyup', this.debouncedRefresh);\n\n const toTransactionId = Craft.namespaceId(\n 'inventoryMovement-toInventoryTransactionType',\n this.namespace\n );\n this.$toInventoryMovementTypeInput = this.$container.find(\n '#' + toTransactionId\n );\n this.addListener(\n this.$toInventoryMovementTypeInput,\n 'change',\n this.refresh\n );\n },\n refresh: function () {\n let postData = Garnish.getPostData(this.$container);\n let expandedData = Craft.expandPostArray(postData);\n\n let data = {\n data: expandedData,\n headers: {\n 'X-Craft-Namespace': this.namespace,\n },\n };\n\n Craft.sendActionRequest('POST', this.action, data).then((response) => {\n this.showLoadSpinner();\n this.update(response.data)\n .then(() => {\n // focus on the quantity input\n this.$quantityInput.trigger('focus');\n this.updateSizeAndPosition();\n })\n .finally(() => {\n this.hideLoadSpinner();\n });\n });\n },\n debounce: function (func, delay) {\n let timer;\n return (...args) => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n func.apply(this, args);\n }, delay);\n };\n },\n});\n","// style-loader: Adds some css to the DOM by adding a