CodingNic

Final Capstone Project

Feature Integration (Tasks and Expenses in GUI)

Final Capstone Project 75 min read

Feature Integration (Tasks and Expenses in GUI)

Feature Integration (Tasks and Expenses in GUI)

Now you will complete the core functionality of your application.

You will fill the two empty panels on your dashboard with:

  • full task management (add, complete, delete)
  • full expense tracking (add, view total)

You will also wire the stat cards so they update automatically whenever data changes.

At the end of this lesson, your app will be fully interactive.


Goal of This Lesson

By the end of this lesson, you will:

  • build the task section inside the Tasks panel
  • build the expense section inside the Expenses panel
  • refresh the stat cards automatically when data changes
  • handle user input safely

Step 1: Import Services

Open:

text
app/ui/app_ui.py

Add to the imports at the top:

python
from app.services.task_service import (
    add_task, get_tasks, complete_task, delete_task
)
from app.services.expense_service import (
    add_expense, get_expenses, get_total_expenses
)

Step 2: Build the Task Section

Add this function. It builds the input row, the listbox, and the action buttons inside a parent panel.

python
def build_task_section(parent, user_id, on_change=None):
    # Input row: text field + Add button on the same line
    input_row = tk.Frame(parent, bg=THEME["card"])
    input_row.pack(fill="x", pady=(0, 6))

    task_entry = tk.Entry(
        input_row, font=FONT_LABEL, relief="flat",
        bg="#f9fafb", highlightthickness=1,
        highlightbackground=THEME["border"],
    )
    task_entry.pack(side="left", fill="x", expand=True, ipady=6)

    # Message label for feedback
    message_label = tk.Label(parent, text="", font=FONT_LABEL,
                             bg=THEME["card"], fg=THEME["danger"])
    message_label.pack(anchor="w", pady=(2, 4))

    # Listbox inside a thin border frame
    list_frame = tk.Frame(parent, bg=THEME["border"])
    list_frame.pack(fill="both", expand=True, pady=(0, 8))

    task_list = tk.Listbox(
        list_frame, font=FONT_LABEL, relief="flat", bd=0,
        bg=THEME["card"], fg=THEME["text"],
        selectbackground=THEME["primary"], selectforeground="white",
        activestyle="none", highlightthickness=0,
    )
    task_list.pack(fill="both", expand=True, padx=1, pady=1)

    # Maps each row in the listbox to the task's database id
    task_id_by_row = []

    def load_tasks():
        task_list.delete(0, tk.END)
        task_id_by_row.clear()
        for task in get_tasks(user_id):
            task_id, title, completed = task
            marker = "✓" if completed == 1 else "○"
            task_list.insert(tk.END, f"  {marker}   {title}")
            task_id_by_row.append(task_id)
            if completed == 1:
                task_list.itemconfig(tk.END, fg=THEME["muted"])

    def add_new_task():
        title = task_entry.get().strip()
        if not title:
            message_label.config(text="Enter a task title",
                                 fg=THEME["danger"])
            return
        add_task(user_id, title)
        task_entry.delete(0, tk.END)
        message_label.config(text="Task added", fg=THEME["success"])
        load_tasks()
        if on_change:
            on_change()

    def complete_selected():
        selection = task_list.curselection()
        if not selection:
            message_label.config(text="Select a task first",
                                 fg=THEME["danger"])
            return
        complete_task(task_id_by_row[selection[0]])
        load_tasks()
        if on_change:
            on_change()

    def delete_selected():
        selection = task_list.curselection()
        if not selection:
            message_label.config(text="Select a task first",
                                 fg=THEME["danger"])
            return
        delete_task(task_id_by_row[selection[0]])
        load_tasks()
        if on_change:
            on_change()

    # Add button (next to the input)
    _styled_button(input_row, "Add", add_new_task, "primary").pack(
        side="left", padx=(8, 0), ipady=2)
    task_entry.bind("<Return>", lambda _e: add_new_task())

    # Action buttons row (below the listbox)
    actions = tk.Frame(parent, bg=THEME["card"])
    actions.pack(fill="x")
    _styled_button(actions, "✓ Complete", complete_selected, "success").pack(
        side="left", padx=(0, 6))
    _styled_button(actions, "Delete", delete_selected, "danger").pack(
        side="left")

    load_tasks()

Understanding the on_change Callback

on_change is a function that the section calls whenever data changes.

The dashboard passes refresh_summary as the callback so the stat cards update automatically.

This pattern keeps responsibilities clean:

  • the task section only knows about tasks
  • the dashboard tells the section: “when you change something, call this”

Understanding task_id_by_row

The listbox shows task titles, but actions need the task id.

task_id_by_row[selection[0]] translates “user clicked row 2” into “the database id of that task.”

Without this, you can’t reliably complete or delete the right task.


Step 3: Build the Expense Section

