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:
app/ui/app_ui.py
Add to the imports at the top:
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.
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
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:
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:
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
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
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.