partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit bd7cc506579a68f1a23819440d84e04ba723f0f0
parent 63c1961db9dea20e046feeb5759ac7b0c2cc175c
Author: Timo A. Hummel <timo@netraver.de>
Date:   Tue, 14 Jun 2011 02:57:20 +0200

- Added a new part filter panel for the part grid
- Minor cosmetic changes
- Added some documentation

Diffstat:
Mfrontend/index.php | 1+
Mfrontend/js/Components/Editor/EditorGrid.js | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Afrontend/js/Components/Part/PartFilterPanel.js | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/js/Components/Part/PartsGrid.js | 286++++++++++++++++++++++---------------------------------------------------------
Mfrontend/js/Components/Part/PartsManager.js | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/de/RaumZeitLabor/PartDB2/Part/PartManager.php | 7++++++-
Msrc/de/RaumZeitLabor/PartDB2/Part/PartService.php | 3++-
7 files changed, 360 insertions(+), 218 deletions(-)

diff --git a/frontend/index.php b/frontend/index.php @@ -125,6 +125,7 @@ <script type="text/javascript" src="js/Components/Part/PartManufacturerGrid.js"></script> <script type="text/javascript" src="js/Components/Part/PartEditorWindow.js"></script> <script type="text/javascript" src="js/Components/Part/PartEditor.js"></script> + <script type="text/javascript" src="js/Components/Part/PartFilterPanel.js"></script> <script type="text/javascript" src="js/Components/Part/PartDisplay.js"></script> <script type="text/javascript" src="js/Components/Part/PartStockWindow.js"></script> <script type="text/javascript" src="js/Components/Part/PartStockHistory.js"></script> diff --git a/frontend/js/Components/Editor/EditorGrid.js b/frontend/js/Components/Editor/EditorGrid.js @@ -1,12 +1,60 @@ +/** + * This class extends a regular GridPanel with the following features: + * + * - Buttons to add/delete items + * - Enable/Disable the delete button if an item is selected + * - Search field + * - Paging Toolbar + */ Ext.define('PartDB2.EditorGrid', { extend: 'Ext.grid.Panel', alias: 'widget.EditorGrid', + + /** + * @cfg {String} text The text for the "delete" button + */ deleteButtonText: i18n("Delete Item"), + + /** + * @cfg {String} text The text for the "add" button + */ addButtonText: i18n("Add Item"), + + /** + * @cfg {String} text Defines if the "add"/"delete" buttons should show their text or icon only. If "hide", the + * button text is hidden, anything else shows the text. + */ buttonTextMode: 'hide', + initComponent: function () { - this.addEvents("itemSelect", "itemDelete", "itemAdd"); + this.addEvents( + /** + * @event itemSelect + * Fires if a record was selected within the grid. + * @param {Object} record The selected record + */ + "itemSelect", + + /** + * @event itemDeselect + * Fires if a record was deselected within the grid. + * @param {Object} record The deselected record + */ + "itemDeselect", + + /** + * @event itemDelete + * Fires if the delete button was clicked. + */ + "itemDelete", + + /** + * @event itemDelete + * Fires if the add button was clicked. + */ + "itemAdd"); + this.getSelectionModel().on("select", Ext.bind(function (rsm, r, i) { @@ -54,6 +102,7 @@ Ext.define('PartDB2.EditorGrid', { this.bottomToolbar = Ext.create("Ext.toolbar.Paging", { store: this.store, + enableOverflow: true, dock: 'bottom', displayInfo: false }); diff --git a/frontend/js/Components/Part/PartFilterPanel.js b/frontend/js/Components/Part/PartFilterPanel.js @@ -0,0 +1,158 @@ +Ext.define('PartDB2.PartFilterPanel', { + extend: 'Ext.form.Panel', + alias: 'widget.PartFilterPanel', + bodyPadding: '10px', + layout: 'column', + initComponent: function () { + + // Create the filter fields + this.createFilterFields(); + + + // Creates the left column of the filter panel + this.leftColumn = { + xtype: 'container', + anchor: '100%', + layout: 'anchor', + columnWidth: .5, + items: [ + this.storageLocationFilter, + this.categoryFilter, + this.partsWithoutPrice + ] + }; + + // Creates the right column of the filter panel + this.rightColumn = { + xtype: 'container', + anchor: '100%', + columnWidth: .5, + layout: 'anchor', + items: [ + this.stockFilter + ] + }; + + // Apply both columns to this panel + this.items = [ this.leftColumn, this.rightColumn ]; + + // Create the reset button + this.resetButton = Ext.create("Ext.button.Button", { + text: i18n("Reset"), + handler: this.onReset, + scope: this + }); + + // Create the apply button + this.applyButton = Ext.create("Ext.button.Button", { + text: i18n("Apply"), + handler: this.onApply, + scope: this + }); + + // Append both buttons to a toolbar + this.dockedItems = [{ + xtype: 'toolbar', + enableOverflow: true, + dock: 'bottom', + ui: 'footer', + items: [ this.applyButton, this.resetButton ] + }]; + + this.callParent(); + }, + /** + * Applies the parameters from the filter panel to the proxy, then + * reload the store to refresh the grid. + */ + onApply: function () { + this.applyFilterParameters(this.store.getProxy().extraParams); + this.store.getProxy().extraParams.start = 0; + this.store.currentPage = 1; + this.store.load({ start: 0}); + }, + /** + * Resets the fields to their original values, then call onApply() + * to reload the store. + */ + onReset: function () { + this.storageLocationFilter.setValue(""); + this.categoryFilter.setValue({ category: 'all'}); + this.stockFilter.setValue({ stock: 'any'}); + + this.onApply(); + }, + /** + * Creates the filter fields required for this filter panel + */ + createFilterFields: function () { + + // Create the storage location filter field + this.storageLocationFilter = Ext.create("PartDB2.StorageLocationComboBox", { + fieldLabel: i18n("Storage Location"), + forceSelection: true + }); + + // Create the category scope field + this.categoryFilter = Ext.create("Ext.form.RadioGroup", { + fieldLabel: i18n("Category Scope"), + columns: 1, + items: [{ + boxLabel: i18n("All Subcategories"), + name: 'category', + inputValue: "all", + checked: true + }, + { + boxLabel: i18n("Selected Category"), + name: 'category', + inputValue: "selected" + }] + }); + + // Create the stock level filter field + this.stockFilter = Ext.create("Ext.form.RadioGroup", { + fieldLabel: i18n("Stock Mode"), + columns: 1, + items: [{ + boxLabel: i18n("Any Stock Level"), + name: 'stock', + inputValue: "any", + checked: true + },{ + boxLabel: i18n("Stock Level = 0"), + name: 'stock', + inputValue: "zero" + },{ + boxLabel: i18n("Stock Level > 0"), + name: 'stock', + inputValue: "nonzero" + },{ + boxLabel: i18n("Stock Level < Minimum Stock Level"), + name: 'stock', + inputValue: "below" + }] + }); + + this.partsWithoutPrice = Ext.create("Ext.form.field.Checkbox", { + fieldLabel: i18n("Item Price"), + boxLabel: i18n("Show Parts without Price only") + }); + }, + /** + * Applies the filter parameters to the passed extraParams object. + * @param extraParams An object containing the extraParams from a proxy. + */ + applyFilterParameters: function (extraParams) { + extraParams.withoutPrice = this.partsWithoutPrice.getValue(); + extraParams.categoryScope = this.categoryFilter.getValue().category; + extraParams.stockMode = this.stockFilter.getValue().stock; + + /** + * Get the raw (=text) value. I really wish that ExtJS would handle selected values (from a store) + * distinct than entered values. + */ + extraParams.storageLocation = this.storageLocationFilter.getRawValue(); + } + +});+ \ No newline at end of file diff --git a/frontend/js/Components/Part/PartsGrid.js b/frontend/js/Components/Part/PartsGrid.js @@ -1,232 +1,104 @@ +/** + * This class is the main part list grid. + * + */ Ext.define('PartDB2.PartsGrid', { extend: 'PartDB2.EditorGrid', alias: 'widget.PartsGrid', - columns: [ - {header: i18n("Name"), dataIndex: 'name', flex: 1, renderer: Ext.util.Format.htmlEncode}, - {header: i18n("Storage Location"), dataIndex: 'storageLocationName'}, - { - header: i18n("Stock"), - dataIndex: 'stockLevel', - renderer: function (val,q,rec) - { - if (rec.get("partUnitDefault") !== true) { - return rec.get("stockLevel") + " " + rec.get("partUnit"); - } else { - return val; - } - } - }, - {header: i18n("Min. Stock"), dataIndex: 'minStockLevel', xtype:'templatecolumn', tpl:'{minStockLevel} {partUnit}'}, - {header: i18n("Avg. Price"), dataIndex: 'averagePrice' }, - {header: i18n("Footprint"), dataIndex: 'footprintName'} - ], - buttonTextMode: 'show', - - categoryScope: 'all', - - scopeAllText: i18n("All subcategories"), - scopeSelectedText: i18n('Selected category'), - - - categoryScopeAllIcon: "resources/silkicons/folder_magnify.png", - categoryScopeSelectedIcon: "resources/silkicons/folder_go.png", - - stockMode: "all", - stockModeAll: i18n("Any stock level"), - stockModeZero: i18n("Stock = 0"), - stockModeBelow: i18n("Stock < Min. Stock"), - stockModeAvailable: i18n("Stock > 0"), - stockModeBelowIcon: 'resources/icons/stock_below.png', - stockModeAllIcon: 'resources/icons/stock_any.png', - stockModeZeroIcon: 'resources/icons/stock_zero.png', - stockModeNonzeroIcon: 'resources/icons/stock_nonzero.png', - - partsWithoutPriceText: i18n("Parts without price"), - - stockModes: [ "all", "nonzero", "zero", "below" ], + // We want to display the texts for the add/delete buttons + buttonTextMode: 'show', initComponent: function () { - this.callParent(); - this.categoryScopeButton = Ext.create("Ext.button.Split", { - text: this.scopeAllText, - icon: this.categoryScopeAllIcon, - handler: this.categoryModeButtonHandler, - scope: this, - width: 140, - menu: { - items: [ - { - text: this.scopeAllText, - handler: this.allSubcategoriesHandler, - icon: this.categoryScopeAllIcon, - scope: this - }, - { - text: this.scopeSelectedText, - handler: this.onlySelectedCategoryHandler, - icon: this.categoryScopeSelectedIcon, - scope: this - }] - } - }); + // Create the columns + this.defineColumns(); - this.stockModeButton = Ext.create("Ext.button.Split", { - text: this.stockModeAll, - width: 140, - handler: this.stockModeButtonHandler, - icon: this.stockModeAllIcon, - scope: this, - menu: { - items: [ - { - text: this.stockModeAll, - handler: this.stockModeAllHandler, - icon: this.stockModeAllIcon, - scope: this - }, - { - text: this.stockModeZero, - handler: this.stockModeZeroHandler, - icon: this.stockModeZeroIcon, - scope: this - }, - { - text: this.stockModeBelow, - handler: this.stockModeBelowHandler, - icon: this.stockModeBelowIcon, - scope: this - }, - { - text: this.stockModeAvailable, - handler: this.stockModeAvailableHandler, - icon: this.stockModeNonzeroIcon, - scope: this - } - ] - } + // Initialize the panel + this.callParent(); + + // Create the filter panel + this.filterPanel = Ext.create("PartDB2.PartFilterPanel", { + dock: 'bottom', + title: i18n("Filter"), + height: 200, + store: this.store }); - this.priceModeButton = Ext.create("Ext.button.Button", { + this.filterButton = Ext.create("Ext.button.Button", { enableToggle: true, - text: this.partsWithoutPriceText, - handler: this.priceModeHandler, + text: i18n("Filter"), + handler: this.onFilterClick, scope: this }); - this.bottomToolbar.add('-'); - this.bottomToolbar.add(this.categoryScopeButton); - this.bottomToolbar.add(this.stockModeButton); - this.bottomToolbar.add(this.priceModeButton); - this.setScopeMode("all"); + // Add the filter button + this.bottomToolbar.add([ '-', this.filterButton ]); }, - priceModeHandler: function () { - var proxy = this.store.getProxy(); - proxy.extraParams.withoutPrice = this.priceModeButton.pressed; - - this.store.load(); + /** + * Defines the columns used in this grid. + */ + defineColumns: function () { + this.columns = [ + { + header: i18n("Name"), + dataIndex: 'name', + flex: 1, + minWidth: 200, + renderer: Ext.util.Format.htmlEncode + },{ + header: i18n("Storage Location"), + dataIndex: 'storageLocationName' + },{ + header: i18n("Stock"), + dataIndex: 'stockLevel', + renderer: this.stockLevelRenderer + },{ + header: i18n("Min. Stock"), + dataIndex: 'minStockLevel', + renderer: this.stockLevelRenderer + },{ + header: i18n("Avg. Price"), + dataIndex: 'averagePrice' + },{ + header: i18n("Footprint"), + dataIndex: 'footprintName' + } + ]; }, - categoryModeButtonHandler: function () { - if (this.categoryScope == "all") { - this.setScopeMode("selected"); + /** + * Used as renderer for the stock level columns. + * + * If a part contains a non-default unit, we display it. + * Otherwise we hide it. + */ + stockLevelRenderer: function (val,q,rec) + { + if (rec.get("partUnitDefault") !== true) { + return val + " " + rec.get("partUnit"); } else { - this.setScopeMode("all"); + return val; } - this.store.load(); - }, - allSubcategoriesHandler: function () { - this.setScopeMode("all"); - this.store.load(); - }, - onlySelectedCategoryHandler: function () { - this.setScopeMode("selected"); - this.store.load(); }, - setScopeMode: function (mode) { - switch (mode) { - case "all": - case "selected": - this.categoryScope = mode; - break; - default: - alert("Invalid mode in setScopeMode!"); - break; - } - - /* Update button text */ - if (this.categoryScope == "all") { - this.categoryScopeButton.setText(this.scopeAllText); - this.categoryScopeButton.setIcon(this.categoryScopeAllIcon); + /** + * Shows or hides the filter panel. + * + * Unfortunately, we can't simply use show() or hide() on filterPanel, so we add and remove the panel. + */ + onFilterClick: function () { + if (this.filterButton.pressed) { + if (!this.getDockedComponent(this.filterPanel)) { + this.addDocked(this.filterPanel); + } } else { - this.categoryScopeButton.setText(this.scopeSelectedText); - this.categoryScopeButton.setIcon(this.categoryScopeSelectedIcon); - } - - var proxy = this.store.getProxy(); - proxy.extraParams.categoryScope = this.categoryScope; - }, - stockModeAllHandler: function () { - this.setStockMode("all"); - this.store.load(); - }, - stockModeZeroHandler: function () { - this.setStockMode("zero"); - this.store.load(); - }, - stockModeBelowHandler: function () { - this.setStockMode("below"); - this.store.load(); - }, - stockModeAvailableHandler: function () { - this.setStockMode("nonzero"); - this.store.load(); - }, - stockModeButtonHandler: function () { - var idx = Ext.Array.indexOf(this.stockModes, this.stockMode); - - idx++; - - if (idx > (this.stockModes.length - 1)) { - idx = idx - this.stockModes.length; - } - - this.setStockMode(this.stockModes[idx]); - this.store.load(); - - }, - setStockMode: function (mode) { - if (!Ext.Array.contains(this.stockModes, mode)) { - window.alert("Invalid stock mode "+mode+" for setStockMode!"); - return; - } - - this.stockMode = mode; - - switch (this.stockMode) { - case "zero": - this.stockModeButton.setText(this.stockModeZero); - this.stockModeButton.setIcon(this.stockModeZeroIcon); - break; - case "nonzero": - this.stockModeButton.setText(this.stockModeAvailable); - this.stockModeButton.setIcon(this.stockModeNonzeroIcon); - break; - case "below": - this.stockModeButton.setText(this.stockModeBelow); - this.stockModeButton.setIcon(this.stockModeBelowIcon); - break; - case "all": - this.stockModeButton.setText(this.stockModeAll); - this.stockModeButton.setIcon(this.stockModeAllIcon); - break; - default: - break; + if (this.getDockedComponent(this.filterPanel)) { + this.removeDocked(this.filterPanel, false); + } } - - var proxy = this.store.getProxy(); - proxy.extraParams.stockMode = this.stockMode; }, + /** + * Sets the category. Triggers a store reload with a category filter. + */ setCategory: function (category) { this.currentCategory = category; diff --git a/frontend/js/Components/Part/PartsManager.js b/frontend/js/Components/Part/PartsManager.js @@ -1,38 +1,57 @@ /** * @class PartDB2.PartManager * @todo Document the editor system a bit better + * + * The part manager encapsulates the category tree, the part display grid and the part detail view. */ Ext.define('PartDB2.PartManager', { extend: 'Ext.panel.Panel', alias: 'widget.PartManager', layout: 'border', initComponent: function () { - this.createStore({ + + /** + * Create the store with the default sorter "name ASC" + */ + this.createStore({ model: 'Part', sorters: [{ - property: 'name', - direction:'ASC' - }] + property: 'name', + direction:'ASC' + }] }); - - this.tree = Ext.create("PartDB2.CategoryEditorTree", { region: 'west', split: true, width: 300}); + // Create the tree + this.tree = Ext.create("PartDB2.CategoryEditorTree", { + region: 'west', + split: true, + width: 300, // @todo Make this configurable + collapsible: true // We want to collapse the tree panel on small screens + }); + + // Trigger a grid reload on category change this.tree.on("selectionchange", Ext.bind(function (t,s) { if (s.length > 0) { this.grid.setCategory(s[0].get("id")); } }, this)); + // Create the detail panel this.detail = Ext.create("PartDB2.PartDisplay", { title: i18n("Part Details") }); this.detail.on("editPart", this.onEditPart, this); + + // Create the grid this.grid = Ext.create("PartDB2.PartsGrid", { region: 'center', layout: 'fit', store: this.getStore()}); + // Create the grid listeners this.grid.on("itemSelect", this.onItemSelect, this); this.grid.on("itemAdd", this.onItemAdd, this); this.grid.on("itemDelete", this.onItemDelete, this); + // Listen on the partChanged event, which is fired when the users edits the part this.detail.on("partChanged", function () { this.grid.getStore().load(); }, this); + // Create the stock level panel this.stockLevel = Ext.create("PartDB2.PartStockHistory", { title: "Stock History"}); this.detailPanel = Ext.create("Ext.tab.Panel", { @@ -42,15 +61,29 @@ Ext.define('PartDB2.PartManager', { width: 300, items: [ this.detail, this.stockLevel ] }); - this.items = [ this.tree, this.grid, this.detailPanel /*this.editor*/ ]; + + this.items = [ this.tree, this.grid, this.detailPanel ]; this.callParent(); }, + + /** + * Called when the delete button was clicked. + * + * Prompts the user if he really wishes to delete the part. If yes, it calls deletePart. + */ onItemDelete: function () { var r = this.grid.getSelectionModel().getLastSelected(); Ext.Msg.confirm(i18n("Delete Part"), sprintf(i18n("Do you really wish to delete the part %s?"),r.get("name")), this.deletePart, this); }, + /** + * Deletes the selected part. + * + * @param {String} btn The clicked button in the message box window. + * @todo We use the current selection of the grid. If for some reason the selection changes during the user is prompted, + * we delete the wrong part. Fix that to pass the selected item to the onItemDelete then to this function. + */ deletePart: function (btn) { var r = this.grid.getSelectionModel().getLastSelected(); @@ -67,6 +100,9 @@ Ext.define('PartDB2.PartManager', { call.doCall(); } }, + /** + * Creates a new, empty part editor window + */ onItemAdd: function () { var j = Ext.create("PartDB2.PartEditorWindow"); @@ -80,29 +116,45 @@ Ext.define('PartDB2.PartManager', { j.applyRecord(defaults); j.show(); }, + /** + * Called when a part was edited. Refreshes the grid. + */ onEditPart: function (id) { this.loadPart(id, Ext.bind(this.onPartLoaded, this)); }, + /** + * Called when a part was loaded. Displays the part in the editor window. + */ onPartLoaded: function (f,g) { var j = Ext.create("PartDB2.PartEditorWindow"); j.applyRecord(f); j.show(); }, + /** + * Called when a part was selected in the grid. Displays the details for this part. + */ onItemSelect: function (r) { this.detailPanel.setActiveTab(this.detail); this.detailPanel.show(); this.detail.setValues(r); this.stockLevel.part = r.get("id"); }, + /** + * Triggers loading of a part + * @param {Integer} id The ID of the part to load + * @param {Function} handler The callback to call when the part was loaded + */ loadPart: function (id, handler) { var call = new PartDB2.ServiceCall( "Part", "getPart"); call.setParameter("part", id); - call.setLoadMessage('$[de.RaumZeitLabor.PartDB2.CategoryEditor.loadCategories]'); call.setHandler(handler); call.doCall(); }, + /** + * Creates the store + */ createStore: function (config) { Ext.Object.merge(config, { autoLoad: true, @@ -125,6 +177,9 @@ Ext.define('PartDB2.PartManager', { } }); }, + /** + * Returns the store + */ getStore: function () { return this.store; } diff --git a/src/de/RaumZeitLabor/PartDB2/Part/PartManager.php b/src/de/RaumZeitLabor/PartDB2/Part/PartManager.php @@ -30,7 +30,7 @@ use de\RaumZeitLabor\PartDB2\Util\Singleton, de\RaumZeitLabor\PartDB2\Footprint\Exceptions\FootprintNotFoundException; class PartManager extends Singleton { - public function getParts ($start = 0, $limit = 10, $sort = "name", $dir = "asc", $filter = "", $category = 0, $categoryScope = "all", $stockMode = "all", $withoutPrice = false) { + public function getParts ($start = 0, $limit = 10, $sort = "name", $dir = "asc", $filter = "", $category = 0, $categoryScope = "all", $stockMode = "all", $withoutPrice = false, $storageLocation = "") { $qb = PartDB2::getEM()->createQueryBuilder(); $qb->select("COUNT(p.id)")->from("de\RaumZeitLabor\PartDB2\Part\Part","p") @@ -44,6 +44,11 @@ class PartManager extends Singleton { $qb = $qb->where("p.name LIKE :filter"); $qb->setParameter("filter", "%".$filter."%"); } + + if ($storageLocation != "") { + $qb->andWhere("st.name = :storageLocation"); + $qb->setParameter("storageLocation", $storageLocation); + } switch ($sort) { case "storageLocationName": diff --git a/src/de/RaumZeitLabor/PartDB2/Part/PartService.php b/src/de/RaumZeitLabor/PartDB2/Part/PartService.php @@ -33,7 +33,8 @@ class PartService extends Service implements RestfulService { $this->getParameter("category", 0), $this->getParameter("categoryScope", "all"), $this->getParameter("stockMode", "all"), - $this->getParameter("withoutPrice", false)); + $this->getParameter("withoutPrice", false), + $this->getParameter("storageLocation", "")); } }