diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml new file mode 100644 index 0000000..4bb498d --- /dev/null +++ b/.github/workflows/auto-pr.yml @@ -0,0 +1,30 @@ +name: Auto-create PR on push to dev + +on: + push: + branches: + - dev + +jobs: + create-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Create Pull Request if not exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING=$(gh pr list --base main --head dev --json number --jq '.[0].number') + if [ -z "$EXISTING" ]; then + gh pr create \ + --base main \ + --head dev \ + --title "Sprint 5: Price, Expiry, and Supplier Tracking" \ + --body "## Changes\n- Added price field with total inventory value on dashboard\n- Added expiration date tracking with expiring-soon alerts\n- Added supplier tracking\n- Added 'Add Expiring Soon Items' button in Receive Delivery\n- Updated seed data with real-world supplier names" + else + echo "PR #$EXISTING already exists, skipping." + fi diff --git a/doc/sprint 5.md b/doc/sprint 5.md new file mode 100644 index 0000000..5ff1208 --- /dev/null +++ b/doc/sprint 5.md @@ -0,0 +1,49 @@ +# Sprint 5 Ceremony Minutes + +Date: 2026-04-29 + +## Members Present + +- Dustin De Luna +- Jared Miller +- Nnaemeka Onochie + +--- + +## Demo + +This sprint, we completed: + +- Added price field to inventory items with total inventory value displayed on the dashboard +- Added expiration date tracking with visual alerts for items expiring within 7 days +- Added Supplier tracking field to all inventory items +- Updated seed data with real-world supplier names (Prairie Farms, Kroger, Great Value, Kirkland, Tech, Target Essentials) +- Wrote `doc/sprint5.md` + +--- + +## Screenshots + +Here are screenshots of what we did: +![alt text](<../images/Screenshot 2026-04-29 at 11.03.18 PM.png>) + +--- + +## Retro + +### What Went Well + +- Price and expiration date features add real business value to the app +- Supplier tracking helps contextualize inventory data +- Dashboard summary bar now shows total inventory value and expiring-soon count at a glance + +### What Could Be Improved + +- UI polish and consistency could be improved +- The Delivery transaction type is currently logged as UPDATE in transaction history + +### Actionable Commitments + +- As a team, we will fix the DELIVERY transaction type logging and continue improving the UI + +--- diff --git "a/images/Screenshot 2026-04-29 at 11.03.18\342\200\257PM.png" "b/images/Screenshot 2026-04-29 at 11.03.18\342\200\257PM.png" new file mode 100644 index 0000000..321b9a3 Binary files /dev/null and "b/images/Screenshot 2026-04-29 at 11.03.18\342\200\257PM.png" differ diff --git a/src/main/java/com/inventory/model/Item.java b/src/main/java/com/inventory/model/Item.java index 051f87e..6909761 100644 --- a/src/main/java/com/inventory/model/Item.java +++ b/src/main/java/com/inventory/model/Item.java @@ -1,5 +1,7 @@ package com.inventory.model; +import java.time.LocalDate; + public class Item { private final String id; @@ -7,10 +9,14 @@ public class Item { private final String category; private int quantity; private final String location; + private double price; + private String expiresAt; // "YYYY-MM-DD" or null/empty = no expiry + private String supplier; private int lowStockThreshold = 5; - - public Item(String id, String name, String category, int quantity, String location) { + + public Item(String id, String name, String category, int quantity, String location, + double price, String expiresAt, String supplier) { if (quantity < 0) { throw new IllegalArgumentException("Quantity must not be negative."); } @@ -19,13 +25,46 @@ public Item(String id, String name, String category, int quantity, String locati this.category = category; this.quantity = quantity; this.location = location; + this.price = price; + this.expiresAt = expiresAt; + this.supplier = supplier; + } + + public Item(String id, String name, String category, int quantity, String location) { + this(id, name, category, quantity, location, 0.0, null, null); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCategory() { + return category; + } + + public int getQuantity() { + return quantity; + } + + public String getLocation() { + return location; + } + + public double getPrice() { + return price; + } + + public String getExpiresAt() { + return expiresAt; } - public String getId() { return id; } - public String getName() { return name; } - public String getCategory() { return category; } - public int getQuantity() { return quantity; } - public String getLocation() { return location; } + public String getSupplier() { + return supplier; + } public int getLowStockThreshold() { return lowStockThreshold; @@ -45,10 +84,33 @@ public void setQuantity(int quantity) { this.quantity = quantity; } + public void setPrice(double price) { + this.price = price; + } + + public void setExpiresAt(String expiresAt) { + this.expiresAt = expiresAt; + } + + public void setSupplier(String supplier) { + this.supplier = supplier; + } + public boolean isLowStock() { return quantity <= lowStockThreshold; } + public boolean isExpiringSoon() { + if (expiresAt == null || expiresAt.isEmpty()) + return false; + try { + LocalDate expiry = LocalDate.parse(expiresAt); + return !expiry.isAfter(LocalDate.now().plusDays(7)); + } catch (Exception e) { + return false; + } + } + @Override public String toString() { return "Item{id='" + id + "', name='" + name + "', category='" + category diff --git a/src/main/java/com/inventory/model/ItemFactory.java b/src/main/java/com/inventory/model/ItemFactory.java index 9e13c05..e3fa762 100644 --- a/src/main/java/com/inventory/model/ItemFactory.java +++ b/src/main/java/com/inventory/model/ItemFactory.java @@ -5,10 +5,19 @@ public class ItemFactory { /** - * Creates a new Item with an auto-generated unique ID. + * Creates a new Item with an auto-generated unique ID */ public static Item createItem(String name, String category, int quantity, String location) { String id = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); return new Item(id, name, category, quantity, location); } + + /** + * Creates a new Item with price, expiry date, and supplier. + */ + public static Item createItem(String name, String category, int quantity, String location, + double price, String expiresAt, String supplier) { + String id = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + return new Item(id, name, category, quantity, location, price, expiresAt, supplier); + } } diff --git a/src/main/java/com/inventory/service/Database.java b/src/main/java/com/inventory/service/Database.java index 77c581e..9685d90 100644 --- a/src/main/java/com/inventory/service/Database.java +++ b/src/main/java/com/inventory/service/Database.java @@ -36,11 +36,14 @@ public Connection getConnection() { public void init() { String sql = """ CREATE TABLE IF NOT EXISTS items ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - category TEXT NOT NULL, - quantity INTEGER NOT NULL, - location TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + quantity INTEGER NOT NULL, + location TEXT NOT NULL, + price REAL NOT NULL DEFAULT 0.0, + expires_at TEXT DEFAULT '', + supplier TEXT DEFAULT '' ) """; String transactionSql = """ @@ -58,6 +61,18 @@ CREATE TABLE IF NOT EXISTS transactions ( } catch (SQLException e) { System.err.println("Database init error: " + e.getMessage()); } + // Migrate existing databases that predate the new columns + String[] migrations = { + "ALTER TABLE items ADD COLUMN price REAL NOT NULL DEFAULT 0.0", + "ALTER TABLE items ADD COLUMN expires_at TEXT DEFAULT ''", + "ALTER TABLE items ADD COLUMN supplier TEXT DEFAULT ''" + }; + for (String migration : migrations) { + try (Statement m = connection.createStatement()) { + m.execute(migration); + } catch (SQLException ignored) { + /* column already exists */ } + } seedData(); } @@ -73,45 +88,74 @@ private void seedData() { return; } + // id, name, category, quantity, location, price, expires_at, supplier String[][] data = { - { "SEED0001", "Whole Milk", "Grocery", "24", "Aisle 2 - Dairy & Eggs" }, - { "SEED0002", "White Bread", "Grocery", "18", "Aisle 3 - Bakery" }, - { "SEED0003", "Bananas", "Grocery", "40", "Aisle 1 - Produce" }, - { "SEED0004", "Chicken Breast", "Grocery", "8", "Aisle 4 - Meat & Seafood" }, - { "SEED0005", "Frozen Pizza", "Grocery", "15", "Aisle 5 - Frozen Foods" }, - { "SEED0006", "Orange Juice", "Grocery", "12", "Aisle 6 - Beverages" }, - { "SEED0007", "Potato Chips", "Grocery", "30", "Aisle 7 - Snacks & Candy" }, - { "SEED0008", "Paper Towels", "Home & Garden", "10", "Aisle 8 - Household" }, - { "SEED0009", "AA Batteries", "Electronics", "3", "Aisle 9 - Electronics" }, - { "SEED0010", "Ground Beef", "Grocery", "6", "Aisle 4 - Meat & Seafood" }, - { "SEED0011", "Cheddar Cheese", "Grocery", "14", "Aisle 2 - Dairy & Eggs" }, - { "SEED0012", "Eggs (dozen)", "Grocery", "20", "Aisle 2 - Dairy & Eggs" }, - { "SEED0013", "Apples", "Grocery", "35", "Aisle 1 - Produce" }, - { "SEED0014", "Ice Cream", "Grocery", "4", "Aisle 5 - Frozen Foods" }, - { "SEED0015", "Cola 12-pack", "Grocery", "9", "Aisle 6 - Beverages" }, - { "SEED0016", "Dish Soap", "Home & Garden", "7", "Aisle 8 - Household" }, - { "SEED0017", "T-Shirt (M)", "Clothing", "12", "Aisle 10 - Clothing" }, - { "SEED0018", "Basketball", "Sports", "2", "Aisle 12 - Sports & Outdoors" }, - { "SEED0019", "Notebook (3-pack)", "Office Supplies", "25", "Aisle 8 - Household" }, - { "SEED0020", "Candy Bar", "Grocery", "50", "Aisle 7 - Snacks & Candy" }, - { "SEED0021", "Great Value Water 24pk", "Grocery", "36", "Aisle 6 - Beverages" }, - { "SEED0022", "Peanut Butter", "Grocery", "11", "Aisle 7 - Snacks & Candy" }, - { "SEED0023", "Spaghetti Noodles", "Grocery", "22", "Aisle 7 - Snacks & Candy" }, - { "SEED0024", "Tomato Sauce", "Grocery", "19", "Aisle 7 - Snacks & Candy" }, - { "SEED0025", "Butter", "Grocery", "7", "Aisle 2 - Dairy & Eggs" }, - { "SEED0026", "Greek Yogurt", "Grocery", "16", "Aisle 2 - Dairy & Eggs" }, - { "SEED0027", "Bacon", "Grocery", "3", "Aisle 4 - Meat & Seafood" }, - { "SEED0028", "Strawberries", "Grocery", "10", "Aisle 1 - Produce" }, - { "SEED0029", "Laundry Detergent", "Home & Garden", "5", "Aisle 8 - Household" }, - { "SEED0030", "Trash Bags", "Home & Garden", "8", "Aisle 8 - Household" }, - { "SEED0031", "Toothpaste", "Health & Beauty", "13", "Aisle 8 - Household" }, - { "SEED0032", "Shampoo", "Health & Beauty", "9", "Aisle 8 - Household" }, - { "SEED0033", "USB-C Cable", "Electronics", "4", "Aisle 9 - Electronics" }, - { "SEED0034", "Cereal", "Grocery", "27", "Aisle 3 - Bakery" }, - { "SEED0035", "Frozen Chicken Nuggets", "Grocery", "1", "Aisle 5 - Frozen Foods" }, + { "SEED0001", "Whole Milk", "Grocery", "24", "Aisle 2 - Dairy & Eggs", "3.49", "2026-05-02", + "Prairie Farms" }, + { "SEED0002", "White Bread", "Grocery", "18", "Aisle 3 - Bakery", "2.99", "2026-05-03", + "Great Value" }, + { "SEED0003", "Bananas", "Grocery", "40", "Aisle 1 - Produce", "0.59", "2026-04-30", "Kroger" }, + { "SEED0004", "Chicken Breast", "Grocery", "8", "Aisle 4 - Meat & Seafood", "8.99", "2026-05-01", + "Kroger" }, + { "SEED0005", "Frozen Pizza", "Grocery", "15", "Aisle 5 - Frozen Foods", "6.49", "2026-08-15", + "Great Value" }, + { "SEED0006", "Orange Juice", "Grocery", "12", "Aisle 6 - Beverages", "4.29", "2026-05-10", + "Kroger" }, + { "SEED0007", "Potato Chips", "Grocery", "30", "Aisle 7 - Snacks & Candy", "3.99", "2026-10-01", + "Great Value" }, + { "SEED0008", "Paper Towels", "Home & Garden", "10", "Aisle 8 - Household", "8.49", "", + "Kirkland" }, + { "SEED0009", "AA Batteries", "Electronics", "3", "Aisle 9 - Electronics", "7.99", "", "Tech" }, + { "SEED0010", "Ground Beef", "Grocery", "6", "Aisle 4 - Meat & Seafood", "7.49", "2026-04-30", + "Kroger" }, + { "SEED0011", "Cheddar Cheese", "Grocery", "14", "Aisle 2 - Dairy & Eggs", "4.79", "2026-05-12", + "Prairie Farms" }, + { "SEED0012", "Eggs (dozen)", "Grocery", "20", "Aisle 2 - Dairy & Eggs", "3.29", "2026-05-05", + "Prairie Farms" }, + { "SEED0013", "Apples", "Grocery", "35", "Aisle 1 - Produce", "1.49", "2026-05-08", "Kroger" }, + { "SEED0014", "Ice Cream", "Grocery", "4", "Aisle 5 - Frozen Foods", "5.29", "2026-07-20", + "Great Value" }, + { "SEED0015", "Cola 12-pack", "Grocery", "9", "Aisle 6 - Beverages", "5.99", "2026-12-01", + "Great Value" }, + { "SEED0016", "Dish Soap", "Home & Garden", "7", "Aisle 8 - Household", "3.49", "", "Kirkland" }, + { "SEED0017", "T-Shirt (M)", "Clothing", "12", "Aisle 10 - Clothing", "14.99", "", "Target Essentials" }, + { "SEED0018", "Basketball", "Sports", "2", "Aisle 12 - Sports & Outdoors", "24.99", "", + "Target Essentials" }, + { "SEED0019", "Notebook (3-pack)", "Office Supplies", "25", "Aisle 8 - Household", "6.99", "", + "Kirkland" }, + { "SEED0020", "Candy Bar", "Grocery", "50", "Aisle 7 - Snacks & Candy", "1.29", "2026-11-15", + "Great Value" }, + { "SEED0021", "Great Value Water 24pk", "Grocery", "36", "Aisle 6 - Beverages", "4.99", "2027-01-01", + "Great Value" }, + { "SEED0022", "Peanut Butter", "Grocery", "11", "Aisle 7 - Snacks & Candy", "3.99", "2026-09-30", + "Great Value" }, + { "SEED0023", "Spaghetti Noodles", "Grocery", "22", "Aisle 7 - Snacks & Candy", "1.89", "2026-12-31", + "Great Value" }, + { "SEED0024", "Tomato Sauce", "Grocery", "19", "Aisle 7 - Snacks & Candy", "1.99", "2026-11-30", + "Great Value" }, + { "SEED0025", "Butter", "Grocery", "7", "Aisle 2 - Dairy & Eggs", "4.49", "2026-05-20", + "Prairie Farms" }, + { "SEED0026", "Greek Yogurt", "Grocery", "16", "Aisle 2 - Dairy & Eggs", "1.99", "2026-05-04", + "Prairie Farms" }, + { "SEED0027", "Bacon", "Grocery", "3", "Aisle 4 - Meat & Seafood", "6.99", "2026-05-01", + "Kroger" }, + { "SEED0028", "Strawberries", "Grocery", "10", "Aisle 1 - Produce", "3.99", "2026-04-30", + "Kroger" }, + { "SEED0029", "Laundry Detergent", "Home & Garden", "5", "Aisle 8 - Household", "11.99", "", + "Kirkland" }, + { "SEED0030", "Trash Bags", "Home & Garden", "8", "Aisle 8 - Household", "7.99", "", + "Kirkland" }, + { "SEED0031", "Toothpaste", "Health & Beauty", "13", "Aisle 8 - Household", "3.49", "2026-12-31", + "Kirkland" }, + { "SEED0032", "Shampoo", "Health & Beauty", "9", "Aisle 8 - Household", "5.99", "2027-06-30", + "Kirkland" }, + { "SEED0033", "USB-C Cable", "Electronics", "4", "Aisle 9 - Electronics", "12.99", "", "Tech" }, + { "SEED0034", "Cereal", "Grocery", "27", "Aisle 3 - Bakery", "3.99", "2026-08-01", "Great Value" }, + { "SEED0035", "Frozen Chicken Nuggets", "Grocery", "1", "Aisle 5 - Frozen Foods", "5.49", "2026-05-04", + "Kroger" }, }; - String sql = "INSERT INTO items (id, name, category, quantity, location) VALUES (?, ?, ?, ?, ?)"; + String sql = "INSERT INTO items (id, name, category, quantity, location, price, expires_at, supplier) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = connection.prepareStatement(sql)) { for (String[] row : data) { ps.setString(1, row[0]); @@ -119,6 +163,9 @@ private void seedData() { ps.setString(3, row[2]); ps.setInt(4, Integer.parseInt(row[3])); ps.setString(5, row[4]); + ps.setDouble(6, Double.parseDouble(row[5])); + ps.setString(7, row[6]); + ps.setString(8, row[7]); ps.executeUpdate(); } } catch (SQLException e) { @@ -128,7 +175,7 @@ private void seedData() { public List loadAll() { List list = new ArrayList<>(); - String sql = "SELECT id, name, category, quantity, location FROM items"; + String sql = "SELECT id, name, category, quantity, location, price, expires_at, supplier FROM items"; try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { @@ -137,7 +184,10 @@ public List loadAll() { rs.getString("name"), rs.getString("category"), rs.getInt("quantity"), - rs.getString("location"))); + rs.getString("location"), + rs.getDouble("price"), + rs.getString("expires_at"), + rs.getString("supplier"))); } } catch (SQLException e) { System.err.println("Database load error: " + e.getMessage()); @@ -155,13 +205,16 @@ public void resetToSeed() { } public void insert(Item item) { - String sql = "INSERT INTO items (id, name, category, quantity, location) VALUES (?, ?, ?, ?, ?)"; + String sql = "INSERT INTO items (id, name, category, quantity, location, price, expires_at, supplier) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, item.getId()); ps.setString(2, item.getName()); ps.setString(3, item.getCategory()); ps.setInt(4, item.getQuantity()); ps.setString(5, item.getLocation()); + ps.setDouble(6, item.getPrice()); + ps.setString(7, item.getExpiresAt() != null ? item.getExpiresAt() : ""); + ps.setString(8, item.getSupplier() != null ? item.getSupplier() : ""); ps.executeUpdate(); } catch (SQLException e) { System.err.println("Database insert error: " + e.getMessage()); @@ -169,13 +222,16 @@ public void insert(Item item) { } public void update(Item item) { - String sql = "UPDATE items SET name = ?, category = ?, quantity = ?, location = ? WHERE id = ?"; + String sql = "UPDATE items SET name = ?, category = ?, quantity = ?, location = ?, price = ?, expires_at = ?, supplier = ? WHERE id = ?"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, item.getName()); ps.setString(2, item.getCategory()); ps.setInt(3, item.getQuantity()); ps.setString(4, item.getLocation()); - ps.setString(5, item.getId()); + ps.setDouble(5, item.getPrice()); + ps.setString(6, item.getExpiresAt() != null ? item.getExpiresAt() : ""); + ps.setString(7, item.getSupplier() != null ? item.getSupplier() : ""); + ps.setString(8, item.getId()); ps.executeUpdate(); } catch (SQLException e) { System.err.println("Database update error: " + e.getMessage()); @@ -211,23 +267,22 @@ public List loadTransactionsForItem(String itemId) { String sql = "SELECT id, item_id, type, amount, timestamp FROM transactions WHERE item_id = ? ORDER BY timestamp DESC"; try (PreparedStatement ps = connection.prepareStatement(sql)) { - ps.setString(1, itemId); + ps.setString(1, itemId); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - list.add(new Transaction( - rs.getString("id"), - rs.getString("item_id"), - rs.getString("type"), - rs.getInt("amount"), - rs.getString("timestamp") - )); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + list.add(new Transaction( + rs.getString("id"), + rs.getString("item_id"), + rs.getString("type"), + rs.getInt("amount"), + rs.getString("timestamp"))); + } } + } catch (SQLException e) { + System.err.println("Transaction load error: " + e.getMessage()); } - } catch (SQLException e) { - System.err.println("Transaction load error: " + e.getMessage()); - } - return list; -} + return list; + } } diff --git a/src/main/java/com/inventorypro/ui/Scenes.java b/src/main/java/com/inventorypro/ui/Scenes.java index c29b615..b083d74 100644 --- a/src/main/java/com/inventorypro/ui/Scenes.java +++ b/src/main/java/com/inventorypro/ui/Scenes.java @@ -36,26 +36,37 @@ public static Parent createDashboard(Stage stage) { Label totalItemsLabel = new Label(); Label lowStockLabel = new Label(); Label totalQtyLabel = new Label(); + Label totalValueLabel = new Label(); + Label expiringSoonLabel = new Label(); String statStyle = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-padding: 8 16; " + "-fx-background-color: #d9edf7; -fx-background-radius: 8; -fx-text-fill: #31708f;"; String lowStockStyle = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-padding: 8 16; " + "-fx-background-color: #fcf8e3; -fx-background-radius: 8; -fx-text-fill: #8a6d3b;"; + String expirySoonStyle = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-padding: 8 16; " + + "-fx-background-color: #fde8d8; -fx-background-radius: 8; -fx-text-fill: #c0392b; -fx-cursor: hand;"; totalItemsLabel.setStyle(statStyle); totalQtyLabel.setStyle(statStyle); + totalValueLabel.setStyle(statStyle); lowStockLabel.setStyle(lowStockStyle + "-fx-cursor: hand;"); + expiringSoonLabel.setStyle(expirySoonStyle); Runnable updateSummary = () -> { int total = items.size(); long lowStock = items.stream().filter(Item::isLowStock).count(); int totalQty = items.stream().mapToInt(Item::getQuantity).sum(); + double totalValue = items.stream().mapToDouble(i -> i.getPrice() * i.getQuantity()).sum(); + long expiringSoon = items.stream().filter(Item::isExpiringSoon).count(); totalItemsLabel.setText("Total Items: " + total); lowStockLabel.setText("Low Stock: " + lowStock); totalQtyLabel.setText("Total Quantity: " + totalQty); + totalValueLabel.setText(String.format("Total Value: $%.2f", totalValue)); + expiringSoonLabel.setText("Expiring Soon: " + expiringSoon); }; updateSummary.run(); items.addListener((ListChangeListener) c -> updateSummary.run()); - HBox summaryBar = new HBox(12, totalItemsLabel, lowStockLabel, totalQtyLabel); + HBox summaryBar = new HBox(12, totalItemsLabel, lowStockLabel, totalQtyLabel, totalValueLabel, + expiringSoonLabel); summaryBar.setPadding(new Insets(4, 0, 8, 0)); summaryBar.setStyle("-fx-border-color: #ddd; -fx-border-width: 0 0 1 0; -fx-padding: 6 0 10 0;"); @@ -65,13 +76,18 @@ public static Parent createDashboard(Stage stage) { FilteredList filtered = new FilteredList<>(items, p -> true); final boolean[] lowStockActive = { false }; + final boolean[] expiringSoonActive = { false }; final Button[] showAllBtn = { null }; String lowStockNormal = lowStockStyle + "-fx-cursor: hand;"; String lowStockHighlight = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-padding: 8 16; " + "-fx-background-color: #f2dede; -fx-background-radius: 8; -fx-text-fill: #a94442; -fx-cursor: hand;"; + String expirySoonHighlight = "-fx-font-size: 14px; -fx-font-weight: bold; -fx-padding: 8 16; " + + "-fx-background-color: #e74c3c; -fx-background-radius: 8; -fx-text-fill: white; -fx-cursor: hand;"; lowStockLabel.setOnMouseClicked(ev -> { lowStockActive[0] = !lowStockActive[0]; + expiringSoonActive[0] = false; + expiringSoonLabel.setStyle(expirySoonStyle); if (lowStockActive[0]) { lowStockLabel.setStyle(lowStockHighlight); search.clear(); @@ -84,11 +100,29 @@ public static Parent createDashboard(Stage stage) { } }); + expiringSoonLabel.setOnMouseClicked(ev -> { + expiringSoonActive[0] = !expiringSoonActive[0]; + lowStockActive[0] = false; + lowStockLabel.setStyle(lowStockNormal); + if (expiringSoonActive[0]) { + expiringSoonLabel.setStyle(expirySoonHighlight); + search.clear(); + filtered.setPredicate(Item::isExpiringSoon); + showAllBtn[0].setVisible(true); + } else { + expiringSoonLabel.setStyle(expirySoonStyle); + filtered.setPredicate(item -> true); + showAllBtn[0].setVisible(false); + } + }); + search.textProperty().addListener((obs, oldVal, newVal) -> { String lower = (newVal == null) ? "" : newVal.trim().toLowerCase(); lowStockActive[0] = false; + expiringSoonActive[0] = false; lowStockLabel.setStyle(lowStockNormal); + expiringSoonLabel.setStyle(expirySoonStyle); showAllBtn[0].setVisible(false); if (lower.isEmpty()) { @@ -123,6 +157,8 @@ protected void updateItem(Item item, boolean empty) { setStyle(""); } else if (item.isLowStock()) { setStyle("-fx-background-color: #ffcccc;"); + } else if (item.isExpiringSoon()) { + setStyle("-fx-background-color: #ffe0b2;"); } else if (item.getQuantity() <= 10) { setStyle("-fx-background-color: #fff7cc;"); } else { @@ -156,6 +192,23 @@ protected void updateItem(Item item, boolean empty) { colQty.setMaxWidth(90); colQty.setMinWidth(70); + TableColumn colPrice = new TableColumn<>("Price"); + colPrice.setCellValueFactory(cd -> new javafx.beans.property.SimpleStringProperty( + cd.getValue().getPrice() > 0 ? String.format("$%.2f", cd.getValue().getPrice()) : "—")); + colPrice.setSortable(true); + colPrice.setMaxWidth(80); + colPrice.setMinWidth(60); + + TableColumn colExpiry = new TableColumn<>("Expiry"); + colExpiry.setCellValueFactory(cd -> { + String exp = cd.getValue().getExpiresAt(); + return new javafx.beans.property.SimpleStringProperty( + (exp == null || exp.isEmpty()) ? "N/A" : exp); + }); + colExpiry.setSortable(true); + colExpiry.setMaxWidth(110); + colExpiry.setMinWidth(90); + TableColumn colAdj = new TableColumn<>(""); colAdj.setSortable(false); colAdj.setMinWidth(80); @@ -187,7 +240,7 @@ protected void updateItem(Void v, boolean empty) { } }); - table.getColumns().addAll(colName, colCategory, colLocation, colQty, colAdj); + table.getColumns().addAll(colName, colCategory, colLocation, colQty, colPrice, colExpiry, colAdj); Button goAdd = new Button("Add Item"); goAdd.setOnAction(e -> stage.getScene().setRoot(createAddItem(stage))); @@ -284,7 +337,9 @@ protected void updateItem(Void v, boolean empty) { showAll.setVisible(false); showAll.setOnAction(e -> { lowStockActive[0] = false; + expiringSoonActive[0] = false; lowStockLabel.setStyle(lowStockNormal); + expiringSoonLabel.setStyle(expirySoonStyle); filtered.setPredicate(item -> true); showAll.setVisible(false); }); @@ -325,6 +380,16 @@ public static Parent createAddItem(Stage stage) { TextField quantity = new TextField(); quantity.setPromptText("Quantity"); + TextField price = new TextField(); + price.setPromptText("e.g. 2.99 (optional)"); + + javafx.scene.control.DatePicker expiry = new javafx.scene.control.DatePicker(); + expiry.setPromptText("Expiry date (optional)"); + expiry.setMaxWidth(Double.MAX_VALUE); + + TextField supplier = new TextField(); + supplier.setPromptText("Supplier name (optional)"); + Label status = new Label(); status.setStyle("-fx-text-fill: red;"); @@ -339,17 +404,39 @@ public static Parent createAddItem(Stage stage) { return; } + double pValue = 0.0; + String pText = price.getText().trim(); + if (!pText.isEmpty()) { + try { + pValue = Double.parseDouble(pText); + if (pValue < 0) { + status.setText("Price must not be negative."); + return; + } + } catch (NumberFormatException ex) { + status.setText("Price must be a valid number (e.g. 2.99)."); + return; + } + } + String error = ItemValidator.validate( name.getText(), category.getValue(), location.getValue(), qValue); if (error != null) { status.setText(error); return; } + + String expiryVal = expiry.getValue() != null ? expiry.getValue().toString() : ""; + String supplierVal = supplier.getText().trim(); + Item newItem = ItemFactory.createItem( name.getText().trim(), category.getValue(), qValue, - location.getValue()); + location.getValue(), + pValue, + expiryVal, + supplierVal); service.addItem(newItem); items.add(newItem); @@ -376,6 +463,12 @@ public static Parent createAddItem(Stage stage) { form.add(location, 1, 2); form.add(new Label("Quantity:"), 0, 3); form.add(quantity, 1, 3); + form.add(new Label("Price ($):"), 0, 4); + form.add(price, 1, 4); + form.add(new Label("Expiry:"), 0, 5); + form.add(expiry, 1, 5); + form.add(new Label("Supplier:"), 0, 6); + form.add(supplier, 1, 6); ColumnConstraints c1 = new ColumnConstraints(); c1.setMinWidth(90); @@ -414,6 +507,21 @@ public static Parent createEditItem(Stage stage, Item item) { TextField quantity = new TextField(String.valueOf(item.getQuantity())); + TextField price = new TextField(item.getPrice() > 0 ? String.format("%.2f", item.getPrice()) : ""); + price.setPromptText("e.g. 2.99 (optional)"); + + javafx.scene.control.DatePicker expiry = new javafx.scene.control.DatePicker(); + if (item.getExpiresAt() != null && !item.getExpiresAt().isEmpty()) { + try { + expiry.setValue(java.time.LocalDate.parse(item.getExpiresAt())); + } catch (Exception ignored) { + } + } + expiry.setMaxWidth(Double.MAX_VALUE); + + TextField supplier = new TextField(item.getSupplier() != null ? item.getSupplier() : ""); + supplier.setPromptText("Supplier name (optional)"); + Label status = new Label(); status.setStyle("-fx-text-fill: red;"); @@ -430,6 +538,22 @@ public static Parent createEditItem(Stage stage, Item item) { return; } + double pValue = 0.0; + String pText = price.getText().trim(); + if (!pText.isEmpty()) { + try { + pValue = Double.parseDouble(pText); + if (pValue < 0) { + status.setText("Price must not be negative."); + return; + } + } catch (NumberFormatException ex) { + status.setStyle("-fx-text-fill: red;"); + status.setText("Price must be a valid number (e.g. 2.99)."); + return; + } + } + String error = ItemValidator.validate( name.getText(), category.getValue(), @@ -442,12 +566,18 @@ public static Parent createEditItem(Stage stage, Item item) { return; } + String expiryVal = expiry.getValue() != null ? expiry.getValue().toString() : ""; + String supplierVal = supplier.getText().trim(); + Item updated = new Item( item.getId(), name.getText().trim(), category.getValue(), qValue, - location.getValue()); + location.getValue(), + pValue, + expiryVal, + supplierVal); service.updateItem(updated); @@ -478,6 +608,12 @@ public static Parent createEditItem(Stage stage, Item item) { form.add(location, 1, 2); form.add(new Label("Quantity:"), 0, 3); form.add(quantity, 1, 3); + form.add(new Label("Price ($):"), 0, 4); + form.add(price, 1, 4); + form.add(new Label("Expiry:"), 0, 5); + form.add(expiry, 1, 5); + form.add(new Label("Supplier:"), 0, 6); + form.add(supplier, 1, 6); ColumnConstraints c1 = new ColumnConstraints(); c1.setMinWidth(90); @@ -638,10 +774,47 @@ protected void updateItem(Item item, boolean empty) { manifestCount.setText("Delivery Order: 0 items"); }); + Button addExpiringSoon = new Button("Add Expiring Soon Items"); + addExpiringSoon.setStyle("-fx-background-color: #ff9800; -fx-text-fill: white; -fx-font-weight: bold;"); + addExpiringSoon.setOnAction(e -> { + List expiring = items.stream() + .filter(Item::isExpiringSoon) + .collect(java.util.stream.Collectors.toList()); + if (expiring.isEmpty()) { + addStatus.setStyle("-fx-text-fill: #555;"); + addStatus.setText("No items are expiring soon."); + return; + } + java.util.Set alreadyAdded = new java.util.HashSet<>(); + for (String[] row : manifest) + alreadyAdded.add(row[0]); + int added = 0; + for (Item exp : expiring) { + if (!alreadyAdded.contains(exp.getId())) { + manifest.add(new String[] { + exp.getId(), + exp.getName(), + String.valueOf(exp.getQuantity()), + "10", + String.valueOf(exp.getQuantity() + 10) + }); + added++; + } + } + manifestCount.setText("Delivery Order: " + manifest.size() + " item(s)"); + if (added > 0) { + addStatus.setStyle("-fx-text-fill: green;"); + addStatus.setText(added + " expiring item(s) added to order."); + } else { + addStatus.setStyle("-fx-text-fill: #555;"); + addStatus.setText("All expiring items are already in the order."); + } + }); + Button back = new Button("Back to Dashboard"); back.setOnAction(e -> stage.getScene().setRoot(createDashboard(stage))); - HBox buttons = new HBox(10, processDelivery, clearManifest, back); + HBox buttons = new HBox(10, processDelivery, clearManifest, addExpiringSoon, back); buttons.setPadding(new Insets(10, 0, 0, 0)); VBox root = new VBox(10, title, instructions, pickerRow, currentStockLabel, addStatus, @@ -665,7 +838,7 @@ public static Parent createTransactionHistory(Stage stage, Item item) { ComboBox filterBox = new ComboBox<>(); filterBox.getItems().addAll("ALL", "ADD", "UPDATE", "DELETE", "DELIVERY"); filterBox.setValue("ALL"); - + TableView table = new TableView<>(); TableColumn typeCol = new TableColumn<>("Type"); @@ -686,15 +859,15 @@ public static Parent createTransactionHistory(Stage stage, Item item) { FilteredList filteredTransactions = new FilteredList<>(transactionItems, p -> true); filterBox.setOnAction(e -> { - String selectedType = filterBox.getValue(); + String selectedType = filterBox.getValue(); - if ("ALL".equals(selectedType)) { - filteredTransactions.setPredicate(t -> true); - } else { - filteredTransactions.setPredicate(t -> selectedType.equals(t.getType())); - } + if ("ALL".equals(selectedType)) { + filteredTransactions.setPredicate(t -> true); + } else { + filteredTransactions.setPredicate(t -> selectedType.equals(t.getType())); + } }); - + table.setItems(filteredTransactions); if (historyList.isEmpty()) {