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 := anInteger

2. 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 := nil

Instance 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;
        yourself

Instance 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 open

Instance 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!

⚠️ **GitHub.com Fallback** ⚠️