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:
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", ""));
}
}