python
def build_expense_section(parent, user_id, on_change=None):
    # Two-column input grid: amount (left) + category (right)
    input_grid = tk.Frame(parent, bg=THEME["card"])
    input_grid.pack(fill="x", pady=(0, 6))
    input_grid.columnconfigure(0, weight=1)
    input_grid.columnconfigure(1, weight=2)

    tk.Label(input_grid, text="Amount", font=FONT_LABEL,
             bg=THEME["card"], fg=THEME["muted"]).grid(row=0, column=0, sticky="w")
    tk.Label(input_grid, text="Category", font=FONT_LABEL,
             bg=THEME["card"], fg=THEME["muted"]).grid(row=0, column=1, sticky="w", padx=(8, 0))

    amount_entry = tk.Entry(
        input_grid, font=FONT_LABEL, relief="flat",
        bg="#f9fafb", highlightthickness=1,
        highlightbackground=THEME["border"],
    )
    amount_entry.grid(row=1, column=0, sticky="ew", ipady=6)

    category_entry = tk.Entry(
        input_grid, font=FONT_LABEL, relief="flat",
        bg="#f9fafb", highlightthickness=1,
        highlightbackground=THEME["border"],
    )
    category_entry.grid(row=1, column=1, sticky="ew", padx=(8, 0), ipady=6)

    message_label = tk.Label(parent, text="", font=FONT_LABEL,
                             bg=THEME["card"], fg=THEME["danger"])
    message_label.pack(anchor="w", pady=(2, 4))

    list_frame = tk.Frame(parent, bg=THEME["border"])
    list_frame.pack(fill="both", expand=True, pady=(0, 8))

    expense_list = tk.Listbox(
        list_frame, font=FONT_LABEL, relief="flat", bd=0,
        bg=THEME["card"], fg=THEME["text"],
        selectbackground=THEME["primary"], selectforeground="white",
        activestyle="none", highlightthickness=0,
    )
    expense_list.pack(fill="both", expand=True, padx=1, pady=1)

    # Running-total label inside the panel
    total_label = tk.Label(
        parent, text="Total: $0", font=FONT_BTN,
        bg=THEME["card"], fg=THEME["text"], anchor="e",
    )
    total_label.pack(fill="x", pady=(0, 6))

    def load_expenses():
        expense_list.delete(0, tk.END)
        for exp in get_expenses(user_id):
            _, amount, category = exp
            line = f"  {category:<20}  ${amount:g}"
            expense_list.insert(tk.END, line)
        total = get_total_expenses(user_id)
        total_label.config(text=f"Total: ${total:g}")

    def add_new_expense():
        try:
            amount = float(amount_entry.get())
        except ValueError:
            message_label.config(text="Amount must be a number",
                                 fg=THEME["danger"])
            return

        category = category_entry.get().strip()
        if not category:
            message_label.config(text="Enter a category",
                                 fg=THEME["danger"])
            return

        add_expense(user_id, amount, category)
        amount_entry.delete(0, tk.END)
        category_entry.delete(0, tk.END)
        message_label.config(text="Expense added", fg=THEME["success"])
        load_expenses()
        if on_change:
            on_change()

    _styled_button(parent, "Add Expense", add_new_expense, "primary").pack(
        anchor="w", ipady=2)

    amount_entry.bind("<Return>", lambda _e: add_new_expense())
    category_entry.bind("<Return>", lambda _e: add_new_expense())

    load_expenses()

Step 4: Connect the Sections to the Dashboard

Open show_dashboard from the previous lesson.

Find the part where you create the panels:

python
task_panel = _build_panel(panels, title="Tasks")
task_panel["card"].grid(row=0, column=0, sticky="nsew", padx=(0, 8))

expense_panel = _build_panel(panels, title="Expenses")
expense_panel["card"].grid(row=0, column=1, sticky="nsew", padx=(8, 0))

Add calls to fill them with real content:

python
task_panel = _build_panel(panels, title="Tasks")
task_panel["card"].grid(row=0, column=0, sticky="nsew", padx=(0, 8))
build_task_section(task_panel["body"], user_id, on_change=refresh_summary)

expense_panel = _build_panel(panels, title="Expenses")
expense_panel["card"].grid(row=0, column=1, sticky="nsew", padx=(8, 0))
build_expense_section(expense_panel["body"], user_id, on_change=refresh_summary)

Notice: each section is built inside panel["body"], not the panel itself. The body is the inner frame from _build_panel.


Step 5: Run Application

bash
python main.py

What You Can Now Do

After login:

Tasks

  • type a task and click Add (or press Enter)
  • click a task to select it
  • click ✓ Complete to mark it done (✓ marker, grey text)
  • click Delete to remove it

Expenses

  • enter amount and category, click Add Expense
  • see the running total at the bottom of the panel

Stat Cards

  • update automatically every time you add/complete/delete a task or add an expense

Important Behavior

  • data is stored in the database
  • UI updates automatically after actions
  • each user sees their own data
  • feedback messages appear after every action
  • keyboard shortcuts (Enter) work in every input

Why This Step Matters

Your application is now:

  • interactive
  • data-driven
  • user-based
  • persistent

This is a complete working system.


Current Project Structure

text
app/
    services/
        database.py
        user_service.py
        task_service.py
        expense_service.py
        dashboard_service.py
    ui/
        app_ui.py

Summary

You filled the dashboard panels with full task and expense management, and connected them to the stat cards using the on_change callback pattern. Your app is now fully functional.