diff --git a/README.md b/README.md index b2588bc51..aebd7cf89 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -My groupmembers are: -- XXXX -- XXXX -- XXXX -- XXXX +# Group Members +- Caycee Harrell +- Edwina Sesay +- Malaya Conell +- Rigel Sliteris +- Abigiya Yohannes +- Aldaberto Gomez - ------------------- Fill in some information about your project under this ------------------ +--- +## Descriptions +The Project is an app that will allow users to input their medications, when it was taken, and any side effects or symptoms they may be experiencing. diff --git a/Sprint 1/MediTrack Main Screen Code b/Sprint 1/MediTrack Main Screen Code new file mode 100644 index 000000000..148898619 --- /dev/null +++ b/Sprint 1/MediTrack Main Screen Code @@ -0,0 +1,110 @@ +import tkinter as tk +import tkinter.font as tkFont +from tkinter import messagebox + +users = {"sample":"123"} + +#Main window configurations +mainWindow = tk.Tk() +mainWindow.title("MediTrack Login form") +mainWindow.geometry("350x450") +mainWindow.configure(bg = '#F0F0F0') +font1 = tkFont.Font(family = "Arial", size = 12, weight = tkFont.NORMAL) + +#Dashboard Window configurations +dashboard = tk.Toplevel(mainWindow) +dashboard.title("MediTrack dashboard") +dashboard.geometry("350x450") +dashboard.configure(bg = '#F0F0F0') +dashboard.withdraw() + +#Sign-in Window +signIn = tk.Toplevel(mainWindow) +signIn.title("Create new User") +signIn.geometry("350x450") +signIn.configure(bg = '#F0F0F0') +signIn.withdraw() + +label = tk.Label(mainWindow, text = "MediTrack", bg= "#F5D5F7", font = font1) +label.place(x= 125,y=0) + +def login(): + username = userEntry.get().strip() + password = passEntry.get().strip() + print("attempted login:", username, password) + + if username in users and users[username] == password: + messagebox.showinfo(title = "Logged in", message= "Successfully logged in!") + dashboard.deiconify() + mainWindow.withdraw() + + else: + messagebox.showinfo(title = "Login Failed", message= "Invalid Username or Password") + +def signUp(): + mainWindow.withdraw() + signIn.deiconify() + +def createUser(): + username = signUserEntry.get().strip() + password = signPassEntry.get().strip() + confirmPass = passRentry.get().strip() + + if username in users: + messagebox.showinfo(title = "Sign up failed", message = "Account with this user already exists") + + elif password != confirmPass: + messagebox.showinfo(title = "Invalid Password", message = "Passwords don't match") + + else: + users[username] = password + print(users) + messagebox.showinfo(title = "User created", message = "Redirecting back to login") + signIn.withdraw() + mainWindow.deiconify() + +#widgets main +userLabel = tk.Label(mainWindow, text = "Username", bg = "#F5D5F7", font = font1) +passLabel = tk.Label(mainWindow, text = "Password", bg = "#F5D5F7", font = font1) +userEntry = tk.Entry(mainWindow) +passEntry = tk.Entry(mainWindow, show = "*") +loginButton = tk.Button(mainWindow, text = "Login", bg = "#F5D5F7", font = font1, command = login) +signUpButton = tk.Button(mainWindow, text = "Sign up", bg = "#F5D5F7", font = font1, command = signUp) + +#widgets sign-in +signUserEntryL = tk.Label(signIn, text = "Enter Username", bg = "#F5D5F7", font = font1) +signPassEntryL = tk.Label(signIn, text = "Enter Password", bg = "#F5D5F7", font = font1) +passRentryL = tk.Label(signIn, text = "Confirm Password", bg = "#F5D5F7", font = font1) +signUserEntry = tk.Entry(signIn) +signPassEntry = tk.Entry(signIn, show = "*") +passRentry = tk.Entry(signIn,show = "*") +createUserButton = tk.Button(signIn, text = "Create User", bg = "#F5D5F7", font = font1, command = createUser) + +#widgets dashboard +dashboardInput = tk.Entry(dashboard) +dashboardInputL = passRentryL = tk.Label(dashboard, text = "Input Medications", bg = "#F5D5F7", font = font1) + + +#positions for labels and buttons +#login +loginButton.place(x = 125, y = 100) +signUpButton.place(x = 125, y = 250) +userLabel.place(x=50, y=150) +userEntry.place(x=130 , y=150) +passLabel.place(x=50, y=190) +passEntry.place(x=130, y= 190) +#signin +signUserEntry.place(x=195, y= 150) +signPassEntry.place(x=195, y= 190) +passRentry.place(x=195,y=230) +signUserEntryL.place(x=30, y=150) +signPassEntryL.place(x=30, y=190) +passRentryL.place(x=30, y=230) +createUserButton.place(x = 130, y= 300) +#dashboard +dashboardInput.place(x=130, y=150) + + + +mainWindow.mainloop() + diff --git a/Sprint 1/Sprint 1 Test Case Recording b/Sprint 1/Sprint 1 Test Case Recording new file mode 100644 index 000000000..1d3c44c29 --- /dev/null +++ b/Sprint 1/Sprint 1 Test Case Recording @@ -0,0 +1 @@ +https://vcu.zoom.us/rec/share/NFEIxuddtujtpLaQX4Scsvssi1sRmw-_CzQKJ-3qdN_KS0dRzinh6_FWEiUNga9U.4hqBf_iIo3qciR-J?startTime=1762015481000 diff --git a/Sprint 1/Sprint 1 Test Cases b/Sprint 1/Sprint 1 Test Cases new file mode 100644 index 000000000..aa2dbe9d1 --- /dev/null +++ b/Sprint 1/Sprint 1 Test Cases @@ -0,0 +1,83 @@ +Use Case 1 - Meditrack Login/Sign-in Page +Test Case ID +TC01 - User attempts to login +Test Objective +The User will log into meditracker and be shown their dashboard +Preconditions +User must have a pre-existing username and password +Test steps +User opens the app +User inputs their username + User inputs their password +User presses login +Test ends +Input Values +Username- ##### +Password- ##### +Expected results +“Successfully logged in” will be displayed, +User will be redirected to a screen displaying an entry to input medications + + + +Test Case ID +TC02 - New user attempts to create an account +Test Objective +The user will attempt to create a new account +Preconditions +A supported browser is being used +Test steps +User selects ‘Create New Account’ +In the ‘E-mail or Phone Number’ field user enters their email or phone number +User creates a username in the ‘Username’ field +User creates a password in the ‘Create Password’ field +User re-enters password in the ‘Confirm Password’ field +Click the ‘Create User’ button +Input Values +Email : ##### +Password : ##### +Re-enter Password : ##### +Expected results +User is taken to the login page and “Account Created!” is displayed + + + +Test Case ID +TC03 - User’s login fails +Test Objective +If an invalid username or password is inputted, the login process should fail. +Preconditions +The entered username and/or password are not registered in the system +Test steps +User inputs their username in the ‘Username’ field +User inputs their password in the ‘Password’ field +Click ‘login’ +Input Values +Username : ##### +Password : ##### +Expected results +The login process will fail, and “Invalid Username or Password” will be displayed + + + +Test Case ID +TC04 - User’s sign in attempt fails/ account already exists +Test Objective +Ensure the system prevents users from creating an account that already exists. +Preconditions +An account must already be made with a username and password +Test steps +User opens the Meditrack sign-up page. +User enters an existing email address or phone number in the “Email or Phone Number” field. +User enters a username already associated with another account +User creates and confirms a password +User clicks the “Create User” button to complete registration. +Input Values +Email: #### (already registered) +Username: #### (already registered) +Password: ##### +Confirm Password: ##### +Expected results +The system displays an error message such as “Account with this user already exists.“ The user remains on the sign-up page, and no new account is created. + + diff --git a/Sprint 1/TestCasesPlaceholder.txt b/Sprint 1/TestCasesPlaceholder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 1/codePlaceHolder.txt b/Sprint 1/codePlaceHolder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 1/videoPlaceholder.txt b/Sprint 1/videoPlaceholder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 2/Sprint 2 Recording b/Sprint 2/Sprint 2 Recording new file mode 100644 index 000000000..8013e771c --- /dev/null +++ b/Sprint 2/Sprint 2 Recording @@ -0,0 +1 @@ +https://vcu.zoom.us/rec/share/QMlyAqaltyCIUfxmdpkLMSzu_f3dxUNJzAfEurDeKJXjgb2YtcwfXPzMQcCdH5mO.ckMbnzbtlpDzTwPD?startTime=1763164710000 diff --git a/Sprint 2/Sprint 2 Test Case b/Sprint 2/Sprint 2 Test Case new file mode 100644 index 000000000..7f37ce907 --- /dev/null +++ b/Sprint 2/Sprint 2 Test Case @@ -0,0 +1,96 @@ +Use Case 2 - <> +Test Case ID +TC01 - The user adds a medication to their list of medicine +Test Objective +The user will add a medicine to their account through the dashboard +Preconditions +The user has already logged into their account +Test steps +The user logs in and goes to their dashboard +The user presses Add Medicine +The user inputs the Medicine +The user presses add medicine +A pop up displaying “Medicine has been added” will show, and the medicine will be displayed in a list on the dashboard +Test case ends +Input Values +Medicine Name- “Ibuprofen” +Dosage- “200 mg” +Time Taken = “8:00 AM” +Symptoms- “Nausea” +Expected results +The medicine will be added and will be shown on a list of medicines + + + +Test Case ID +TC02 - The User records an administered dosage of a specific mediation, and symptoms +Test Objective +Verify that the user can record an administered dosage of a specific medication and note any symptoms after taking it. +Preconditions +The dashboard window is visible and active +Test steps +Navigate to dashboard window +The user input their medications +Click the save or record button +Observe confirmation of successful entry or appearance of the saved data in the log/list +Input Values +Medication name - “ibuprofen +Dosage - “200 mg” +Time Taken = “8:00 AM” +Symptoms - “Nausea” +Expected results +A message box appears stating”Medication record saved successfully: or the data appears in a displayed medication history/log +The input field clears after saving +The saved record is stored in memory(or in a file/database, depending on implementation) + + + +Test Case ID +TC03 - The user removes a medication +Test Objective +To verify that the system correctly removes a medication from the user’s medication list and the display updates accordingly. +Preconditions +The user is logged into their account, the user has at least one medication on their list +Test steps +The user navigates to their Dashboard +The user opens the Medication List +The user selects a specific medication they wish to remove. +The user clicks on the “Remove” or “Delete button” next to the medication +The system will then prompt a confirmation message, “Are you sure you want to remove this medication?” +The user confirms the deletion by clicking “Yes”? +The system displays a success message (“Medication removed successfully”) +The list refreshes to reflect the removal +Input Values +Medication to remove: “Ibuprofen 200 mg” +Confirmation: Yes +Expected results +The selected medication is deleted from the list. +The updated medication list no longer displays the removed medication. +A success message appears confirming removal +No errors or crashes during. + + + +Test Case ID +TC04 - The user attempts to input an medication that is already registered +Test Objective +Verify that the MediTrack system prevents the user from adding a medication entry that has already been recorded +Preconditions +The dashboard window is active, at least one medication record +Test steps +Navigate to the Dashboard window +In the medication input field, enter a medication record identical to an existing one +Click the save or record button +Observe the system's response +Observe the system’s response +Input Values +Medication name - “Ibuprofen” +Dosage - “200 mg” +Time Taken = “8:00 AM” +Symptoms - “Nausea” +Expected results +A message box appears stating: “This medication entry already exists.” +The duplicate record is not saved or added to the log +The existing medication record remains unchanged + + diff --git a/Sprint 2/TestCasesPlaceholder.txt b/Sprint 2/TestCasesPlaceholder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 2/codePlaceHolder.txt b/Sprint 2/codePlaceHolder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 2/meditrackCode.py b/Sprint 2/meditrackCode.py new file mode 100644 index 000000000..9749a222a --- /dev/null +++ b/Sprint 2/meditrackCode.py @@ -0,0 +1,315 @@ +import tkinter as tk +import tkinter.font as tkFont +from tkinter import Listbox, messagebox + +#User database +users = { + "sample": { + "password": "123", + "medications": [ + { + "name": "Aspirin", + "dosage": "100mg", + "time": "8:00 AM", + "symptoms": "Headache" + } + ] + } +} + +current_user = None + +#Main window configurations +mainWindow = tk.Tk() +mainWindow.title("MediTrack Login form") +mainWindow.geometry("350x450") +mainWindow.configure(bg = '#F0F0F0') +font1 = tkFont.Font(family = "Arial", size = 12, weight = tkFont.NORMAL) + +#Dashboard Window configurations +dashboard = tk.Toplevel(mainWindow) +dashboard.title("MediTrack dashboard") +dashboard.geometry("800x450") +dashboard.configure(bg = '#F0F0F0') +dashboard.withdraw() + +#Sign-in Window +signIn = tk.Toplevel(mainWindow) +signIn.title("Create new User") +signIn.geometry("350x450") +signIn.configure(bg = '#F0F0F0') +signIn.withdraw() + + + +''' TODO: Implement history window + CODE: + historyWindow = tk.Toplevel(dashboard) + historyWindow.title("Medication History") + historyWindow.geometry("350x450") + historyWindow.configure(bg = '#F0F0F0') + historyWindow.withdraw() ''' + +label = tk.Label(mainWindow, text = "MediTrack", bg= "#F5D5F7", font = font1) +label.place(x= 125,y=0) + +def login(): + global users + global current_user + username = userEntry.get().strip() + password = passEntry.get().strip() + print("attempted login:", username, password) + + if username in users and users[username]["password"] == password: + current_user = username + messagebox.showinfo(title = "Login Successful", message = f"Welcome back, {username}!") + dashboard.deiconify() + mainWindow.withdraw() + updateMedListbox() + else: + messagebox.showinfo(title = "Login Failed", message = "Invalid username or password") + +def signUp(): + mainWindow.withdraw() + signIn.deiconify() + +def createUser(): + username = signUserEntry.get().strip() + password = signPassEntry.get().strip() + confirmPass = passRentry.get().strip() + + if username in users: + messagebox.showinfo(title = "Sign up failed", message = "Account with this user already exists") + + elif password != confirmPass: + messagebox.showinfo(title = "Invalid Password", message = "Passwords don't match") + + else: + users[username] = password + print(users) + messagebox.showinfo(title = "User created", message = "Redirecting back to login") + signIn.withdraw() + mainWindow.deiconify() + +#adds medication to the list if not already present +def addMedication(): + global current_user + user_medications = users[current_user].setdefault("medications", []) + + med_input = medicationEntry.get().strip() + dosage_input = dosageEntry.get().strip() + time_input = timeEntry.get().strip() + symptoms_input = symptomsEntry.get().strip() + + #input validation + if not med_input: + messagebox.showinfo(title="Missing input", message="Medication cannot be empty!") + return + + if not dosage_input: + messagebox.showinfo(title="Missing input", message="Please enter the dosage") + return + + if not time_input: + messagebox.showinfo(title="Missing input", message="Please enter the time to take the medication") + return + + + med_norm = med_input.casefold() + + #duplicate check + is_duplicate = any(med.get("name", "").strip().casefold() == med_norm + for med in user_medications) + + if is_duplicate: + messagebox.showinfo(title="Duplicate Entry", message=f"{med_input} is already in your medications!") + return + + #add medication to the list + new_med = ( { + "name": med_input, + "dosage": dosage_input, + "time": time_input, + "symptoms": symptoms_input + }) + user_medications.append(new_med) + users[current_user]["medications"] = user_medications + + #refresh listbox + updateMedListbox() + + #clear entries + medicationEntry.delete(0, tk.END) + dosageEntry.delete(0, tk.END) + timeEntry.delete(0, tk.END) + symptomsEntry.delete(0, tk.END) + + #confirmation message + messagebox.showinfo(title = "Medication Added", message = f"{med_input} is added to your medications!") + print(f"[DEBUG] Current medications for {current_user}: {users[current_user]['medications']}") + +#function to remove selected medication from listbox and user data +def onMedSelect(event): + selection = medListbox.curselection() + + if selection and selection[0] != 0: + removeMedButton.place(x=350, y=390, width=200, height=30) + else: + removeMedButton.place_forget() + selection = medListbox.curselection() + + medListbox.bind('<>', onMedSelect) + + +#function to confirm and remove medication +def confirmRemoveMedication(): + global current_user + + selection = medListbox.curselection() + + if not selection or selection[0] == 0: + return + + med_index = selection[0] - 1 + med_name = users[current_user]["medications"][med_index]["name"] + + answer = messagebox.askyesno(title="Confirm Removal", message=f"Are you sure you want to remove {med_name}?") + if answer: + users[current_user]["medications"].pop(med_index) + updateMedListbox() + removeMedButton.place_forget() + + +def updateMedListbox(): + global current_user + medListbox.delete(0, tk.END) + + medListbox.insert(tk.END, "Name | Dose | Time | Symptoms") + medListbox.itemconfig(0, {'fg': 'white', 'bg': 'black'}) + + user_medications = users[current_user].get("medications", []) + for med in user_medications: + #if symptoms is empty, display "None" + symptoms_display = med.get("symptoms", "").strip() or "None" + line = f"{med['name']} | {med['dosage']} | {med['time']} | {symptoms_display}" + medListbox.insert(tk.END, line) + + + +#widgets main login window +userLabel = tk.Label(mainWindow, text = "Username", bg = "#F5D5F7", font = font1) +passLabel = tk.Label(mainWindow, text = "Password", bg = "#F5D5F7", font = font1) +userEntry = tk.Entry(mainWindow) +passEntry = tk.Entry(mainWindow, show = "*") +loginButton = tk.Button(mainWindow, text = "Login", bg = "#F5D5F7", font = font1, command = login) +signUpButton = tk.Button(mainWindow, text = "Sign up", bg = "#F5D5F7", font = font1, command = signUp) + +#widgets create user window +signUserEntryL = tk.Label(signIn, text = "Enter Username", bg = "#F5D5F7", font = font1) +signPassEntryL = tk.Label(signIn, text = "Enter Password", bg = "#F5D5F7", font = font1) +passRentryL = tk.Label(signIn, text = "Confirm Password", bg = "#F5D5F7", font = font1) +signUserEntry = tk.Entry(signIn) +signPassEntry = tk.Entry(signIn, show = "*") +passRentry = tk.Entry(signIn,show = "*") +createUserButton = tk.Button(signIn, text = "Create User", bg = "#F5D5F7", font = font1, command = createUser) +backButton = tk.Button(signIn, text="Go Back", bg="#F5D5F7", font=font1, command=lambda: [signIn.withdraw(), mainWindow.deiconify()]) + + +#Widgets dashboard: + +#Add Medication Button and Entries +addMedButton = tk.Button(dashboard, text="Add New Medication", bg="#F5D5F7", font=font1, command=addMedication) +medicationEntry = tk.Entry(dashboard) +medicationEntryL = tk.Label(dashboard, text = "Input Medications", bg = "#F5D5F7", font = font1) +dosageEntry = tk.Entry(dashboard) +dosageEntryL = tk.Label(dashboard, text = "Dosage", bg = "#F5D5F7", font = font1) +timeEntry = tk.Entry(dashboard) +timeEntryL = tk.Label(dashboard, text = "Take at", bg = "#F5D5F7", font = font1) +symptomsEntry = tk.Entry(dashboard) +symptomsEntryL = tk.Label(dashboard, text = "Symptoms", bg = "#F5D5F7", font = font1) + +#Listbox and Scrollbar for medications +medListbox = tk.Listbox(dashboard, font=font1) +scrollbar = tk.Scrollbar(dashboard, orient=tk.VERTICAL) + +medListbox.config(yscrollcommand=scrollbar.set) +scrollbar.config(command=medListbox.yview) + +#remove medication button appears when a medication is selected +removeMedButton = tk.Button(dashboard, text="Remove Selected Medication", font=font1, bg="#F5D5F7", command=confirmRemoveMedication) +removeMedButton.place(x=350, y=390, width=200, height=30) +removeMedButton.place_forget() + + +'''TODO: Implement history window and its button +historyButton = tk.Button(dashboard, text="View Medication History", bg="#F5D5F7", font=font1, command=lambda: [dashboard.withdraw(), historyWindow.deiconify()])''' + + +#positions for labels and buttons + +#login +loginButton.place(x = 125, y = 100) +signUpButton.place(x = 125, y = 250) +userLabel.place(x=50, y=150) +userEntry.place(x=130 , y=150) +passLabel.place(x=50, y=190) +passEntry.place(x=130, y= 190) +#create +signUserEntry.place(x=195, y= 150) +signPassEntry.place(x=195, y= 190) +passRentry.place(x=195,y=230) +signUserEntryL.place(x=30, y=150) +signPassEntryL.place(x=30, y=190) +passRentryL.place(x=30, y=230) +createUserButton.place(x = 130, y= 300) +backButton.place(x=130, y=350) +#dashboard +medicationEntryL.place(x=20, y=30) +medicationEntry.place(x=150, y=30, width=140) + +dosageEntryL.place(x=20, y=80) +dosageEntry.place(x=150, y=80, width=140) + +timeEntryL.place(x=20, y=130) +timeEntry.place(x=150, y=130, width=140) + +symptomsEntryL.place(x=20, y=180) +symptomsEntry.place(x=150, y=180, width=140) + +addMedButton.place(x=20, y=230, width=260, height=30) + +medListbox.place(x=350, y=30, width=400, height=350) +scrollbar.place(x=750, y=30, width=20, height=350) + +'''TODO: Implement history window layout''' + +def onMedSelect(event): + selection = medListbox.curselection() + + if selection and selection[0] != 0: + removeMedButton.place(x=350, y=390, width=200, height=30) + else: + removeMedButton.place_forget() + selection = medListbox.curselection() + +medListbox.bind('<>', onMedSelect) + +def confirmRemoveMedication(): + global current_user + + selection = medListbox.curselection() + + if not selection or selection[0] == 0: + return + + med_index = selection[0] - 1 + med_name = users[current_user]["medications"][med_index]["name"] + + answer = messagebox.askyesno(title="Confirm Removal", message=f"Are you sure you want to remove {med_name}?") + if answer: + users[current_user]["medications"].pop(med_index) + updateMedListbox() + removeMedButton.place_forget() + + +mainWindow.mainloop() diff --git a/Sprint 2/videoPlaceholder.txt b/Sprint 2/videoPlaceholder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 3/Sprint 3 Test cases b/Sprint 3/Sprint 3 Test cases new file mode 100644 index 000000000..1a5110525 --- /dev/null +++ b/Sprint 3/Sprint 3 Test cases @@ -0,0 +1,163 @@ +Use Case 3- <> +Test Case ID +TC01 - Notifications Pop up for the specified dosages +Test Objective +The notification for medication use will pop up for the user at the designated time +Preconditions +User’s account exists +User is logged in successfully +At least one medication with a name, dosage, and scheduled time is already added +Test steps +Login to the app +Add a new medication +Open the scheduling window +Select a time using the hour, minute, and AM/PM dropdowns +Select an occurrence value (example: Daily) +(Optional) Select specific days of the week if using custom days +Save the schedule so the time field is auto-filled +Save the medication +Wait for the scheduled time +Close the pop-up notification +Input Values +Username: sample, Password: 123 +Name: TestMed, Dosage: 10mg, Scheduling window values: Time 1–2 minutes from current time, Occurrence: Daily, symptoms: none +N/A +Click OK +Expected results +dashboard opens, user medications load +Scheduling window opens and accepts user selections +Selected time appears in the medication time field +Medication is added to the listbox and confirmation message shows +A pop-up appears at the scheduled time displaying: “Time to take TestMed (10mg)” +Pop-up closes + + + +Test Case ID +TC02 - Confirmation that dosage was taken +Test Objective +The user will be able to press taken or not taken. +Preconditions +The user has received a pop-up for the medication they need to take +Test steps +The user receives a pop-up alert for medication +The User presses yes +The pop-up window closes +Test case ends +Input Values +Click Yes +Expected results +The pop-up from the notification will disappear and will not reappear until the next dosage. + + + +Test Case ID +TC03 - Overdue Notifications +Test Objective +If the user has pressed wait, the pop-up will return in a minute +Preconditions +The user received a pop-up for the medication they needed to take +Test steps +The user receives a pop-up alert for the medication +The user selects wait +The pop-up opens again after a minute and the user is prompted to press “Ok” or “Wait” again. +User waits for the reminder to pop up again + +The user presses “Ok” +Test case ends. +Input Values +1. Input the Medication’s dosage and scheduled time + +2.Wait for the reminder to pop up + +3.Press “Wait” + + + +Expected results +The pop-up window will reappear after the user selects “Wait” to the prompt: +“Time to take MedA (5mg)” +The user can then press Wait again, or press Ok. + + + +Test Case ID +TC04 - Overlapping Dosage Handling +Test Objective +Ensure that when two or more medications have scheduled times that overlap (occur at the same minute), the app correctly displays multiple notifications without crashing, overlapping UI, or blocking the user from responding to each alert. +Preconditions +User’s account exists + + +User is logged in successfully + + +At least two medications are added with dosage times scheduled for the same minute + + +Both medications have valid names, dosages, and times saved + + +Test steps +Login to the app + + +Add Medication A + + +Add Medication B + + +Set both medications to notify within 1–2 minutes of the current time + + +Wait for scheduled notification time + + +Observe whether one or both pop-ups appear + + +Press Yes or OK on the first pop-up + + +Close the second pop-up after responding + + +End test case +Input Values +Username: sample, Password: 123 + + +Medication A: Name: MedA, Dosage: 5mg, Time: 1–2 minutes from now + + +Medication B: Name: MedB, Dosage: 20mg, Time: 1–2 minutes from now + + +User clicks Yes (taken) or OK on both pop-ups + + +Expected results +Both medications appear in the listbox after being added + + +Two separate pop-up notifications appear, one for each medication: + + +“Time to take MedA (5mg)” + + +“Time to take MedB (20mg)” + + +Pop-ups do not overlap in a way that prevents interaction + + +App does not freeze, delay, or prevent the user from closing each alert + + +Once each notification is handled, no duplicate notification appears until the next scheduled dosage time + + + + diff --git a/Sprint 3/TestCasesPlaceholder.txt b/Sprint 3/TestCasesPlaceholder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 3/UMLDiagrams.txt b/Sprint 3/UMLDiagrams.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 3/codePlaceHolder.txt b/Sprint 3/codePlaceHolder.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/Sprint 3/meditrackCode.py b/Sprint 3/meditrackCode.py new file mode 100644 index 000000000..55940a937 --- /dev/null +++ b/Sprint 3/meditrackCode.py @@ -0,0 +1,504 @@ +import tkinter as tk +import tkinter.font as tkFont +from tkinter import Listbox, messagebox +from tkinter import ttk +from datetime import datetime, timedelta + +import datetime + +def parse_time(t): + return datetime.datetime.strptime(t, "%I:%M %p").time() + +def seconds_until(t): + now = datetime.datetime.now() + target = datetime.datetime.combine(now.date(), t) + if target < now: + target += datetime.timedelta(days=1) + return (target - now).total_seconds() + +#User database +users = { + "sample": { + "password": "123", + "medications": [ + { + "name": "Aspirin", + "dosage": "100mg", + "time": "8:00 AM", + "symptoms": "Headache", + "occurrence": "Daily", + "days": [] + } + ] + } +} + +#Global variables +current_user = None +reminders = {} # key: (username, med_name, time), value: afterID +current_schedule = { #stores current schedule selections + "time": None, + "occurrence": None, + "days": [] +} + +#Main window configurations +mainWindow = tk.Tk() +mainWindow.title("MediTrack Login form") +mainWindow.geometry("350x450") +mainWindow.configure(bg = '#F0F0F0') +font1 = tkFont.Font(family = "Arial", size = 12, weight = tkFont.NORMAL) + +#Dashboard Window configurations +dashboard = tk.Toplevel(mainWindow) +dashboard.title("MediTrack dashboard") +dashboard.geometry("800x450") +dashboard.configure(bg = '#F0F0F0') +dashboard.withdraw() + +#Sign-in Window +signIn = tk.Toplevel(mainWindow) +signIn.title("Create new User") +signIn.geometry("350x450") +signIn.configure(bg = '#F0F0F0') +signIn.withdraw() + + + +''' TODO: Implement history window + CODE: + historyWindow = tk.Toplevel(dashboard) + historyWindow.title("Medication History") + historyWindow.geometry("350x450") + historyWindow.configure(bg = '#F0F0F0') + historyWindow.withdraw() ''' + +label = tk.Label(mainWindow, text = "MediTrack", bg= "#F5D5F7", font = font1) +label.place(x= 125,y=0) + +def login(): + global users + global current_user + username = userEntry.get().strip() + password = passEntry.get().strip() + print("attempted login:", username, password) + + if username in users and users[username]["password"] == password: + current_user = username + messagebox.showinfo(title = "Login Successful", message = f"Welcome back, {username}!") + dashboard.deiconify() + mainWindow.withdraw() + updateMedListbox() + else: + messagebox.showinfo(title = "Login Failed", message = "Invalid username or password") + +def signUp(): + mainWindow.withdraw() + signIn.deiconify() + +def createUser(): + username = signUserEntry.get().strip() + password = signPassEntry.get().strip() + confirmPass = passRentry.get().strip() + + if username in users: + messagebox.showinfo(title = "Sign up failed", message = "Account with this user already exists") + + elif password != confirmPass: + messagebox.showinfo(title = "Invalid Password", message = "Passwords don't match") + + else: + users[username] = {"password": password, "medications": []} + print(users) + messagebox.showinfo(title = "User created", message = "Redirecting back to login") + signIn.withdraw() + mainWindow.deiconify() + +#adds medication to the list if not already present +def addMedication(): + global current_user + user_medications = users[current_user].setdefault("medications", []) + + med_input = medicationEntry.get().strip() + dosage_input = dosageEntry.get().strip() + symptoms_input = symptomsEntry.get().strip() + + #input validation + if not med_input: + messagebox.showinfo(title="Missing input", message="Medication cannot be empty!") + return + + if not dosage_input: + messagebox.showinfo(title="Missing input", message="Please enter the dosage") + return + + if not current_schedule["time"]: + messagebox.showinfo(title="Missing input", message="Please set a schedule for the medication") + return + + if parse_time(current_schedule["time"]) is None: + messagebox.showinfo(title="Invalid Time Format", message="Please click 'Set Schedule' and choose a time and occurrence for the medication") + return + + #normalize medication name for duplicate check + med_norm = med_input.casefold() + + #duplicate check + is_duplicate = any(med.get("name", "").strip().casefold() == med_norm + for med in user_medications) + if is_duplicate: + messagebox.showinfo(title="Duplicate Entry", message=f"{med_input} is already in your medications!") + return + + #add medication to the list + new_med = { + "name": med_input, + "dosage": dosage_input, + "time": current_schedule["time"], + "symptoms": symptoms_input, + "occurrence": current_schedule["occurrence"], + "days": current_schedule["days"], + } + + user_medications.append(new_med) + users[current_user]["medications"] = user_medications + + scheduleReminder(current_user, new_med) + + #refresh listbox + updateMedListbox() + + #clear entries + medicationEntry.delete(0, tk.END) + dosageEntry.delete(0, tk.END) + symptomsEntry.delete(0, tk.END) + + #confirmation message + messagebox.showinfo(title = "Medication Added", message = f"{med_input} is added to your medications!") + print(f"[DEBUG] Current medications for {current_user}: {users[current_user]['medications']}") + +#function to remove selected medication from listbox and user data +def onMedSelect(event): + selection = medListbox.curselection() + + if selection and selection[0] != 0: + removeMedButton.place(x=350, y=390, width=200, height=30) + else: + removeMedButton.place_forget() + selection = medListbox.curselection() + + medListbox.bind('<>', onMedSelect) + + +#function to confirm and remove medication +def confirmRemoveMedication(): + global current_user + + selection = medListbox.curselection() + + if not selection or selection[0] == 0: + return + + med_index = selection[0] - 1 + med_name = users[current_user]["medications"][med_index]["name"] + + answer = messagebox.askyesno(title="Confirm Removal", message=f"Are you sure you want to remove {med_name}?") + if answer: + users[current_user]["medications"].pop(med_index) + updateMedListbox() + removeMedButton.place_forget() + +#function to update the medication listbox +def updateMedListbox(): + global current_user + medListbox.delete(0, tk.END) + + medListbox.insert(tk.END, "Name | Dose | Time | Symptoms") + medListbox.itemconfig(0, {'fg': 'white', 'bg': 'black'}) + + user_medications = users[current_user].get("medications", []) + for med in user_medications: + #if symptoms is empty, display "None" + symptoms_display = med.get("symptoms", "").strip() or "None" + line = f"{med['name']} | {med['dosage']} | {med['time']} | {symptoms_display}" + medListbox.insert(tk.END, line) + + +#parses the time input string into a real datetime object +def parse_time(time_str): + try: + parsed = datetime.strptime(time_str, "%I:%M %p") + return parsed.time() + except ValueError: + return None + +#function to calculate milliseconds until the next occurrence of the specified time +def seconds_until(time_str): + try: + now = datetime.now() + medTime = datetime.strptime(time_str, "%I:%M %p") #parse time string + medDate = now.replace(hour=medTime.hour, minute=medTime.minute, second=0, microsecond=0) + + if medDate <= now: + medDate += timedelta(days=1) #schedule for next day if time has passed + + return int((medDate - now).total_seconds()*1000) #convert to milliseconds + + except ValueError: + return None + + + +# function to schedule reminders (TC01 + TC02) +def scheduleReminder(username, med): + # get time string and calculate delay using YOUR helper + time_str = med['time'] + delay_ms = seconds_until(time_str) + if not delay_ms or delay_ms <= 0: + print(f"[ERROR] Invalid time format for medication {med['name']}: {time_str}") + return + + # unique key for each reminder (so we can cancel/reschedule correctly) + key = (username, med['name'], time_str) + + # cancel existing reminder if present + if key in reminders: + try: + dashboard.after_cancel(reminders[key]) + except Exception: + pass + reminders.pop(key, None) + + # function to show reminder popup + def notify(): + popup = tk.Toplevel(dashboard) + popup.title("Medication Reminder") + popup.geometry("300x150") + tk.Label(popup, text=f"Time to take {med['name']} ({med['dosage']})", font=font1).pack(pady=20) + + #function to delay the reminder popup + def waitButton(): + popup.destroy() + + #delaying the reminder for 1 minute + waitDelay = 60*1000 + remindID = dashboard.after(waitDelay, showReminder) + reminders[key] = remindID + + def okButton(): + popup.destroy() + scheduleReminder(username, med) + + tk.Button(popup, text="OK", command=okButton).pack(pady=10) + tk.Button(popup, text = "Wait (1min)", command = waitButton).pack(pady = 5) + + remindID = dashboard.after(delay_ms, showReminder) #schedule the reminder + reminders[key] = remindID #store the reminder ID + print(f"[DEBUG] Scheduled reminder for {username} to take {med['name']} in {delay_ms/1000:.2f} seconds.") + + +#callback function when schedule is chosen +def on_schedule_chosen(time_str, occurence, days): + #update current schedule + current_schedule["time"] = time_str + current_schedule["occurrence"] = occurence + current_schedule["days"] = days + + #update time entry field + print("[DEBUG] Selected schedule:", time_str, occurence, days) + + +#function to open scheduler window +def open_scheduler(on_done): + + #create scheduler window + window = tk.Toplevel(dashboard) + window.title("Schedule Your Medication") + window.geometry("320x260") + window.configure(bg='#F0F0F0') + window.grab_set() + + # --- time selection widgets --- + tk.Label(window, text="Time:", bg="#F0F0F0", font=font1).place(x=20, y=20) + + # default time values + hourVar = tk.StringVar(value="8") + minuteVar = tk.StringVar(value="00") + ampmVar = tk.StringVar(value="AM") + + # options for time selection + hours = [f"{i}" for i in range(1, 13)] + minutes = [f"{i:02d}" for i in range(0, 60)] + ampm = ["AM", "PM"] + + #time selection comboboxes + hours_box = ttk.Combobox(window, textvariable=hourVar, values=hours, width=5, state="readonly") + hours_box.place(x=80, y=20) + + tk.Label(window, text=":", bg="#F0F0F0", font=font1).place(x=140, y=20) + + minute_box = ttk.Combobox(window, textvariable=minuteVar, values=minutes, width=5, state="readonly") + minute_box.place(x=135, y=20) + + ampm_box = ttk.Combobox(window, textvariable=ampmVar, values=ampm, width=5, state="readonly") + ampm_box.place(x=195, y=20) + + tk.Label(window, text="Occurrrence:", bg="#F0F0F0", font=font1).place(x=20, y=70) + + # occurrence selection combobox + occurrenceVar = tk.StringVar(value="Daily") + occurrence_box = ttk.Combobox(window, textvariable=occurrenceVar, + values=["Once", "Daily", "Monthly", "Custom days"], + state="readonly", + width=15) + occurrence_box.place(x=120, y=70) + + # --- Optional specific days selection --- + + tk.Label(window, text="(Optional) Select Days:", bg="#F0F0F0", font=font1).place(x=20, y=110) + + days_frame = tk.Frame(window, bg="#F0F0F0") + days_frame.place(x=20, y=135) + + dayVars = {} + days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + for i, name in enumerate(days): + var = tk.BooleanVar(value=False) + chk = tk.Checkbutton(days_frame, text=name, variable=var, bg="#F0F0F0") + chk.grid(row=0, column=i, padx=2) + dayVars[name] = var + +# --- Save and Cancel buttons --- + + def on_save(): + hour = hourVar.get() + minute = minuteVar.get() + ampm = ampmVar.get() + occurrence = occurrenceVar.get() + + time_str = f"{int(hour)}:{minute} {ampm}" + selected_days = [name for name, var in dayVars.items() if var.get()] + + on_done(time_str, occurrence, selected_days) + window.destroy() + + def on_cancel(): + window.destroy() + + tk.Button(window, text="Save", bg="#F5D5F7", font=font1, command=on_save).place(x=70, y=200, width=80, height=30) + tk.Button(window, text="Cancel", bg="#F5D5F7", font=font1, command=on_cancel).place(x=170, y=200, width=80, height=30) + + + + + +# --- widgets main login window --- + +userLabel = tk.Label(mainWindow, text = "Username", bg = "#F5D5F7", font = font1) +passLabel = tk.Label(mainWindow, text = "Password", bg = "#F5D5F7", font = font1) +userEntry = tk.Entry(mainWindow) +passEntry = tk.Entry(mainWindow, show = "*") +loginButton = tk.Button(mainWindow, text = "Login", bg = "#F5D5F7", font = font1, command = login) +signUpButton = tk.Button(mainWindow, text = "Sign up", bg = "#F5D5F7", font = font1, command = signUp) + +# --- widgets create user window --- + +signUserEntryL = tk.Label(signIn, text = "Enter Username", bg = "#F5D5F7", font = font1) +signPassEntryL = tk.Label(signIn, text = "Enter Password", bg = "#F5D5F7", font = font1) +passRentryL = tk.Label(signIn, text = "Confirm Password", bg = "#F5D5F7", font = font1) +signUserEntry = tk.Entry(signIn) +signPassEntry = tk.Entry(signIn, show = "*") +passRentry = tk.Entry(signIn,show = "*") +createUserButton = tk.Button(signIn, text = "Create User", bg = "#F5D5F7", font = font1, command = createUser) +backButton = tk.Button(signIn, text="Go Back", bg="#F5D5F7", font=font1, command=lambda: [signIn.withdraw(), mainWindow.deiconify()]) + + +# --- widgets dashboard window --- + +#Entries +medicationEntry = tk.Entry(dashboard) +medicationEntryL = tk.Label(dashboard, text = "Input Medications", bg = "#F5D5F7", font = font1) +dosageEntry = tk.Entry(dashboard) +dosageEntryL = tk.Label(dashboard, text = "Dosage", bg = "#F5D5F7", font = font1) +symptomsEntry = tk.Entry(dashboard) +symptomsEntryL = tk.Label(dashboard, text = "Symptoms", bg = "#F5D5F7", font = font1) + +#buttons +scheduleButton = tk.Button( + dashboard, + text="Set Schedule", + bg="#F5D5F7", + font=font1, + command=lambda: open_scheduler(on_schedule_chosen) +) + +addMedButton = tk.Button(dashboard, text="Add Medication", bg="#F5D5F7", font=font1, command=addMedication) + +#Listbox and Scrollbar for medications +medListbox = tk.Listbox(dashboard, font=font1) +scrollbar = tk.Scrollbar(dashboard, orient=tk.VERTICAL) + +medListbox.config(yscrollcommand=scrollbar.set) +scrollbar.config(command=medListbox.yview) + +#remove medication button appears when a medication is selected +removeMedButton = tk.Button(dashboard, text="Remove Selected Medication", font=font1, bg="#F5D5F7", command=confirmRemoveMedication) +removeMedButton.place(x=350, y=390, width=200, height=30) +removeMedButton.place_forget() + + + +'''TODO: Implement history window and its button +historyButton = tk.Button(dashboard, text="View Medication History", bg="#F5D5F7", font=font1, command=lambda: [dashboard.withdraw(), historyWindow.deiconify()])''' + + +# --- positions for labels and buttons --- + +#login +loginButton.place(x = 125, y = 100) +signUpButton.place(x = 125, y = 250) +userLabel.place(x=50, y=150) +userEntry.place(x=130 , y=150) +passLabel.place(x=50, y=190) +passEntry.place(x=130, y= 190) +#create +signUserEntry.place(x=195, y= 150) +signPassEntry.place(x=195, y= 190) +passRentry.place(x=195,y=230) +signUserEntryL.place(x=30, y=150) +signPassEntryL.place(x=30, y=190) +passRentryL.place(x=30, y=230) +createUserButton.place(x = 130, y= 300) +backButton.place(x=130, y=350) +#dashboard +medicationEntryL.place(x=20, y=30) +medicationEntry.place(x=155, y=30, width=140) + +dosageEntryL.place(x=20, y=80) +dosageEntry.place(x=155, y=80, width=140) + +symptomsEntryL.place(x=20, y=130) +symptomsEntry.place(x=155, y=130, width=140) + +scheduleButton.place(x=20, y=180, width=110, height=30) +addMedButton.place(x=155, y=180, width=145, height=30) + +medListbox.place(x=350, y=30, width=400, height=350) +scrollbar.place(x=750, y=30, width=20, height=350) + + +'''TODO: Implement history window layout''' + +def onMedSelect(event): + selection = medListbox.curselection() + + if selection and selection[0] != 0: + removeMedButton.place(x=350, y=390, width=215, height=30) + else: + removeMedButton.place_forget() + selection = medListbox.curselection() + +medListbox.bind('<>', onMedSelect) + + +mainWindow.mainloop() diff --git a/Sprint 3/videoPlaceholder.txt b/Sprint 3/videoPlaceholder.txt index e69de29bb..f0acd7102 100644 --- a/Sprint 3/videoPlaceholder.txt +++ b/Sprint 3/videoPlaceholder.txt @@ -0,0 +1 @@ +https://drive.google.com/file/d/1oMbpqjaWqCMOxSYe05NM6bsGCuZQFPtb/view?usp=sharing