-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgui.py
More file actions
1198 lines (1001 loc) · 45 KB
/
Copy pathgui.py
File metadata and controls
1198 lines (1001 loc) · 45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Graphical User Interface module for the PennyPilot application.
This module handles all user interface components and interactions,
including the main application window, login screen, and trip planning interface.
The GUI is built using Tkinter and includes:
- Login/authentication system
- Trip selection and cost breakdown
- Savings calculation and display
- Date selection for trip planning
- Expense breakdown visualization
"""
import tkinter as tk
from tkinter import ttk, messagebox
#new
import os
from PIL import Image, ImageTk
from controllers import get_trips, calculate_savings_goal, handle_update_savings, fetch_trip_expense_breakdown
from database import get_user_savings, create_connection
from tkcalendar import DateEntry
import datetime
import threading
import mysql.connector
from controllers import handle_login
def set_window_size(window, width=400, height=600):
"""
Centers and sets the size of a window on the screen.
Args:
window (tk.Tk or tk.Toplevel): The window to resize
width (int): Desired width of the window
height (int): Desired height of the window
"""
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = (screen_width - width) // 2
y = (screen_height - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}")
def setup_window_closing(window, root=None, cleanup_func=None):
"""
Configures proper window closing behavior with cleanup.
Args:
window (tk.Tk or tk.Toplevel): The window to configure
root (tk.Tk, optional): The root window to destroy
cleanup_func (callable, optional): Function to call before closing
"""
def on_closing():
try:
if cleanup_func:
cleanup_func()
if root:
root.destroy()
window.destroy()
except tk.TclError:
# Window was already destroyed, ignore the error
pass
window.protocol("WM_DELETE_WINDOW", on_closing)
def generate_future_dates():
"""
Generates a list of dates for the next 30 days.
Used for trip planning and date selection.
Returns:
list: List of datetime.date objects for the next 30 days
"""
today = datetime.date.today()
future_dates = []
for i in range(1, 31):
future_dates.append(today + datetime.timedelta(days=i))
return future_dates
class PennyPilotApp:
"""
Main application class that manages the PennyPilot GUI.
This class handles all user interface components and their interactions.
Attributes:
root (tk.Tk): The main application window
connector (mysql.connector): Database connection object
trips (list): List of available trips
trip_var (tk.StringVar): Variable for selected trip
calculation_ready (bool): Flag indicating if savings have been calculated
last_calculated_data (dict): Stores the last successful calculation results
"""
def on_trip_selected(self, event):
"""
Handles the trip selection event.
Updates the expense breakdown when a new trip is selected.
Args:
event: The selection event
"""
selected = self.trip_var.get()
if selected:
location = selected.split(" - ")[0]
self.update_expense_breakdown(location)
#new
def back_to_login(self):
"""
Returns the user to the login screen.
"""
self.root.withdraw() # Hide main window
from gui import show_login_window
show_login_window(show_main_app, self.root)
def __init__(self, root, username):
"""
Initializes the PennyPilot application GUI.
Args:
root (tk.Tk): The main application window
"""
self.root = root
self.savings_result = {}
self.username = username
self.root.title("Penny Pilot - Trip Savings")
self.connector = None
# Set window size
set_window_size(root)
# Set up window closing
setup_window_closing(root, cleanup_func=self.on_closing)
self.display_username()
# Initialize trip selection
self.trip_var = tk.StringVar()
success, result = get_trips()
if success:
self.trips = result
else:
self.trips = []
messagebox.showerror("Database Error", result)
# Create trip selection dropdown
self.trip_dropdown = ttk.Combobox(root, textvariable=self.trip_var, state="readonly", width=24)
self.trip_dropdown["values"] = [f"{trip[0]} - ${trip[1]:.2f}" for trip in self.trips]
self.trip_dropdown.pack(pady=5)
# Bind selection event to update expense breakdown
self.trip_dropdown.bind('<<ComboboxSelected>>', self.on_trip_selected)
# Create savings input field with placeholder
self.int_input = tk.Entry(root, fg='grey', width=24)
self.int_input.insert(0, "Already Saved")
self.int_input.pack(pady=5)
# Set up placeholder behavior
self.int_input.bind("<FocusIn>", self.clear_placeholder)
self.int_input.bind("<FocusOut>", self.add_placeholder)
# Create date selection widget
self.create_date_dropdown()
# Initialize calculation state
self.calculation_ready = False
self.last_calculated_data = {}
# Create calculate button
self.calc_btn = tk.Button(root, text="Calculate Savings Goal", command=self.calculate)
self.calc_btn.pack(pady=5)
# Create temporary message label (initially empty)
self.temp_message_label = tk.Label(root, text="", fg="green", font=("Arial", 10))
self.temp_message_label.pack(pady=2)
# Create result display
self.result_label = tk.Label(root, text="", font=("Arial", 12))
self.result_label.pack(pady=5)
# Create savings breakdown table
self.savings_table = ttk.Treeview(root, columns=("Period", "Amount"), show="headings", height=3)
self.savings_table.heading("Period", text="Period")
self.savings_table.heading("Amount", text="Amount")
self.savings_table.pack(pady=10)
# Initialize savings table with default values
self.savings_table.insert("", "end", iid="month", values=("Monthly", "—"))
self.savings_table.insert("", "end", iid="week", values=("Weekly", "—"))
self.savings_table.insert("", "end", iid="day", values=("Daily", "—"))
# Create expense breakdown table
self.expense_breakdown_table = ttk.Treeview(root, columns=("Category", "Estimated Cost"), show="headings", height=7)
self.expense_breakdown_table.heading("Category", text="Category")
self.expense_breakdown_table.heading("Estimated Cost", text="Estimated Cost")
self.expense_breakdown_table.pack(pady=10)
# Initialize expense table with default values
self.expense_breakdown_table.insert("", "end", iid="travelto", values=("Travel To", "—"))
self.expense_breakdown_table.insert("", "end", iid="travelthere", values=("Travel There", "—"))
self.expense_breakdown_table.insert("", "end", iid="food", values=("Food", "—"))
self.expense_breakdown_table.insert("", "end", iid="housing", values=("Housing", "—"))
self.expense_breakdown_table.insert("", "end", iid="school", values=("School", "—"))
self.expense_breakdown_table.insert("", "end", iid="misc", values=("Misc", "—"))
self.expense_breakdown_table.insert("", "end", iid="total", values=("Total", "—"))
# Create confirm button
self.confirm_btn = tk.Button(
root,
text="Confirm Trip Destination",bg="green", fg="white", activebackground="darkgreen",
command=self.handle_confirm_click
)
self.confirm_btn.pack(pady=10)
self.confirm_btn.pack(ipadx=10)
self.confirm_btn.pack(ipady=15)
#new
# Back to login button
self.back_btn = tk.Button(self.root, text="Back to Login", command=self.back_to_login, bg="green", fg="white", activebackground="darkgreen")
self.back_btn.pack(pady=10)
def display_username(self):
"""
Displays a welcome message to the user.
"""
user = self.username
greeting = tk.Label(self.root, text=f"Hello {user}! Welcome to PennyPilot", font=("Arial", 14))
greeting.pack(pady=10)
def create_date_dropdown(self):
"""
Creates a date selection widget using tkcalendar.
"""
self.date_entry = DateEntry(
self.root,
width=21,
background='darkblue',
foreground='white',
borderwidth=2,
year=datetime.datetime.now().year,
month=datetime.datetime.now().month,
day=datetime.datetime.now().day
)
self.date_entry.pack(pady=5)
def calculate(self):
"""
Initiates the savings calculation process.
Validates input and starts calculation in a background thread.
"""
selected = self.trip_var.get()
if not selected:
return
username = selected.split(" - ")[0]
trip = next(t for t in self.trips if t[0] == username)
# Get selected date
date_str = self.date_entry.get_date().strftime("%Y-%m-%d")
# Get and validate savings input
saved_text = self.int_input.get()
try:
already_saved = int(saved_text) if saved_text != "Already Saved" else 0
except ValueError:
messagebox.showerror("Input Error", "Please enter a valid integer for 'Already Saved'")
return
# Show temporary message
self.show_temporary_message("Updated Goal")
# Start calculation in background thread
threading.Thread(target=self.calculate_in_background, args=(trip, date_str, already_saved)).start()
def calculate_in_background(self, trip, date_str, already_saved):
"""
Performs savings calculation in a background thread.
Updates the UI with calculation results.
Args:
trip (tuple): Selected trip data (destination, cost)
date_str (str): Selected departure date
already_saved (int): Amount already saved
"""
success, result = calculate_savings_goal(trip[1], date_str, already_saved)
if success:
self.result_label.config(text="")
# Update savings table
self.savings_table.item("month", values=("Monthly", f"${result['savings_per_month']}"))
self.savings_table.item("week", values=("Weekly", f"${result['savings_per_week']}"))
self.savings_table.item("day", values=("Daily", f"${result['savings_per_day']}"))
self.savings_result = result
self.update_expense_breakdown(trip[0])
else:
messagebox.showerror("Error", result)
self.calculation_ready = True
self.last_calculated_data = {
"trip_name": trip[0],
"total_cost": trip[1],
"savings": result,
"end_date": date_str,
"progress": round(min((already_saved / trip[1]) * 100, 100), 2)
}
self.update_expense_breakdown(trip[0])
def update_expense_breakdown(self, location):
"""
Updates the expense breakdown table with data for the selected location.
Args:
location (str): The selected trip location
"""
success, data = fetch_trip_expense_breakdown(location)
print("DEBUG - raw_data:", data, "| type:", type(data))
categories = ["Travel To", "Travel There", "Food", "Housing", "School", "Misc"]
if success and data:
total = 0
for cat, val in zip(categories, data):
self.expense_breakdown_table.item(cat.lower().replace(" ", ""), values=(cat, f"${val:,.2f}"))
total += val
self.expense_breakdown_table.item("total", values=("Total", f"${total:,.2f}"))
else:
for cat in categories:
self.expense_breakdown_table.item(cat.lower().replace(" ", ""), values=(cat, "—"))
self.expense_breakdown_table.item("total", values=("Total", "—"))
def on_closing(self):
"""
Handles cleanup when the application window is closed.
Closes database connections and performs other cleanup tasks.
"""
if self.connector:
try:
self.connector.close()
print("Database connection closed")
except Exception as e:
print(f"Error closing database connection: {e}")
def create_connection(self):
"""
Establishes a connection to the MySQL database.
Returns:
mysql.connector: Database connection object or None if connection fails
"""
import config as myconfig
config = myconfig.Config.dbinfo().copy()
try:
self.connector = mysql.connector.Connect(**config)
return self.connector
except mysql.connector.Error as err:
print(f"Error connecting to database: {err}")
return None
def clear_placeholder(self, event):
"""
Clears the placeholder text when the input field is focused.
Args:
event: The focus event
"""
if self.int_input.get() == "Already Saved":
self.int_input.delete(0, tk.END)
self.int_input.config(fg='black')
def add_placeholder(self, event):
"""
Restores the placeholder text if the input field is empty.
Args:
event: The focus event
"""
current_value = self.int_input.get()
# Only restore placeholder if the field is empty or contains "Already Saved"
if not current_value or current_value == "Already Saved":
self.int_input.insert(0, "Already Saved")
self.int_input.config(fg='grey')
def handle_confirm_click(self):
if not self.calculation_ready:
messagebox.showerror("Error", "Please calculate trip savings before confirming.")
return
selected = self.trip_var.get()
if not selected:
messagebox.showerror("Error", "No trip selected")
return
location = selected.split(" - ")[0]
# Get money saved
saved_text = self.int_input.get()
try:
money_saved = int(saved_text) if saved_text != "Already Saved" else 0
except ValueError:
messagebox.showerror("Error", "Please enter a valid integer for money saved")
return
try:
conn = self.create_connection()
if conn is None:
messagebox.showerror("Error", "Failed to connect to database")
return
cursor = conn.cursor()
cursor.execute("SELECT location FROM tripDestination WHERE location = %s", (location,))
if not cursor.fetchone():
messagebox.showerror("Error", f"Invalid destination: {location}")
return
cursor.execute("DELETE FROM trip WHERE userName = %s", (self.username,))
date_start = datetime.datetime.now().date()
date_selected = self.date_entry.get_date()
cursor.execute("""
INSERT INTO trip (userName, location, dateStart, dateSelected, moneySaved)
VALUES (%s, %s, %s, %s, %s)
""", (self.username, location, date_start, date_selected, money_saved))
conn.commit()
cursor.execute("""
SELECT
t.location,
td.university,
t.dateStart,
t.dateSelected,
t.moneySaved,
p.Travelto,
p.Travelthere,
p.Food,
p.Housing,
p.School,
p.Misc
FROM trip t
JOIN tripDestination td ON t.location = td.location
JOIN prices p ON t.location = p.location
WHERE t.userName = %s
""", (self.username,))
row = cursor.fetchone()
if not row:
messagebox.showerror("Error", "No trip data found for user.")
return
trip_data = {
"trip_name": row[0],
"university": row[1],
"date_start": row[2],
"date_selected": row[3],
"money_saved": row[4],
"prices": {
"Travelto": row[5],
"Travelthere": row[6],
"Food": row[7],
"Housing": row[8],
"School": row[9],
"Misc": row[10]
},
"savings": self.savings_result,
"progress": self.last_calculated_data.get("progress", 0)
}
trip_data["main_window"] = self.root
show_progress_window(trip_data)
except Exception as e:
messagebox.showerror("Error", f"Failed to confirm trip: {str(e)}")
finally:
if conn:
conn.close()
def show_temporary_message(self, message):
"""
Shows a temporary message that disappears after 3 seconds.
Args:
message (str): The message to display
"""
# Update the label with the message
self.temp_message_label.config(text=message)
# Schedule the message to disappear after 3 seconds
self.root.after(3000, self.clear_temporary_message)
def clear_temporary_message(self):
"""
Clears the temporary message.
"""
self.temp_message_label.config(text="")
def show_main_app(root, username):
"""
Initializes and displays the main application window.
Args:
root (tk.Tk): The root window
username (str): The logged in user's username
"""
# Check if user has existing trip and get saved amount
try:
conn = create_connection()
if conn:
cursor = conn.cursor()
cursor.execute("SELECT moneySaved FROM trip WHERE userName = %s", (username,))
result = cursor.fetchone()
cursor.close()
conn.close()
app = PennyPilotApp(root, username)
# If there's an existing saved amount, populate it
if result and result[0] is not None:
app.int_input.delete(0, tk.END)
app.int_input.insert(0, str(result[0]))
app.int_input.config(fg='black')
except Exception as e:
print(f"Error fetching saved amount: {e}")
app = PennyPilotApp(root, username)
def show_login_window(start_main_app_callback, root):
"""
Displays the login window and handles authentication.
Args:
start_main_app_callback (callable): Function to call after successful login
root (tk.Tk): The root window
"""
import tkinter as tk
from tkinter import messagebox
from controllers import handle_login
from database import create_connection
import datetime
# Create login window
login_window = tk.Toplevel(root)
login_window.title("Login")
#new
login_window.configure(bg="#f5f5f5")
# Set window size
set_window_size(login_window)
# Set up window closing
setup_window_closing(login_window, root)
# Create main frame
main_frame = tk.Frame(login_window)
main_frame.pack(expand=True, fill='both', padx=20, pady=20)
# Add title
title_label = tk.Label(main_frame, text="PennyPilot", font=("Arial", 16, "bold"))
title_label.pack(pady=(0, 20))
# Create login form
login_frame = tk.Frame(main_frame)
login_frame.pack(expand=True)
# Username field
username_entry = tk.Entry(login_frame, fg='grey')
username_entry.insert(0, "Username")
username_entry.grid(row=0, column=0, padx=10, pady=10)
def clear_username_placeholder(event):
if username_entry.get() == "Username":
username_entry.delete(0, tk.END)
username_entry.config(fg='black')
def add_username_placeholder(event):
if not username_entry.get():
username_entry.insert(0, "Username")
username_entry.config(fg='grey')
username_entry.bind("<FocusIn>", clear_username_placeholder)
username_entry.bind("<FocusOut>", add_username_placeholder)
# Password field
password_entry = tk.Entry(login_frame, fg='grey')
password_entry.insert(0, "Password")
password_entry.grid(row=1, column=0, padx=10, pady=10)
def clear_password_placeholder(event):
if password_entry.get() == "Password":
password_entry.delete(0, tk.END)
password_entry.config(fg='black', show='*')
def add_password_placeholder(event):
if not password_entry.get():
password_entry.insert(0, "Password")
password_entry.config(fg='grey', show='')
password_entry.bind("<FocusIn>", clear_password_placeholder)
password_entry.bind("<FocusOut>", add_password_placeholder)
def attempt_login():
"""
Attempts to authenticate the user with provided credentials.
"""
username = username_entry.get() if username_entry.get() != "Username" else ""
password = password_entry.get() if password_entry.get() != "Password" else ""
if handle_login(username, password):
# Check if user has an existing trip
try:
conn = create_connection()
if conn:
cursor = conn.cursor()
cursor.execute("""
SELECT
t.location,
td.university,
t.dateStart,
t.dateSelected,
t.moneySaved,
p.Travelto,
p.Travelthere,
p.Food,
p.Housing,
p.School,
p.Misc
FROM trip t
JOIN tripDestination td ON t.location = td.location
JOIN prices p ON t.location = p.location
WHERE t.userName = %s
""", (username,))
trip_data = cursor.fetchone()
cursor.close()
conn.close()
if trip_data:
# Calculate total cost
total_cost = sum([trip_data[5], trip_data[6], trip_data[7],
trip_data[8], trip_data[9], trip_data[10]])
# Calculate daily savings based on remaining time and money
days_remaining = (trip_data[3] - datetime.datetime.now().date()).days
money_saved = trip_data[4]
remaining_amount = max(0, total_cost - money_saved) # Ensure non-negative
if days_remaining > 0:
daily_amount = min(remaining_amount / days_remaining, remaining_amount) # Cap at remaining amount
savings_info = {
"savings_per_day": daily_amount,
"savings_per_week": min(daily_amount * 7, remaining_amount),
"savings_per_month": min(daily_amount * 30, remaining_amount)
}
else:
# If past the target date, set all values to remaining amount
savings_info = {
"savings_per_day": remaining_amount,
"savings_per_week": remaining_amount,
"savings_per_month": remaining_amount
}
# User has an existing trip, prepare data for progress window
trip_info = {
"trip_name": trip_data[0],
"university": trip_data[1],
"date_start": trip_data[2],
"date_selected": trip_data[3],
"money_saved": trip_data[4],
"prices": {
"Travelto": trip_data[5],
"Travelthere": trip_data[6],
"Food": trip_data[7],
"Housing": trip_data[8],
"School": trip_data[9],
"Misc": trip_data[10]
},
"savings": savings_info,
"progress": round(min((trip_data[4] / total_cost) * 100, 100), 2),
"main_window": root,
"userName": username # Add username to trip data
}
login_window.destroy()
show_progress_window(trip_info)
return
# If no trip found or error occurred, proceed to main app
login_window.destroy()
root.deiconify()
start_main_app_callback(root, username)
except Exception as e:
print(f"Error checking for existing trip: {e}")
# If there's an error, fall back to main app
login_window.destroy()
root.deiconify()
start_main_app_callback(root, username)
else:
messagebox.showerror("Login Failed", "Invalid username or password")
# Add Enter key binding to trigger login
password_entry.bind("<Return>", lambda event: attempt_login())
# Login button
#new
login_button = tk.Button(main_frame, text="Login", command=attempt_login, bg="green", fg="white", activebackground="darkgreen")
login_button.pack(pady=10)
# Create Account button
#new
create_account_button = tk.Button(main_frame, text="Create Account", bg="green", fg="white", activebackground="darkgreen",
command=lambda: show_create_account_window(login_window))
create_account_button.pack(pady=10)
#new
# Add images to the login window
try:
base_dir = os.path.dirname(__file__)
image_dir = os.path.join(base_dir, "images")
earth_img_path = os.path.join(image_dir, "earth.png")
student_img_path = os.path.join(image_dir, "student.png")
earth_img = Image.open(earth_img_path).convert("RGBA").resize((95, 95), Image.Resampling.LANCZOS)
student_img = Image.open(student_img_path).convert("RGBA").resize((100, 250), Image.Resampling.LANCZOS)
earth_photo = ImageTk.PhotoImage(earth_img)
student_photo = ImageTk.PhotoImage(student_img)
earth_label = tk.Label(login_window, image=earth_photo, borderwidth=0, highlightthickness=0)
earth_label.image = earth_photo
earth_label.place(x=10, y=10)
student_label = tk.Label(login_window, image=student_photo,borderwidth=0, highlightthickness=0)
student_label.image = student_photo
student_label.place(relx=1.0, rely=1.0, anchor="se", x=-5, y=-5)
except Exception as e:
print("Image display error:", e)
def show_create_account_window(login_window):
"""
Displays the account creation window.
Args:
login_window (tk.Toplevel): The parent login window
"""
import tkinter as tk
from tkinter import messagebox
from database import create_connection
# Hide login window
login_window.withdraw()
# Create account window
create_window = tk.Toplevel(login_window)
create_window.title("Create Account")
# Set window size
set_window_size(create_window)
def on_closing():
create_window.destroy()
login_window.deiconify()
create_window.protocol("WM_DELETE_WINDOW", on_closing)
# Create main frame
main_frame = tk.Frame(create_window)
main_frame.pack(expand=True, fill='both', padx=20, pady=20)
# Add title
title_label = tk.Label(main_frame, text="Create Account", font=("Arial", 16, "bold"))
title_label.pack(pady=(0, 20))
# Create form
form_frame = tk.Frame(main_frame)
form_frame.pack(expand=True)
# Username field
username_label = tk.Label(form_frame, text="Username:", anchor='w')
username_label.grid(row=0, column=0, padx=10, pady=5, sticky='w')
username_entry = tk.Entry(form_frame)
username_entry.grid(row=0, column=1, padx=10, pady=5)
# Password field
password_label = tk.Label(form_frame, text="Password:", anchor='w')
password_label.grid(row=1, column=0, padx=10, pady=5, sticky='w')
password_entry = tk.Entry(form_frame, show='*')
password_entry.grid(row=1, column=1, padx=10, pady=5)
# Confirm password field
confirm_password_label = tk.Label(form_frame, text="Confirm Password:", anchor='w')
confirm_password_label.grid(row=2, column=0, padx=10, pady=5, sticky='w')
confirm_password_entry = tk.Entry(form_frame, show='*')
confirm_password_entry.grid(row=2, column=1, padx=10, pady=5)
# First name field
first_name_label = tk.Label(form_frame, text="First Name:", anchor='w')
first_name_label.grid(row=3, column=0, padx=10, pady=5, sticky='w')
first_name_entry = tk.Entry(form_frame)
first_name_entry.grid(row=3, column=1, padx=10, pady=5)
# Last name field
last_name_label = tk.Label(form_frame, text="Last Name:", anchor='w')
last_name_label.grid(row=4, column=0, padx=10, pady=5, sticky='w')
last_name_entry = tk.Entry(form_frame)
last_name_entry.grid(row=4, column=1, padx=10, pady=5)
# Email field
email_label = tk.Label(form_frame, text="NAU Email:", anchor='w')
email_label.grid(row=5, column=0, padx=10, pady=5, sticky='w')
email_entry = tk.Entry(form_frame)
email_entry.grid(row=5, column=1, padx=10, pady=5)
def create_account():
"""
Attempts to create a new user account with provided information.
Validates input and handles database operations.
"""
# Get input values
username = username_entry.get()
password = password_entry.get()
confirm_password = confirm_password_entry.get()
first_name = first_name_entry.get()
last_name = last_name_entry.get()
email = email_entry.get()
# Validate required fields
if not all([username, password, confirm_password, first_name, last_name, email]):
messagebox.showerror("Error", "All fields are required")
return
# Validate password match
if password != confirm_password:
messagebox.showerror("Error", "Passwords do not match")
return
# Validate email format
if '@nau.edu' not in email:
messagebox.showerror("Error", "Please use a valid NAU email address")
return
try:
# Connect to database
conn = create_connection()
if conn is None:
messagebox.showerror("Error", "Failed to connect to database")
return
cursor = conn.cursor()
# Check for existing username
cursor.execute("SELECT * FROM userProfile WHERE userName = %s", (username,))
if cursor.fetchone():
messagebox.showerror("Error", "Username already exists")
return
# Check for existing email
cursor.execute("SELECT * FROM userProfile WHERE nauEmail = %s", (email,))
if cursor.fetchone():
messagebox.showerror("Error", "Email address already registered")
return
# Create new user
cursor.execute("""
INSERT INTO userProfile (userName, passwordHash, firstName, lastName, nauEmail)
VALUES (%s, %s, %s, %s, %s)
""", (username, password, first_name, last_name, email))
conn.commit()
create_window.destroy()
login_window.deiconify()
except Exception as e:
messagebox.showerror("Error", f"Failed to create account: {str(e)}")
finally:
if conn:
conn.close()
# Create account button
create_button = tk.Button(main_frame, text="Create Account", command=create_account)
create_button.pack(pady=10)
# Back to Login button
back_button = tk.Button(main_frame, text="Back to Login", bg="green", fg="white", activebackground="darkgreen",
command=lambda: [create_window.destroy(), login_window.deiconify()])
back_button.pack(pady=10)
def show_progress_window(data):
"""
Displays a window showing the progress of the selected trip with
information pulled from the database. This window includes:
- Real-time savings updates
- Progress tracking
- Savings goals calculation
- Visual feedback through progress bar
Args:
data (dict): Trip data including:
- trip_name (str): Name of the destination
- university (str): Associated university
- date_start (datetime): Trip start date
- date_selected (datetime): Selected future date
- money_saved (float): Current savings amount
- prices (dict): Breakdown of trip costs
- savings (dict): Savings goals per period
- userName (str): Current user's username
"""
import tkinter as tk
from tkinter import ttk, messagebox
import datetime
from controllers import handle_update_savings, calculate_savings_goal
# Hide main window while showing progress
main_window = data.get('main_window')
if main_window:
main_window.withdraw()
# Create and configure progress window
progress_window = tk.Toplevel()
progress_window.title("Trip Progress")
set_window_size(progress_window, width=450, height=600)
progress_window.grab_set() # Make window modal
# Create main frame for better organization and padding
main_frame = tk.Frame(progress_window)
main_frame.pack(expand=True, fill='both', padx=20, pady=20)
# Display trip destination and university headers
tripHeader = tk.Label(
main_frame,
text=f"{data['trip_name']}",
font=("Arial", 16, "bold"),
pady=10
)
tripHeader.pack()
uniHeader = tk.Label(
main_frame,
text=f"{data['university']}",
font=("Arial", 12, "bold"),
pady=10
)
uniHeader.pack()
# Calculate initial savings and total cost
original_saved = data.get('money_saved', 0)
start_date = data.get('date_start')
daily_savings = data.get('savings', {}).get('savings_per_day', 0)
total_cost = sum(data.get('prices', {}).values())
# Calculate accumulated savings based on time passed
if isinstance(start_date, datetime.date):
days_passed = (datetime.datetime.now().date() - start_date).days
remaining_cost = max(0, total_cost - original_saved)
if days_passed > 0:
accumulated_savings = min(daily_savings * days_passed, remaining_cost) # Cap at remaining cost
total_saved = min(original_saved + accumulated_savings, total_cost) # Cap at total cost
else:
total_saved = original_saved
else:
total_saved = original_saved
# Create frame for savings update controls
update_frame = tk.Frame(main_frame)
update_frame.pack(pady=10)
# Create entry widget for new savings amount
savings_var = tk.StringVar(value=str(total_saved))
savings_entry = tk.Entry(update_frame, textvariable=savings_var, width=15)
savings_entry.pack(side=tk.LEFT, padx=5)
# Add status message label for feedback
status_label = tk.Label(
main_frame,
text="",
font=("Arial", 10),
fg="green" # Default to green for success messages
)
status_label.pack(pady=5)
def clear_status():
"""Clears the status message after a delay."""
status_label.config(text="")
def update_savings_display():
"""
Handles the savings update process:
1. Validates the new savings amount
2. Updates the database
3. Recalculates all derived values
4. Updates the UI in real-time
5. Provides visual feedback
"""
try:
# Input validation
new_savings = float(savings_var.get())
if new_savings < 0:
status_label.config(text="Error: Savings amount cannot be negative", fg="red")