Simple Inventory App - kendmaclean/pharo12 GitHub Wiki
Note: Pharo is continuously evolving - therefore make sure you use a Pharo 12 image with this example.
(Created with the help of Google AI Studio)
Let's create a simple Inventory application example using Pharo 12 and the Spec2 UI framework.
1. The Domain Model (Product)
First, define the object we want to manage.
Object << #Product
slots: { #name . #description . #quantity . #price };
package: 'SimpleInventoryApp'Add accessors and a basic initializer:
Product >> initialize
super initialize.
name := ''.
description := ''.
quantity := 0.
Product >> initializeWindow: aWindowPresenter
aWindowPresenter
title: 'Simple Inventory System';
initialExtent: 600@450.
Product >> decreaseStockBy: amount
self quantity: self quantity - amount
Product >> description
^ description ifNil: [ '' ]
Product >> description: aString
description := aString
Product >> displayString
^ String streamContents: [ :s |
s << self name.
s << ' (Qty: ' << self quantity asString.
self price ifNotNil: [ :p | s << ', Price: $' << p asString ].
s << ')' ]
Product >> increaseStockBy: amount
self quantity: self quantity + amount
Product >> name
^ name ifNil: [ '' ]
Product >> name: aString
name := aString
Product >> price
^ price ifNil: [ 0 ]
Product >> price: aNumber
price := aNumber
Product >> printOn: aStream
aStream
nextPutAll: self class name;
nextPutAll: ' (';
nextPutAll: self name;
nextPutAll: ' - Qty: ';
print: self quantity;
nextPutAll: ')'
Product >> quantity
^ quantity ifNil: [ 0 ]
Product >> quantity: anInteger
quantity := anInteger2. The Repository (ProductRepository)
We need a place to store our Person objects. For simplicity, we'll use an in-memory repository implemented as a Singleton.
Instance side:
Object << #ProductRepository
slots: { #products };
package: 'SimpleInventoryApp'Class side:
Object class << ProductRepository class
slots: { #SoleInstance }Implement the Singleton pattern and CRUD methods:
Class side:
ProductRepository class >> current
"Answer the singleton instance, creating it if necessary"
^ SoleInstance ifNil: [ SoleInstance := self new ]
ProductRepository class >> reset
"Reset the repository (useful for testing)"
SoleInstance := nilInstance side:
ProductRepository >> initialize
"Initialize the instance with an empty collection"
super initialize.
products := OrderedCollection new
ProductRepository >> allProducts
"Answer all products, sorted by name"
^ products sorted: [ :a :b | a name < b name ]
ProductRepository >> addProduct: aProduct
"Add a new product"
products add: aProduct
ProductRepository >> removeProduct: aProduct
"Add a new product"
products remove: aProduct ifAbsent: []
ProductRepository >> updateProduct: oldProduct with: newProductData
"Find oldProduct and update its state with data from newProductData.
Note: This assumes newProductData is a Product object holding the new values.
A more robust approach might involve finding by a unique ID if names/ages can change."
| productToUpdate |
productToUpdate := products detect: [ :p | p == oldProduct ] ifNone: [ ^ self ]. "Find the exact instance"
productToUpdate name: newProductData name.
productToUpdate description: newProductData description.
productToUpdate quantity: newProductData quantity.
productToUpdate price: newProductData price.
ProductRepository >> addSampleData
self
addProduct: (Product new name: 'Laptop'; description: '14 inch Ultrabook'; quantity: 10; price: 1200.00);
addProduct: (Product new name: 'Keyboard'; description: 'Mechanical Keyboard'; quantity: 25; price: 75.50);
addProduct: (Product new name: 'Mouse'; description: 'Wireless Optical Mouse'; quantity: 50; price: 25.00).3. The Editor Dialog (PersonEditor)
This dialog will be used for both creating and editing Person objects.
SpPresenter << #ProductEditor
slots: { #title . #product . #nameInput . #descriptionInput . #quantityInput . #priceInput };
package: 'SimpleInventoryApp'Implement the UI layout and logic:
Class side:
ProductEditor class >> forProduct: aProduct
^ self new
product: aProduct;
yourself
ProductEditor class >> forProduct: aProduct title: aString
^ (self forProduct: aProduct)
title: aString;
yourselfInstance side:
ProductEditor >> initialize
product := Product new.
super initialize.
ProductEditor >> initializeDialogWindow: aWindowPresenter
aWindowPresenter
title: (self title ifNil: ['Product Details']);
initialExtent: 400@220;
addButton: 'OK' do: [
self updateProduct.
aWindowPresenter close ];
addButton: 'Cancel' do: [
aWindowPresenter close ]
ProductEditor >> initializePresenters
nameInput := self newTextInput
placeholder: 'Product Name';
text: (product name);
yourself.
descriptionInput := self newTextInput
placeholder: 'Description';
text: (product description);
yourself.
quantityInput := self newNumberInput
beInteger;
minimum: 0;
placeholder: 'Quantity';
number: (product quantity);
yourself.
priceInput := self newNumberInput
beFloat;
digits: 2;
minimum: 0;
placeholder: 'Price (optional)';
number: (product price);
yourself.
ProductEditor >> defaultLayout
^ SpGridLayout new
borderWidth: 5;
columnSpacing: 10;
rowSpacing: 5;
add: 'Name:' at: 1@1; add: nameInput at: 2@1;
add: 'Description:' at: 1@2; add: descriptionInput at: 2@2;
add: 'Quantity:' at: 1@3; add: quantityInput at: 2@3;
add: 'Price:' at: 1@4; add: priceInput at: 2@4;
yourself
ProductEditor >> nameInput
^ nameInput
ProductEditor >> nameInput: anObject
nameInput := anObject
ProductEditor >> product
^ product
ProductEditor >> product: aProduct
product := aProduct.
self updateFormFields.
ProductEditor >> title
^ title
ProductEditor >> title: anObject
title := anObject
ProductEditor >> updateFormFields
"Update form fields from product"
nameInput text: product name.
descriptionInput text: product description.
quantityInput number: product quantity.
priceInput number: product price.
ProductEditor >> updateProduct
"Update the product model from the input fields"
product
name: nameInput text;
description: descriptionInput text;
quantity: (quantityInput number ifNil: [0]);
price: priceInput number.
^ product
4. The Main Browser (InventoryApp)
This is the main window showing the list and CRUD buttons.
SpPresenter << #InventoryApp
slots: { #selectedProduct . #productList . #addButton . #editButton . #removeButton . #increaseStockButton . #decreaseStockButton };
package: 'SimpleInventoryApp'Implement the main UI:
Class side:
InventoryApp class >> open
<script>
^ self new openInstance side:
InventoryApp >> initialize
"Add some sample data if the repository is empty"
ProductRepository current allProducts isEmpty ifTrue: [
ProductRepository current addSampleData ].
super initialize.
InventoryApp >> initializeWindow: aWindowPresenter
super initializeWindow: aWindowPresenter.
aWindowPresenter
title: 'Simple Inventory System';
initialExtent: 400@250.
InventoryApp >> initializePresenters
productList := self newList
items: ProductRepository current allProducts;
display: [ :item | item displayString ];
whenSelectionChangedDo: [ :selection |
selectedProduct := selection selectedItem.
self updateButtonStates ];
yourself.
self initializeButtons.
self updateButtonStates.
InventoryApp >> initializeButtons
addButton := self newButton
label: 'Add';
icon: (self iconNamed: #add);
action: [ self addProductAction ];
yourself.
editButton := self newButton
label: 'Edit';
icon: (self iconNamed: #edit);
action: [ self editProductAction ];
yourself.
removeButton := self newButton
label: 'Remove';
icon: (self iconNamed: #remove);
action: [ self removeProductAction ];
yourself.
increaseStockButton := self newButton
label: 'Increase Stock';
icon: (self iconNamed: #up);
action: [ self increaseStockAction ];
yourself.
decreaseStockButton := self newButton
label: 'Decrease Stock';
icon: (self iconNamed: #down);
action: [ self decreaseStockAction ];
yourself.
InventoryApp >> updateButtonStates
| productSelected |
productSelected := selectedProduct isNotNil.
editButton enabled: productSelected.
removeButton enabled: productSelected.
increaseStockButton enabled: productSelected.
decreaseStockButton enabled: productSelected
InventoryApp >> defaultLayout
^ SpBoxLayout newHorizontal
spacing: 10;
add: (self newLabel label: 'Products:') expand: false;
add: productList expand: true;
add: self buildButtonPanel expand: false;
yourself
InventoryApp >> buildButtonPanel
^ SpBoxLayout newVertical
spacing: 5;
add: addButton expand: false;
add: editButton expand: false;
add: removeButton expand: false;
"add: (self newSeparator) expand: false;"
add: increaseStockButton expand: false;
add: decreaseStockButton expand: false;
add: SpBoxLayout newVertical expand: true; "spacer"
yourself
InventoryApp >> addProductAction
| newProduct dialog |
newProduct := Product new.
dialog := ProductEditor forProduct: newProduct title: 'Add New Product'.
dialog openModal.
(newProduct name notEmpty) ifTrue: [
"products add: newProduct."
ProductRepository current addProduct: newProduct.
self updateProductList.
productList selectItem: newProduct ]
InventoryApp >> adjustStockAction: adjustmentBlock title: aString
| amount |
selectedProduct ifNil: [ ^ self ].
amount := UIManager default
request: aString
initialAnswer: '1'
title: 'Adjust Stock'.
amount ifNil: [ ^ self ].
[ | numericAmount |
numericAmount := amount asNumber.
numericAmount > 0 ifFalse: [ self error: 'Amount must be positive.' ].
adjustmentBlock value: selectedProduct value: numericAmount.
self updateProductList
] on: Error do: [ :ex |
self inform: 'Invalid amount: ', ex messageText ]
InventoryApp >> decreaseStockAction
self adjustStockAction: [ :product :amount |
product decreaseStockBy: amount ]
title: 'Decrease Stock by:'
InventoryApp >> editProductAction
| originalProduct dialog editedProduct |
selectedProduct ifNil: [ ^ self ].
originalProduct := selectedProduct. "Keep track of the original instance"
dialog := (ProductEditor forProduct: selectedProduct title: 'Edit Product').
dialog openModal.
editedProduct := dialog product.
editedProduct ifNotNil: [
"Update the original person object in the repository with the new data"
ProductRepository current updateProduct: originalProduct with: editedProduct.
self updateProductList. "Refresh the list"
"listPresenter selectItem: originalPerson"
]
InventoryApp >> increaseStockAction
self adjustStockAction: [ :product :amount |
product increaseStockBy: amount ]
title: 'Increase Stock by:'
InventoryApp >> removeProductAction
selectedProduct ifNil: [ ^ self ].
(self confirm: 'Are you sure you want to remove ', selectedProduct name, '?')
ifFalse: [^self].
ProductRepository current removeProduct: selectedProduct.
selectedProduct := nil.
self updateProductList.
self updateButtonStates
InventoryApp >> selectedProduct
^ selectedProduct
InventoryApp >> updateProductList
productList items: ProductRepository current allProducts.
(selectedProduct notNil and: [ (ProductRepository current allProducts) includes: selectedProduct ])
ifTrue: [ productList selectItem: selectedProduct ]
ifFalse: [
selectedProduct := nil.
self updateButtonStates ]Step 6: How to Run
Run: Open a Playground. Type TaskManagerApp open and "Do it" (Ctrl+D).
You should now have a working Task Manager application!