partkeepr

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

commit 9337e8319580ab2adb1fb166b7ab162d2b26ceab
parent da771320b9f8a2383e220c03f241b94731b58646
Author: Felicitus <felicitus@felicitus.org>
Date:   Thu, 22 Dec 2011 12:57:33 +0100

Added initial project support

Diffstat:
Asrc/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Manager/Exceptions/EntityInUseException.php | 33+++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Project/Project.php | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectManager.php | 35+++++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectPart.php | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectService.php | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/frontend/css/PartKeepr.css | 4++++
Msrc/frontend/js/Components/MenuBar.js | 19+++++++++++++++++--
Asrc/frontend/js/Components/Project/ProjectEditor.js | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/frontend/js/Components/Project/ProjectEditorComponent.js | 22++++++++++++++++++++++
Asrc/frontend/js/Components/Project/ProjectGrid.js | 15+++++++++++++++
Asrc/frontend/js/Components/Project/ProjectPartGrid.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/frontend/js/Components/Widgets/RemotePartComboBox.js | 25+++++++++++++++++++++++++
Asrc/frontend/js/Models/Project.js | 20++++++++++++++++++++
Asrc/frontend/js/Models/ProjectPart.js | 23+++++++++++++++++++++++
16 files changed, 1091 insertions(+), 2 deletions(-)

diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php @@ -0,0 +1,136 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Manager; +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Util\Singleton, + de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Manager\Exceptions\EntityInUseException; + +abstract class AbstractManager extends Singleton { + /** + * Returns the FQCN for the target entity to operate on. + * + * Needs to be implemented by the parent class. + * + * @return string The FQCN, e.g. de\RaumZeitLabor\PartKeepr\Part + */ + abstract public function getEntityName (); + + /** + * Returns all fields which need to appear in the getList ResultSet. + * + * This should include all fields required by the frontend. + * + * @return array An array of all fields which should be returned + */ + abstract public function getQueryFields (); + + /** + * Returns the default sort field + * + * @return string The default sort field + */ + abstract public function getDefaultSortField (); + + /** + * Loads an entity by id + * + * @param int $id The entity id + */ + public function getEntity ($id) { + return call_user_func($this->getEntityName()."::loadById", $id); + } + + /** + * Removes a specific entity from the system + * @param int $id + * @throws EntityInUseException Is thrown when the entity is in use + */ + public function deleteEntity ($id) { + $entity = $this->getEntity($id); + + try { + PartKeepr::getEM()->remove($entity); + PartKeepr::getEM()->flush(); + } catch (\PDOException $e) { + if ($e->getCode() == "23000") { + throw new EntityInUseException($entity); + } + } + } + + /** + * Creates a new entity from an array of parameters + * @param array $parameters An array which gets passed to the deserialize method + */ + public function createEntity ($parameters) { + $class = $this->getEntityName(); + $entity = new $class; + $entity->deserialize($parameters); + + PartKeepr::getEM()->persist($entity); + PartKeepr::getEM()->flush(); + + return $entity; + } + + /** + * Returns a list of entities. + * + * @param ManagerFilter $filter The filter settings for this query + */ + public function getList (ManagerFilter $filter) { + $qb = PartKeepr::getEM()->createQueryBuilder(); + + $qb->select("COUNT(q.id)"); + + //$qb->where("1"); + + if ($filter->getFilter() !== null && $filter->getFilterField() !== null) { + $aOrWhereFields = array(); + + if (is_array($filter->getFilterField())) { + foreach ($filter->getFilterField() as $field) { + $aOrWhereFields[] = "q.".$field." = :filter"; + } + } else { + $aOrWhereFields[] = "q.".$filter->getFilterField()." = :filter"; + } + + $qb->orWhere($aOrWhereFields); + $qb->setParameter("filter", "%".$filter->getFilter()."%"); + } + + $qb->from($this->getEntityName(),"q"); + + $totalQuery = $qb->getQuery(); + + // Prepend a prefix to each field + $aQueryFields = array(); + foreach ($this->getQueryFields() as $field) { + $aQueryFields[] = "q.".$field; + } + + $qb->select($aQueryFields); + + + if ($filter->getStart() !== null) { + $qb->setFirstResult($filter->getStart()); + } + + if ($filter->getLimit() !== null) { + $qb->setMaxResults($filter->getLimit()); + } + + if ($filter->getSortField() !== null) { + $qb->orderBy("q.".$filter->getSortField(), $filter->getDir()); + } + + $query = $qb->getQuery(); + + $result = $query->getResult(); + + return array("data" => $result, "totalCount" => $totalQuery->getSingleScalarResult()); + + } +} + \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Manager/Exceptions/EntityInUseException.php b/src/backend/de/RaumZeitLabor/PartKeepr/Manager/Exceptions/EntityInUseException.php @@ -0,0 +1,32 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Manager\Exceptions; +declare(encoding = 'UTF-8'); + +/** + * This exception is thrown when an entity should be deleted, but is in use by other entities. + * @author felicitus + */ +class EntityInUseException extends \Exception { + + /** + * The entity + * @var BaseEntity + */ + private $entity; + + /** + * Constructs the exception + * @param BaseEntity $entity + */ + public function __construct (\de\RaumZeitLabor\PartKeepr\Util\BaseEntity $entity) { + parent::__construct(sprintf("Entity %s is referenced by other entities, can't delete", get_class($entity))); + $this->entity = $entity; + } + + /** + * Returns the entity which caused the error + */ + public function getEntity () { + return $this->entity; + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php b/src/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php @@ -0,0 +1,213 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Manager; +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Service\Service; + +class ManagerFilter { + /** + * Specifies the record index at which to start + * @var integer + */ + protected $start = 0; + + /** + * Specifies the number of records to retrieve + * @var integer + */ + protected $limit = null; + + /** + * Specifies a string to filter for. Can either be a string + * or null if no filter is wanted + * @var string + */ + protected $filter = null; + + /** + * Specifies the field (or a list of fields) to apply the filter on + * @var mixed null if disabled, a string for a single field or an array of fields + */ + protected $filterField = null; + + /** + * Specifies the field to sort by + * @var string + */ + protected $sortField = null; + + /** + * Specifies the direction (either ASC or DESC) + * @var string + */ + protected $direction = "asc"; + + /** + * Sets the start position + * @param int $start + */ + public function setStart ($start) { + $this->start = intval($start); + } + + /** + * Returns the start position + * @return int + */ + public function getStart () { + if ($this->start === null) { + return 0; + } else { + return $this->start; + } + } + + /** + * Sets the number of records to retrieve + * @param mixed $limit Either a positive integer, or null/-1 for no limit + */ + public function setLimit ($limit) { + if ($limit === null || $limit === -1) { + $this->limit = null; + } else { + $this->limit = intval($limit); + } + } + + /** + * Returns the number of records to retrieve + * @return int + */ + public function getLimit () { + return $this->limit; + } + + /** + * Sets the filter. Specify null if no filter is wanted. + * @param mixed $filter A string to filter for, or null to disable + */ + public function setFilter ($filter) { + $this->filter = $filter; + } + + /** + * Returns the filter. + * @return mixed Either a string to filter for, or null if disabled + */ + public function getFilter () { + return $this->filter; + } + + /** + * Sets the direction to order by + * @param string $direction Either "asc" or "desc". + */ + public function setDirection ($direction) { + switch (strtolower($direction)) { + case "desc": + $this->direction = "desc"; + break; + case "asc": + default: + $this->direction = "asc"; + break; + } + } + + /** + * Sets the direction to order by. Shorthand function for setDirection(). + * @param string $dir The direction, either "asc" or "desc" + */ + public function setDir ($dir) { + $this->setDirection($dir); + } + + /** + * Returns the direction to order by. + * @return either "asc" or "desc". + */ + public function getDirection () { + return $this->direction; + } + + /** + * Shorthand function for getDirection(). + * @see getDirection + */ + public function getDir () { + return $this->getDirection(); + } + + /** + * Sets the field to sort by + * @param string $sortField The sort field + */ + public function setSortField ($sortField) { + $this->sortField = $sortField; + } + + /** + * Returns the sort field + * @return string the field to sort by + */ + public function getSortField () { + return $this->sortField; + } + + /** + * Sets the field(s) to filter for. + * + * If multiple fields are specified, they will be combined using an "OR" clause. + * + * @param mixed $field Either null to disable, a single string to specify a field, or an array of string fields + */ + public function setFilterField ($field) { + $this->filterField = $field; + } + + /** + * Returns the field(s) to filter for + * @return mixed See setFilterField + */ + public function getFilterField () { + return $this->filterField; + } + + /** + * Constructs a new filter set. + * + * If a service is passed, the constructor automatically extracts the parameters from the service + * + * @todo Document which parameters we have + * + * @param Service $service A service to extract the information from, or null + */ + public function __construct (Service $service = null) { + if (is_object($service)) { + if ($service->hasParameter("start")) { + $this->setStart($service->getParameter("start", null)); + } + + if ($service->hasParameter("limit")) { + $this->setLimit($service->getParameter("limit", null)); + } + + if ($service->hasParameter("sort")) { + $tmp = json_decode($service->getParameter("sort"), true); + + $aSortParams = $tmp[0]; + } else { + $aSortParams = array( + "property" => null, + "direction" => "ASC"); + } + + $this->setSortField($aSortParams["property"]); + $this->setDirection($aSortParams["direction"]); + + if ($service->hasParameter("query")) { + $this->setFilter($service->getParameter("query")); + } + } + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Project/Project.php b/src/backend/de/RaumZeitLabor/PartKeepr/Project/Project.php @@ -0,0 +1,137 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Project; + +use de\RaumZeitLabor\PartKeepr\User\User, + de\RaumZeitLabor\PartKeepr\Util\Serializable, + de\RaumZeitLabor\PartKeepr\Util\Deserializable, + de\RaumZeitLabor\PartKeepr\Util\BaseEntity; + +/** + * Represents a part in the database. The heart of our project. Handle with care! + * @Entity **/ +class Project extends BaseEntity implements Serializable, Deserializable { + /** + * Specifies the name of the project + * @Column(type="string") + */ + private $name; + + /** + * Specifies the user this project belongs to + * @ManyToOne(targetEntity="de\RaumZeitLabor\PartKeepr\User\User") + */ + private $user; + + /** + * Holds the parts needed for this project + * @OneToMany(targetEntity="de\RaumZeitLabor\PartKeepr\Project\ProjectPart",mappedBy="project",cascade={"persist", "remove"}) + * @var ArrayCollection + */ + private $parts; + + /** + * Holds the description of this project + * @Column(type="string",nullable=true) + * @var string + */ + private $description; + + /** + * Constructs a new project + */ + public function __construct () { + $this->parts = new \Doctrine\Common\Collections\ArrayCollection(); + } + + /** + * Sets the user for this project + * @param User $user + */ + public function setUser (User $user) { + $this->user = $user; + } + + /** + * Gets the user for this project + * @return User + */ + public function getUser () { + return $this->user; + } + + /** + * Sets the name for this project + * @param string $name + */ + public function setName ($name) { + $this->name = $name; + } + + /** + * Returns the name of this project + */ + public function getName () { + return $this->name; + } + + /** + * Sets the description of this project + * @param string $description The description to set + */ + public function setDescription ($description) { + $this->description = $description; + } + + /** + * Returns the description of this project + * @return string The description + */ + public function getDescription () { + return $this->description; + } + + /** + * Returns the parts array + * @return ArrayCollection An array of ProjectPart objects + */ + public function getParts () { + return $this->parts; + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Util.Serializable::serialize() + */ + public function serialize () { + return array( + "id" => $this->getId(), + "name" => $this->getName(), + "description" => $this->getDescription(), + "parts" => $this->serializeChildren($this->getParts()), + ); + } + + /** + * Deserializes the project + * @param array $parameters The array with the parameters to set + */ + public function deserialize (array $parameters) { + foreach ($parameters as $key => $value) { + switch ($key) { + case "name": + $this->setName($value); + break; + case "description": + $this->setDescription($value); + break; + case "parts": + $this->deserializeChildren($value, $this->getParts(), "de\RaumZeitLabor\PartKeepr\Project\ProjectPart"); + foreach ($this->getParts() as $part) { + $part->setProject($this); + } + break; + } + } + } + +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectManager.php @@ -0,0 +1,34 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Project; +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Manager\AbstractManager, + de\RaumZeitLabor\PartKeepr\Project\Project, + de\RaumZeitLabor\PartKeepr\PartKeepr; + +class ProjectManager extends AbstractManager { + /** + * Returns the FQCN for the target entity to operate on. + * @return string The FQCN, e.g. de\RaumZeitLabor\PartKeepr\Part + */ + public function getEntityName () { + return 'de\RaumZeitLabor\PartKeepr\Project\Project'; + } + + /** + * Returns all fields which need to appear in the getList ResultSet. + * @return array An array of all fields which should be returned + */ + public function getQueryFields () { + return array("id", "name", "description"); + } + + /** + * Returns the default sort field + * + * @return string The default sort field + */ + public function getDefaultSortField () { + return "name"; + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectPart.php b/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectPart.php @@ -0,0 +1,136 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Project; + +use de\RaumZeitLabor\PartKeepr\Part\Part, + de\RaumZeitLabor\PartKeepr\Util\Serializable, + de\RaumZeitLabor\PartKeepr\Util\Deserializable, + de\RaumZeitLabor\PartKeepr\Util\BaseEntity; + +/** + * Represents a part in the database. The heart of our project. Handle with care! + * @Entity **/ +class ProjectPart extends BaseEntity implements Serializable, Deserializable { + /** + * @ManyToOne(targetEntity="de\RaumZeitLabor\PartKeepr\Part\Part") + */ + private $part; + + /** + * Specifies the amount of parts + * @Column(type="integer") + */ + private $quantity; + + /** + * Specifies the project which belongs to this project part + * @ManyToOne(targetEntity="de\RaumZeitLabor\PartKeepr\Part\Part") + */ + private $project; + + /** + * Specifies the remarks for this entry + * @Column(type="string",nullable=true) + */ + private $remarks; + + /** + * Sets the part which belongs to this entry + * @param Part $part + */ + public function setPart (Part $part) { + $this->part = $part; + } + + /** + * Returns the part which belongs to this entry + * @return Part + */ + public function getPart () { + return $this->part; + } + + /** + * Sets the quantity for this entry + * @param int $quantity + */ + public function setQuantity ($quantity) { + $this->quantity = intval($quantity); + } + + /** + * Returns the quantity for this project + * @return int the amount of parts needed + */ + public function getQuantity () { + return $this->quantity; + } + + /** + * Sets the project assigned to this entry + * @param Project $project + */ + public function setProject (Project $project) { + $this->project = $project; + } + + /** + * Returns the project assigned to this entry + * @return Project + */ + public function getProject () { + return $this->project; + } + + /** + * Sets the remarks for this entry + * @param string $remarks + */ + public function setRemarks ($remarks) { + $this->remarks = $remarks; + } + + /** + * Returns the remarks for this entry + * @return string + */ + public function getRemarks () { + return $this->remarks; + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Util.Serializable::serialize() + */ + public function serialize () { + return array( + "id" => $this->getId(), + "quantity" => $this->getQuantity(), + "part_id" => is_object($this->getPart()) ? $this->getPart()->getId() : 0, + "part_name" => is_object($this->getPart()) ? $this->getPart()->getName() : 0, + "project_id" => $this->getProject()->getId(), + "remarks" => $this->getRemarks() + ); + } + + /** + * Deserializes the project + * @param array $parameters The array with the parameters to set + */ + public function deserialize (array $parameters) { + foreach ($parameters as $key => $value) { + switch ($key) { + case "remarks": + $this->setRemarks($value); + break; + case "quantity": + $this->setQuantity($value); + break; + case "part_id": + $part = Part::loadById($value); + $this->setPart($part); + break; + } + } + } + +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectService.php b/src/backend/de/RaumZeitLabor/PartKeepr/Project/ProjectService.php @@ -0,0 +1,65 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Project; +use de\RaumZeitLabor\PartKeepr\Service\RestfulService; + +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Service\Service; +use de\RaumZeitLabor\PartKeepr\Project\ProjectManager, + de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter; + +class ProjectService extends Service implements RestfulService { + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::get() + */ + public function get () { + if ($this->hasParameter("id")) { + return array("data" => ProjectManager::getInstance()->getEntity($this->getParameter("id"))->serialize()); + } else { + $parameters = new ManagerFilter($this); + return ProjectManager::getInstance()->getList($parameters); + } + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::create() + */ + public function create () { + $this->requireParameter("name"); + + $entity = ProjectManager::getInstance()->createEntity($this->getParameters()); + + return array("data" => $entity->serialize()); + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::update() + */ + public function update () { + $this->requireParameter("id"); + $this->requireParameter("name"); + $entity = ProjectManager::getInstance()->getEntity($this->getParameter("id")); + $entity->deserialize($this->getParameters()); + + PartKeepr::getEM()->flush(); + + return array("data" => $entity->serialize()); + + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::destroy() + */ + public function destroy () { + $this->requireParameter("id"); + + ProjectManager::getInstance()->deleteEntity($this->getParameter("id")); + + return array("data" => null); + } +}+ \ No newline at end of file diff --git a/src/frontend/css/PartKeepr.css b/src/frontend/css/PartKeepr.css @@ -116,4 +116,8 @@ td.o2 { .icon-infocard { background-image: url(../resources/fugue-icons/icons/infocard.png) !important; +} + +.icon-drill { + background-image: url(../resources/fugue-icons/icons/drill.png) !important; } \ No newline at end of file diff --git a/src/frontend/js/Components/MenuBar.js b/src/frontend/js/Components/MenuBar.js @@ -1,11 +1,16 @@ Ext.define('PartKeepr.MenuBar', { extend: 'Ext.toolbar.Toolbar', initComponent: function () { - //this.cls = "partkeepr-mainmenu"; this.ui = "mainmenu"; + // @todo this is an ugly list of configurations. Refactor this in a cleaner way. + this.menu = Ext.create('Ext.menu.Menu', { - items: [ + items: [{ + text: i18n('Projects'), + icon: 'resources/fugue-icons/icons/drill.png', + handler: this.editProjects + }, { text: i18n('Footprints'), icon: 'resources/fugue-icons/icons/fingerprint.png', @@ -204,6 +209,16 @@ Ext.define('PartKeepr.MenuBar', { PartKeepr.getApplication().addItem(j); j.show(); + }, + editProjects: function () { + var j = Ext.create("PartKeepr.ProjectEditorComponent", { + title: i18n("Projects"), + iconCls: 'icon-drill', + closable: true + }); + + PartKeepr.getApplication().addItem(j); + j.show(); } }); \ No newline at end of file diff --git a/src/frontend/js/Components/Project/ProjectEditor.js b/src/frontend/js/Components/Project/ProjectEditor.js @@ -0,0 +1,107 @@ +/** + * Represents the project editor view + */ +Ext.define('PartKeepr.ProjectEditor', { + extend: 'PartKeepr.Editor', + alias: 'widget.ProjectEditor', + + // Various style configurations + saveText: i18n("Save Project"), + defaults: { + anchor: '100%', + labelWidth: 110 + }, + layout: { + type: 'vbox', + align : 'stretch', + pack : 'start' + }, + + /** + * Initializes the component + */ + initComponent: function () { + /** + * Due to an ExtJS issue, we need to delay the event + * for a bit. + * + * @todo Fix this in a cleaner way + */ + this.on("startEdit", this.onEditStart, this,{ + delay: 200 + }); + + this.on("itemSaved", this._onItemSaved, this); + + var config = {}; + + // Build the initial (empty) store for the project parts + Ext.Object.merge(config, { + autoLoad: false, + model: "PartKeepr.ProjectPart", + autoSync: false, // Do not change. If true, new (empty) records would be immediately commited to the database. + remoteFilter: false, + remoteSort: false + }); + + this.store = Ext.create('Ext.data.Store', config); + + this.partGrid = Ext.create("PartKeepr.ProjectPartGrid", { + store: this.store, + listeners: { + edit: this.onProjectGridEdit + } + }); + + var container = Ext.create("Ext.form.FieldContainer", { + fieldLabel: i18n("Project Parts"), + labelWidth: 110, + layout: 'fit', + flex: 1, + items: this.partGrid + }); + + this.items = [{ + xtype: 'textfield', + name: 'name', + height: 20, + fieldLabel: i18n("Project Name") + },{ + xtype: 'textarea', + name: 'description', + fieldLabel: i18n("Project Description"), + height: 70 + }, + container + ]; + this.callParent(); + + }, + /** + * Handle transparent setting of the part name after a value was selected from the combobox + */ + onProjectGridEdit: function (editor, e) { + if (e.field == "part_id") { + var rec = e.column.getEditor().store.getById(e.value); + if (rec) { + e.record.set("part_name", rec.get("name")); + } + + } + }, + /** + * Re-bind the store after an item was saved + */ + _onItemSaved: function (record) { + this.partGrid.bindStore(record.parts()); + }, + /** + * Bind the store as soon as the view was rendered. + * + * @todo This is a hack, because invocation of this method is delayed. + */ + onEditStart: function () { + var store = this.record.parts(); + this.partGrid.bindStore(store); + } +});+ \ No newline at end of file diff --git a/src/frontend/js/Components/Project/ProjectEditorComponent.js b/src/frontend/js/Components/Project/ProjectEditorComponent.js @@ -0,0 +1,21 @@ +/** + * Represents the project editor component + */ +Ext.define('PartKeepr.ProjectEditorComponent', { + extend: 'PartKeepr.EditorComponent', + alias: 'widget.ProjectEditorComponent', + navigationClass: 'PartKeepr.ProjectGrid', + editorClass: 'PartKeepr.ProjectEditor', + newItemText: i18n("New Project"), + model: 'PartKeepr.Project', + initComponent: function () { + this.createStore({ + sorters: [{ + property: 'name', + direction:'ASC' + }] + }); + + this.callParent(); + } +});+ \ No newline at end of file diff --git a/src/frontend/js/Components/Project/ProjectGrid.js b/src/frontend/js/Components/Project/ProjectGrid.js @@ -0,0 +1,14 @@ +/** + * Represents the project grid + */ +Ext.define('PartKeepr.ProjectGrid', { + extend: 'PartKeepr.EditorGrid', + alias: 'widget.ProjectGrid', + columns: [ + {header: i18n("Project"), dataIndex: 'name', flex: 1} + ], + addButtonText: i18n("Add Project"), + addButtonIcon: 'resources/fugue-icons/icons/drill--plus.png', + deleteButtonText: i18n("Delete Project"), + deleteButtonIcon: 'resources/fugue-icons/icons/drill--minus.png' +});+ \ No newline at end of file diff --git a/src/frontend/js/Components/Project/ProjectPartGrid.js b/src/frontend/js/Components/Project/ProjectPartGrid.js @@ -0,0 +1,96 @@ +/** + * Represents an editable list of project parts. + */ +Ext.define('PartKeepr.ProjectPartGrid', { + extend: 'Ext.grid.Panel', + + /* Column definitions */ + columns: [{ + header: i18n("Quantity"), dataIndex: 'quantity', + wdith: 50, + editor: { + xtype: 'numberfield', + allowBlank: false, + minValue: 1 + } + }, { + header: i18n("Part"), dataIndex: 'part_id', + flex: 1, + editor: { + xtype: 'RemotePartComboBox' + }, + renderer: function (val,p,rec) { + return rec.get("part_name"); + } + },{ + header: i18n("Remarks"), dataIndex: 'remarks', + flex: 1, + editor: { + xtype: 'textfield' + } + }], + + /** + * Initializes the component + */ + initComponent: function () { + + this.editing = Ext.create('Ext.grid.plugin.CellEditing', { + clicksToEdit: 1 + }); + + this.plugins = [ this.editing ]; + + this.deleteButton = Ext.create("Ext.button.Button", { + text: 'Delete', + disabled: true, + itemId: 'delete', + scope: this, + icon: 'resources/silkicons/brick_delete.png', + handler: this.onDeleteClick + }); + + this.dockedItems = [{ + xtype: 'toolbar', + items: [{ + text: 'Add', + scope: this, + icon: 'resources/silkicons/brick_add.png', + handler: this.onAddClick + }, this.deleteButton] + }]; + + this.callParent(); + + this.getSelectionModel().on('selectionchange', this.onSelectChange, this); + }, + /** + * Creates a new row and sets the default quantity to 1. + */ + onAddClick: function () { + this.editing.cancelEdit(); + + var rec = new PartKeepr.ProjectPart({ + quantity: 1 + }); + + this.store.insert(this.store.count(), rec); + + this.editing.startEdit(rec, this.columns[0]); + }, + /** + * Removes the currently selected row + */ + onDeleteClick: function () { + var selection = this.getView().getSelectionModel().getSelection()[0]; + if (selection) { + this.store.remove(selection); + } + }, + /** + * Enables or disables the delete button, depending on the row selection + */ + onSelectChange: function(selModel, selections){ + this.deleteButton.setDisabled(selections.length === 0); + } +});+ \ No newline at end of file diff --git a/src/frontend/js/Components/Widgets/RemotePartComboBox.js b/src/frontend/js/Components/Widgets/RemotePartComboBox.js @@ -0,0 +1,25 @@ +/** + * Represents a part combobox which supports type-ahead and remote querying. + */ +Ext.define("PartKeepr.RemotePartComboBox",{ + extend:"Ext.form.field.ComboBox", + alias: 'widget.RemotePartComboBox', + displayField: 'name', + valueField: 'id', + queryMode: 'remote', + triggerAction: 'all', + typeAhead: true, + typeAheadDelay: 100, + minChars: 2, + forceSelection: true, + initComponent: function () { + this.store = Ext.create("Ext.data.Store", + { + model: 'PartKeepr.Part', + pageSize: 30, + autoLoad: true + }); + this.callParent(); + } +}); + diff --git a/src/frontend/js/Models/Project.js b/src/frontend/js/Models/Project.js @@ -0,0 +1,19 @@ +/** + * Represents a project + */ +Ext.define("PartKeepr.Project", { + extend: "Ext.data.Model", + fields: [ + { id: 'id', name: 'id', type: 'int' }, + { name: 'name', type: 'string'}, + { name: 'description', type: 'string'}, + { name: 'user_id', type: 'int'} + ], + hasMany: [ + { model: 'PartKeepr.ProjectPart', name: 'parts'} + ], + proxy: PartKeepr.getRESTProxy("Project"), + getRecordName: function () { + return this.get("name"); + } +});+ \ No newline at end of file diff --git a/src/frontend/js/Models/ProjectPart.js b/src/frontend/js/Models/ProjectPart.js @@ -0,0 +1,22 @@ +/** + * Represents a project part + */ +Ext.define("PartKeepr.ProjectPart", { + extend: "Ext.data.Model", + fields: [ + { + id: 'id', + name: 'id', + type: 'int' + }, + { name: 'project_id', type: 'int'}, + { name: 'part_id', type: 'int'}, + { name: 'part_name', type: 'string'}, + { name: 'quantity', type: 'int'}, + { name: 'remarks', type: 'string'} + + ], + belongsTo: { type: 'belongsTo', model: 'PartKeepr.Project', primaryKey: 'id', foreignKey: 'project_id'}, + belongsTo: { type: 'belongsTo', model: 'PartKeepr.Part', primaryKey: 'id', foreignKey: 'part_id'}, + proxy: PartKeepr.getRESTProxy("ProjectPart") +});+ \ No newline at end of file