diff --git a/submissions/Ali-Salad/07_final_project_store_manager/README.md b/submissions/Ali-Salad/07_final_project_store_manager/README.md new file mode 100644 index 00000000..68908c8d --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/README.md @@ -0,0 +1,13 @@ +# Store Manager — Final Project + +**Student:** Ali Salad +**Bootcamp:** Python for Everyone +**Assignment:** 07 — Final Project + +## Description + +This project is a command-line Inventory and Staff Management System built in Python 3.10+. +It allows a store owner to manage products and employees directly from the terminal. +The system supports adding, updating, searching, restocking, and removing products, +as well as hiring, updating, and firing staff members. All data is saved to text files +so it persists between sessions. \ No newline at end of file diff --git a/submissions/Ali-Salad/07_final_project_store_manager/data/products.txt b/submissions/Ali-Salad/07_final_project_store_manager/data/products.txt new file mode 100644 index 00000000..16775b5e --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/data/products.txt @@ -0,0 +1,12 @@ +# Store Manager — products data +id|name|category|price|quantity|supplier +# Store Manager — products data +id|name|category|price|quantity|supplier +1|USB-C Cable 2m|Electronics|4.99|30|TechSupply Co. +2|Screen Protector|Electronics|2.50|4|ShieldGlass Ltd. +3|Phone Case iPhone 14|Accessories|8.00|15|CaseWorld +4|Wireless Earbuds|Electronics|35.00|7|SoundPro +5|Power Bank 10000mAh|Electronics|22.99|3|PowerMax +6|Tempered Glass S23|Electronics|3.00|20|ShieldGlass Ltd. +7|Sim Card Tray Tool|Tools|1.25|50|FixIt Store +8|Charging Adapter EU|Accessories|6.50|12|TechSupply Co. \ No newline at end of file diff --git a/submissions/Ali-Salad/07_final_project_store_manager/data/staff.txt b/submissions/Ali-Salad/07_final_project_store_manager/data/staff.txt new file mode 100644 index 00000000..b9c75a2d --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/data/staff.txt @@ -0,0 +1,6 @@ +# Store Manager — staff data +id|name|role|salary|phone +1|Ali Salad|manager|900.00|+252612000001 +2|Faadumo Hassan|cashier|500.00|+252612000002 +3|Cabdi Warsame|stock_clerk|450.00| +4|Hodan Yusuf|supervisor|700.00|+252612000004 \ No newline at end of file diff --git a/submissions/Ali-Salad/07_final_project_store_manager/main.py b/submissions/Ali-Salad/07_final_project_store_manager/main.py new file mode 100644 index 00000000..938f49b5 --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/main.py @@ -0,0 +1,333 @@ +""" +main.py — Store Manager CLI +============================= +A professional inventory and staff management system. +Covers Python concepts from Sections 1–6: + comments, print/input, conditions, loops, functions, + main(), files (UTF-8), try/except, __str__, @dataclass, composition. + +Python 3.10+ (uses X | Y union type hints) + +Run: + cd store_manager + python main.py +""" +from __future__ import annotations +import sys +from models.inventory import Store +from models.staff import Roster, ROLES +from utils.storage import load_products, save_products, load_staff, save_staff +from utils.reports import ( + inventory_summary, low_stock_report, + staff_summary, category_breakdown, +) +from utils.helpers import ask, ask_float, ask_int, confirm + +_BANNER = r""" + ╔══════════════════════════════════════════╗ + ║ STORE MANAGER v1.0 ║ + ║ Inventory & Staff Management System ║ + ╚══════════════════════════════════════════╝ +""" + +_MAIN_MENU = """ + [1] Inventory + [2] Staff + [3] Reports + [0] Save & Quit +""" + +_INV_MENU = """ + ── Inventory ────────────────────── + [1] Add product + [2] List all products + [3] Search by name + [4] Browse by category + [5] Update product + [6] Restock product + [7] Remove product + [0] Back +""" + +_STAFF_MENU = """ + ── Staff ─────────────────────────── + [1] Hire employee + [2] List all staff + [3] Search by name + [4] Browse by role + [5] Update employee + [6] Fire employee + [0] Back +""" + +_REPORTS_MENU = """ + ── Reports ───────────────────────── + [1] Inventory summary + [2] Low stock alert + [3] Category breakdown + [4] Staff summary + [0] Back +""" + + +# ── Inventory sub-menu ───────────────────────────────────────────────────── + +def menu_inventory(store: Store) -> None: + while True: + print(_INV_MENU) + choice = input(" > ").strip() + + if choice == "1": + print("\n ── Add product ──") + name = ask(" Name : ") + category = ask(" Category : ") + price = ask_float(" Price $ : ", min_val=0.01) + quantity = ask_int(" Qty : ", min_val=0) + supplier = ask(" Supplier (optional, press Enter to skip): ", required=False) or None + p = store.add_product(name, category, price, quantity, supplier) + print(f"\n ✓ Added [{p.id}] {p.name}") + + elif choice == "2": + inventory_summary(store) + + elif choice == "3": + query = ask(" Name query: ") + results = store.find_by_name(query) + if not results: + print(" No products found.") + else: + for p in results: + print(f"\n{p}") + + elif choice == "4": + cats = store.categories() + if not cats: + print(" No categories yet.") + continue + print(" Categories:", ", ".join(cats)) + cat = ask(" Category: ") + results = store.find_by_category(cat) + if not results: + print(" No products in that category.") + else: + for p in results: + print(f"\n{p}") + + elif choice == "5": + pid = ask_int(" Product ID: ", min_val=1) + p = store.find_by_id(pid) + if not p: + print(" ✗ Product not found.") + continue + print(f"\n{p}\n") + new_name = ask(f" New name [{p.name}] (Enter to keep): ", required=False) + new_cat = ask(f" New category [{p.category}] (Enter to keep): ", required=False) + new_price_raw = input(f" New price [${p.price:.2f}] (Enter to keep): ").strip() + new_qty_raw = input(f" New qty [{p.quantity}] (Enter to keep): ").strip() + new_supplier = input(f" New supplier [{p.supplier or 'none'}] (Enter to keep, 'none' to clear): ").strip() + + if new_name: + p.name = new_name + if new_cat: + p.category = new_cat + if new_price_raw: + try: + p.price = float(new_price_raw) + except ValueError: + print(" ✗ Invalid price — skipped.") + if new_qty_raw: + try: + p.quantity = int(new_qty_raw) + except ValueError: + print(" ✗ Invalid quantity — skipped.") + if new_supplier.lower() == "none": + p.supplier = None + elif new_supplier: + p.supplier = new_supplier + print(" ✓ Product updated.") + + elif choice == "6": + pid = ask_int(" Product ID: ", min_val=1) + amount = ask_int(" Add how many units: ", min_val=1) + if store.restock(pid, amount): + p = store.find_by_id(pid) + print(f" ✓ {p.name} now has {p.quantity} units.") # type: ignore[union-attr] + else: + print(" ✗ Product not found.") + + elif choice == "7": + pid = ask_int(" Product ID: ", min_val=1) + p = store.find_by_id(pid) + if not p: + print(" ✗ Product not found.") + continue + print(f"\n{p}\n") + if confirm(" Remove this product? (y/n): "): + store.remove_product(pid) + print(" ✓ Product removed.") + else: + print(" Cancelled.") + + elif choice == "0": + break + else: + print(" ✗ Invalid option.") + + +# ── Staff sub-menu ───────────────────────────────────────────────────────── + +def menu_staff(roster: Roster) -> None: + while True: + print(_STAFF_MENU) + choice = input(" > ").strip() + + if choice == "1": + print("\n ── Hire employee ──") + name = ask(" Name : ") + print(f" Roles : {', '.join(sorted(ROLES))}") + role = ask(" Role : ").lower() + if role not in ROLES: + print(f" ✗ '{role}' is not a valid role. Please choose from the list.") + continue + salary = ask_float(" Salary (monthly $): ", min_val=0) + phone = ask(" Phone (optional, Enter to skip): ", required=False) or None + e = roster.hire(name, role, salary, phone) + print(f"\n ✓ Hired [{e.id}] {e.name} as {e.role}") + + elif choice == "2": + staff_summary(roster) + + elif choice == "3": + query = ask(" Name query: ") + results = roster.find_by_name(query) + if not results: + print(" No employees found.") + else: + for e in results: + print(f"\n{e}") + + elif choice == "4": + print(f" Roles: {', '.join(sorted(ROLES))}") + role = ask(" Role: ").lower() + results = roster.by_role(role) + if not results: + print(" No employees with that role.") + else: + for e in results: + print(f"\n{e}") + + elif choice == "5": + eid = ask_int(" Employee ID: ", min_val=1) + e = roster.find_by_id(eid) + if not e: + print(" ✗ Employee not found.") + continue + print(f"\n{e}\n") + new_name = ask(f" New name [{e.name}] (Enter to keep): ", required=False) + print(f" Roles: {', '.join(sorted(ROLES))}") + new_role = ask(f" New role [{e.role}] (Enter to keep): ", required=False).lower() + new_salary_raw = input(f" New salary [${e.salary:,.2f}] (Enter to keep): ").strip() + new_phone = input(f" New phone [{e.phone or 'none'}] (Enter to keep, 'none' to clear): ").strip() + + if new_name: + e.name = new_name + if new_role: + if new_role not in ROLES: + print(f" ✗ '{new_role}' is not valid — role unchanged.") + else: + e.role = new_role + if new_salary_raw: + try: + e.salary = float(new_salary_raw) + except ValueError: + print(" ✗ Invalid salary — skipped.") + if new_phone.lower() == "none": + e.phone = None + elif new_phone: + e.phone = new_phone + print(" ✓ Employee updated.") + + elif choice == "6": + eid = ask_int(" Employee ID: ", min_val=1) + e = roster.find_by_id(eid) + if not e: + print(" ✗ Employee not found.") + continue + print(f"\n{e}\n") + if confirm(" Remove this employee? (y/n): "): + roster.fire(eid) + print(" ✓ Employee removed.") + else: + print(" Cancelled.") + + elif choice == "0": + break + else: + print(" ✗ Invalid option.") + + +# ── Reports sub-menu ─────────────────────────────────────────────────────── + +def menu_reports(store: Store, roster: Roster) -> None: + while True: + print(_REPORTS_MENU) + choice = input(" > ").strip() + + if choice == "1": + inventory_summary(store) + elif choice == "2": + threshold = ask_int(" Low-stock threshold (default 5): ", min_val=1) if \ + input(" Custom threshold? (y/n): ").strip().lower() == "y" else 5 + low_stock_report(store, threshold) + elif choice == "3": + category_breakdown(store) + elif choice == "4": + staff_summary(roster) + elif choice == "0": + break + else: + print(" ✗ Invalid option.") + + +# ── Entry point ──────────────────────────────────────────────────────────── + +def main() -> None: + store = Store() + roster = Roster() + + print(_BANNER) + print(" Loading data...") + try: + load_products(store) + load_staff(roster) + print(f" ✓ {len(store.products)} product(s) and " + f"{len(roster.employees)} employee(s) loaded.\n") + except Exception as e: + print(f" ⚠ Could not load data: {e}") + + while True: + print(_MAIN_MENU) + choice = input(" > ").strip() + + if choice == "1": + menu_inventory(store) + elif choice == "2": + menu_staff(roster) + elif choice == "3": + menu_reports(store, roster) + elif choice == "0": + print("\n Saving data...", end=" ") + try: + save_products(store) + save_staff(roster) + print("✓") + except Exception as e: + print(f"\n ✗ Save failed: {e}") + print(" Goodbye!\n") + sys.exit(0) + else: + print(" ✗ Choose 0–3.") + + +if __name__ == "__main__": + main() diff --git a/submissions/Ali-Salad/07_final_project_store_manager/models/inventory.py b/submissions/Ali-Salad/07_final_project_store_manager/models/inventory.py new file mode 100644 index 00000000..838598c2 --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/models/inventory.py @@ -0,0 +1,93 @@ +""" +models/inventory.py — Product and Store dataclasses. +""" +from __future__ import annotations +from dataclasses import dataclass, field + + +@dataclass +class Product: + id: int + name: str + category: str + price: float + quantity: int + supplier: str | None = None + + def __str__(self) -> str: + supplier_info = f" Supplier : {self.supplier}" if self.supplier else "" + return ( + f" ID : {self.id}\n" + f" Name : {self.name}\n" + f" Category : {self.category}\n" + f" Price : ${self.price:.2f}\n" + f" Qty : {self.quantity}" + + (f"\n{supplier_info}" if supplier_info else "") + ) + + def total_value(self) -> float: + return self.price * self.quantity + + def is_low_stock(self, threshold: int = 5) -> bool: + return self.quantity <= threshold + + +@dataclass +class Store: + products: list[Product] = field(default_factory=list) + _next_id: int = 1 + + def add_product(self, name: str, category: str, price: float, + quantity: int, supplier: str | None = None) -> Product: + p = Product( + id=self._next_id, + name=name, + category=category, + price=price, + quantity=quantity, + supplier=supplier, + ) + self.products.append(p) + self._next_id += 1 + return p + + def find_by_id(self, pid: int) -> Product | None: + for p in self.products: + if p.id == pid: + return p + return None + + def find_by_name(self, query: str) -> list[Product]: + q = query.lower() + return [p for p in self.products if q in p.name.lower()] + + def find_by_category(self, category: str) -> list[Product]: + c = category.lower() + return [p for p in self.products if c in p.category.lower()] + + def remove_product(self, pid: int) -> bool: + for i, p in enumerate(self.products): + if p.id == pid: + self.products.pop(i) + return True + return False + + def restock(self, pid: int, amount: int) -> bool: + p = self.find_by_id(pid) + if p: + p.quantity += amount + return True + return False + + def low_stock_report(self, threshold: int = 5) -> list[Product]: + return [p for p in self.products if p.is_low_stock(threshold)] + + def inventory_value(self) -> float: + return sum(p.total_value() for p in self.products) + + def categories(self) -> list[str]: + return sorted(set(p.category for p in self.products)) + + def sync_ids(self) -> None: + if self.products: + self._next_id = max(p.id for p in self.products) + 1 diff --git a/submissions/Ali-Salad/07_final_project_store_manager/models/staff.py b/submissions/Ali-Salad/07_final_project_store_manager/models/staff.py new file mode 100644 index 00000000..cd366861 --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/models/staff.py @@ -0,0 +1,68 @@ +""" +models/staff.py — Employee and Roster dataclasses. +""" +from __future__ import annotations +from dataclasses import dataclass, field + + +ROLES = {"manager", "cashier", "stock_clerk", "supervisor", "intern"} + + +@dataclass +class Employee: + id: int + name: str + role: str + salary: float + phone: str | None = None + + def __str__(self) -> str: + phone_line = f"\n Phone : {self.phone}" if self.phone else "" + return ( + f" ID : {self.id}\n" + f" Name : {self.name}\n" + f" Role : {self.role}\n" + f" Salary : ${self.salary:,.2f}/mo" + + phone_line + ) + + +@dataclass +class Roster: + employees: list[Employee] = field(default_factory=list) + _next_id: int = 1 + + def hire(self, name: str, role: str, salary: float, + phone: str | None = None) -> Employee: + e = Employee(id=self._next_id, name=name, role=role, + salary=salary, phone=phone) + self.employees.append(e) + self._next_id += 1 + return e + + def find_by_id(self, eid: int) -> Employee | None: + for e in self.employees: + if e.id == eid: + return e + return None + + def find_by_name(self, query: str) -> list[Employee]: + q = query.lower() + return [e for e in self.employees if q in e.name.lower()] + + def fire(self, eid: int) -> bool: + for i, e in enumerate(self.employees): + if e.id == eid: + self.employees.pop(i) + return True + return False + + def total_payroll(self) -> float: + return sum(e.salary for e in self.employees) + + def by_role(self, role: str) -> list[Employee]: + return [e for e in self.employees if e.role == role] + + def sync_ids(self) -> None: + if self.employees: + self._next_id = max(e.id for e in self.employees) + 1 diff --git a/submissions/Ali-Salad/07_final_project_store_manager/utils/helpers.py b/submissions/Ali-Salad/07_final_project_store_manager/utils/helpers.py new file mode 100644 index 00000000..07d77f98 --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/utils/helpers.py @@ -0,0 +1,42 @@ +""" +utils/helpers.py — Safe input helpers shared across menus. +""" +from __future__ import annotations + + +def ask(prompt: str, *, required: bool = True) -> str: + while True: + val = input(prompt).strip() + if val or not required: + return val + print(" ✗ This field cannot be empty.") + + +def ask_float(prompt: str, *, min_val: float = 0.0) -> float: + while True: + raw = input(prompt).strip() + try: + val = float(raw) + if val < min_val: + print(f" ✗ Must be ≥ {min_val}.") + continue + return val + except ValueError: + print(" ✗ Enter a valid number (e.g. 9.99).") + + +def ask_int(prompt: str, *, min_val: int = 0) -> int: + while True: + raw = input(prompt).strip() + try: + val = int(raw) + if val < min_val: + print(f" ✗ Must be ≥ {min_val}.") + continue + return val + except ValueError: + print(" ✗ Enter a whole number.") + + +def confirm(prompt: str = "Are you sure? (y/n): ") -> bool: + return input(prompt).strip().lower() in {"y", "yes"} diff --git a/submissions/Ali-Salad/07_final_project_store_manager/utils/reports.py b/submissions/Ali-Salad/07_final_project_store_manager/utils/reports.py new file mode 100644 index 00000000..5862c30c --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/utils/reports.py @@ -0,0 +1,70 @@ +""" +utils/reports.py — Print formatted reports to the terminal. +""" +from __future__ import annotations +from models.inventory import Store +from models.staff import Roster + +_SEP = "─" * 54 + + +def _header(title: str) -> None: + print(f"\n{_SEP}") + print(f" {title.upper()}") + print(_SEP) + + +def inventory_summary(store: Store) -> None: + _header("inventory summary") + if not store.products: + print(" No products on file.") + return + print(f" {'ID':<5} {'Name':<22} {'Category':<14} {'Qty':>5} {'Price':>8}") + print(" " + "·" * 52) + for p in store.products: + flag = " ⚠ LOW" if p.is_low_stock() else "" + print(f" {p.id:<5} {p.name:<22} {p.category:<14} " + f"{p.quantity:>5} ${p.price:>7.2f}{flag}") + print(" " + "·" * 52) + print(f" Total products : {len(store.products)}") + print(f" Inventory value : ${store.inventory_value():,.2f}") + print(f" Categories : {', '.join(store.categories()) or 'none'}") + + +def low_stock_report(store: Store, threshold: int = 5) -> None: + _header(f"low stock report (≤ {threshold} units)") + items = store.low_stock_report(threshold) + if not items: + print(" All products are sufficiently stocked.") + return + for p in items: + print(f" [{p.id}] {p.name} — {p.quantity} left") + + +def staff_summary(roster: Roster) -> None: + _header("staff summary") + if not roster.employees: + print(" No employees on file.") + return + print(f" {'ID':<5} {'Name':<22} {'Role':<14} {'Salary':>10}") + print(" " + "·" * 52) + for e in roster.employees: + print(f" {e.id:<5} {e.name:<22} {e.role:<14} ${e.salary:>9,.2f}") + print(" " + "·" * 52) + print(f" Total staff : {len(roster.employees)}") + print(f" Monthly payroll: ${roster.total_payroll():,.2f}") + + +def category_breakdown(store: Store) -> None: + _header("category breakdown") + if not store.products: + print(" No products on file.") + return + cats: dict[str, tuple[int, float]] = {} + for p in store.products: + qty, val = cats.get(p.category, (0, 0.0)) + cats[p.category] = (qty + p.quantity, val + p.total_value()) + print(f" {'Category':<20} {'Items':>6} {'Value':>12}") + print(" " + "·" * 40) + for cat, (qty, val) in sorted(cats.items()): + print(f" {cat:<20} {qty:>6} ${val:>11,.2f}") diff --git a/submissions/Ali-Salad/07_final_project_store_manager/utils/storage.py b/submissions/Ali-Salad/07_final_project_store_manager/utils/storage.py new file mode 100644 index 00000000..8c2057c4 --- /dev/null +++ b/submissions/Ali-Salad/07_final_project_store_manager/utils/storage.py @@ -0,0 +1,95 @@ +""" +utils/storage.py — Read / write products.txt and staff.txt (UTF-8 pipe-delimited). + +Products format: + # comment lines ignored + id|name|category|price|quantity|supplier + 1|USB Cable|Electronics|4.99|30|TechSupply Co. + +Staff format: + # comment lines ignored + id|name|role|salary|phone + 1|Ali Salad|manager|850.00|+252612345678 +""" +from __future__ import annotations +from pathlib import Path +from models.inventory import Product, Store +from models.staff import Employee, Roster + +PRODUCTS_FILE = Path("data/products.txt") +STAFF_FILE = Path("data/staff.txt") + + +# ── Products ────────────────────────────────────────────────────────────────── + +def load_products(store: Store) -> None: + if not PRODUCTS_FILE.exists(): + return + store.products.clear() + with PRODUCTS_FILE.open(encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or line.startswith("id|"): + continue + parts = line.split("|") + if len(parts) < 5: + continue + try: + store.products.append(Product( + id=int(parts[0]), + name=parts[1], + category=parts[2], + price=float(parts[3]), + quantity=int(parts[4]), + supplier=parts[5] if len(parts) > 5 and parts[5] else None, + )) + except ValueError: + pass + store.sync_ids() + + +def save_products(store: Store) -> None: + PRODUCTS_FILE.parent.mkdir(parents=True, exist_ok=True) + with PRODUCTS_FILE.open("w", encoding="utf-8") as f: + f.write("# Store Manager — products data\n") + f.write("id|name|category|price|quantity|supplier\n") + for p in store.products: + f.write(f"{p.id}|{p.name}|{p.category}|{p.price}|" + f"{p.quantity}|{p.supplier or ''}\n") + + +# ── Staff ───────────────────────────────────────────────────────────────────── + +def load_staff(roster: Roster) -> None: + if not STAFF_FILE.exists(): + return + roster.employees.clear() + with STAFF_FILE.open(encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or line.startswith("id|"): + continue + parts = line.split("|") + if len(parts) < 4: + continue + try: + roster.employees.append(Employee( + id=int(parts[0]), + name=parts[1], + role=parts[2], + salary=float(parts[3]), + phone=parts[4] if len(parts) > 4 and parts[4] else None, + )) + except ValueError: + pass + roster.sync_ids() + + +def save_staff(roster: Roster) -> None: + STAFF_FILE.parent.mkdir(parents=True, exist_ok=True) + with STAFF_FILE.open("w", encoding="utf-8") as f: + f.write("# Store Manager — staff data\n") + f.write("id|name|role|salary|phone\n") + for e in roster.employees: + f.write(f"{e.id}|{e.name}|{e.role}|{e.salary}|" + f"{e.phone or ''}\n")