From dc0e1d37d424dcd8488764de58e1ab5c16f43e87 Mon Sep 17 00:00:00 2001 From: Mtasiu Date: Fri, 16 May 2025 02:30:11 +0100 Subject: [PATCH 1/3] Add complete Hotel Management System source code and data files --- .idea/ OOP-PythonAssignment-200L-NUN-2025.iml | 10 + .idea/inspectionProfiles/Project_Default.xml | 14 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 74 ++++ .venv/.gitignore | 9 + GUI.py | 379 ++++++++++++++++++ __pycache__/guest.cpython-312.pyc | Bin 0 -> 2061 bytes __pycache__/hotel_management.cpython-312.pyc | Bin 0 -> 7588 bytes __pycache__/reservation.cpython-312.pyc | Bin 0 -> 4883 bytes __pycache__/room.cpython-312.pyc | Bin 0 -> 2554 bytes guest.py | 36 ++ guests.csv | 4 + hotel_management.py | 149 +++++++ reservation.py | 85 ++++ reservations.csv | 5 + room.py | 43 ++ rooms.csv | 51 +++ 20 files changed, 883 insertions(+) create mode 100644 .idea/ OOP-PythonAssignment-200L-NUN-2025.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 .venv/.gitignore create mode 100644 GUI.py create mode 100644 __pycache__/guest.cpython-312.pyc create mode 100644 __pycache__/hotel_management.cpython-312.pyc create mode 100644 __pycache__/reservation.cpython-312.pyc create mode 100644 __pycache__/room.cpython-312.pyc create mode 100644 guest.py create mode 100644 guests.csv create mode 100644 hotel_management.py create mode 100644 reservation.py create mode 100644 reservations.csv create mode 100644 room.py create mode 100644 rooms.csv diff --git a/.idea/ OOP-PythonAssignment-200L-NUN-2025.iml b/.idea/ OOP-PythonAssignment-200L-NUN-2025.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/ OOP-PythonAssignment-200L-NUN-2025.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..fc5a4f3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..11b66a2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f550868 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..b0f621f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1747358063617 + + + + + + \ No newline at end of file diff --git a/.venv/.gitignore b/.venv/.gitignore new file mode 100644 index 0000000..cd485e2 --- /dev/null +++ b/.venv/.gitignore @@ -0,0 +1,9 @@ +# Python virtualenv +.venv/ +Lib/ +Scripts/ +pyvenv.cfg + +# Byte-compiled files +__pycache__/ +*.py[cod] diff --git a/GUI.py b/GUI.py new file mode 100644 index 0000000..d86f50e --- /dev/null +++ b/GUI.py @@ -0,0 +1,379 @@ +import tkinter as tk +from tkinter import ttk, messagebox +from hotel_management import HotelManagement +from guest import Guest + +class HotelApp: + def __init__(self, root): + self.root = root + root.title("Hotel Management") + root.geometry("360x640") + root.resizable(False, False) + root.configure(bg="skyblue") + + # ─── Styles ─────────────────────────────────────────────────── + self.style = ttk.Style(root) + self.style.theme_use('default') + + # Accent button style (bright blue, white text, padded) + self.style.configure( + 'Accent.TButton', + background='#4eb5f1', + foreground='white', + font=('Helvetica', 16), + padding=10, + borderwidth=0 + ) + self.style.map( + 'Accent.TButton', + background=[('active', '#66c3ff')] + ) + + # Treeview style (taller rows, bold headings, light header bg) + self.style.configure( + 'Custom.Treeview', + rowheight=28, + font=('Helvetica', 12), + fieldbackground='white' + ) + self.style.configure( + 'Custom.Treeview.Heading', + font=('Helvetica', 13, 'bold'), + background='#e0e0e0' + ) + self.style.layout('Custom.Treeview', self.style.layout('Treeview')) + + # ─── Backend ────────────────────────────────────────────────── + self.hm = HotelManagement() + self.hm.load_data() + self.current_guest = None + + # ─── Frames ─────────────────────────────────────────────────── + self.login_frame = tk.Frame(root, bg="gray") + self.menu_frame = tk.Frame(root, bg="gray") + self.book_frame = tk.Frame(root, bg="gray") + self.view_frame = tk.Frame(root, bg="gray") + self.modify_frame = tk.Frame(root, bg="gray") + for f in ( + self.login_frame, + self.menu_frame, + self.book_frame, + self.view_frame, + self.modify_frame + ): + f.place(relwidth=1, relheight=1) + + # ─── Build UI ───────────────────────────────────────────────── + self._build_login() + self._build_menu() + self._build_book() + self._build_view() + self._build_modify() + + # start on login + self._show(self.login_frame) + + def _show(self, frame): + frame.tkraise() + + # ─── Login Screen ─────────────────────────────────────────────── + + def _build_login(self): + frm = self.login_frame + frm.columnconfigure(0, weight=1) + + ttk.Label(frm, text="Select Existing Guest:", background="gray", + font=('Helvetica',12)).grid(row=0, column=0, pady=(30,5)) + self.cb_guest = ttk.Combobox(frm, state="readonly") + self.cb_guest.grid(row=1, column=0, sticky="we", padx=30) + + ttk.Button(frm, text="Log In", style='Accent.TButton', + command=self._on_login).grid(row=2, column=0, pady=15, padx=50) + + ttk.Separator(frm, orient="horizontal")\ + .grid(row=3, column=0, sticky="ew", padx=30, pady=15) + + ttk.Label(frm, text="Name:", background="gray").grid( + row=4, column=0, sticky="w", padx=30 + ) + self.e_name = ttk.Entry(frm) + self.e_name.grid(row=5, column=0, sticky="we", padx=30, pady=5) + + ttk.Label(frm, text="Contact Info:", background="gray").grid( + row=6, column=0, sticky="w", padx=30, pady=(10,0) + ) + self.e_contact = ttk.Entry(frm) + self.e_contact.grid(row=7, column=0, sticky="we", padx=30, pady=5) + + ttk.Button(frm, text="Register & Log In", style='Accent.TButton', + command=self._on_register).grid(row=8, column=0, pady=25, padx=50) + + self._reload_guest_list() + + def _reload_guest_list(self): + self._guest_map = { + f"{g.name} — {g.contact_info}": g.guest_id + for g in self.hm.guests.values() + } + self.cb_guest['values'] = list(self._guest_map) + self.cb_guest.set("") + + def _on_login(self): + sel = self.cb_guest.get() + gid = self._guest_map.get(sel) + if not gid: + messagebox.showwarning("Login Error", "Please select a guest.") + return + self.current_guest = self.hm.guests[gid] + self._enter_menu() + + def _on_register(self): + name = self.e_name.get().strip() + contact = self.e_contact.get().strip() + if not name or not contact: + messagebox.showwarning("Input Error", "Name & Contact required.") + return + guest = Guest(name=name, contact_info=contact) + self.hm.add_guest(guest) + self.current_guest = guest + self._reload_guest_list() + self._enter_menu() + + + # ─── Main Menu ────────────────────────────────────────────────── + + def _build_menu(self): + frm = self.menu_frame + frm.columnconfigure(0, weight=1) + + self.lbl_welcome = ttk.Label( + frm, text="", background="gray", + font=('Helvetica', 16, 'bold') + ) + self.lbl_welcome.grid(row=0, column=0, pady=40) + + ttk.Button(frm, text="Make New Booking", style='Accent.TButton', + command=self._enter_book).grid(row=1, column=0, pady=8, padx=60) + ttk.Button(frm, text="View Bookings", style='Accent.TButton', + command=self._enter_view).grid(row=2, column=0, pady=8, padx=60) + ttk.Button(frm, text="Modify Bookings", style='Accent.TButton', + command=self._enter_modify).grid(row=3, column=0, pady=8, padx=60) + ttk.Button(frm, text="Logout", style='Accent.TButton', + command=self._logout).grid(row=4, column=0, pady=30, padx=60) + + def _enter_menu(self): + self.lbl_welcome.config( + text=f"Hello, {self.current_guest.name} ({self.current_guest.guest_id})" + ) + self._show(self.menu_frame) + + def _logout(self): + self.current_guest = None + self.e_name.delete(0, 'end') + self.e_contact.delete(0, 'end') + self._reload_guest_list() + self._show(self.login_frame) + + + # ─── Search & Book Screen ─────────────────────────────────────── + + def _build_book(self): + frm = self.book_frame + frm.columnconfigure(0, weight=1) + + ttk.Label(frm, text="Room Type:", background="gray").grid( + row=0, column=0, sticky="w", padx=30, pady=(30,5) + ) + self.cb_type = ttk.Combobox(frm, state="readonly") + self.cb_type.grid(row=1, column=0, padx=30, sticky="we") + self.cb_type.bind("<>", lambda e: self._refresh_rooms()) + + ttk.Label(frm, text="Room #:", background="gray").grid( + row=2, column=0, sticky="w", padx=30, pady=(20,5) + ) + self.cb_rooms = ttk.Combobox(frm, state="readonly") + self.cb_rooms.grid(row=3, column=0, padx=30, sticky="we") + + ttk.Label(frm, text="Stay (days):", background="gray").grid( + row=4, column=0, sticky="w", padx=30, pady=(20,5) + ) + self.e_days = ttk.Entry(frm) + self.e_days.grid(row=5, column=0, padx=30, sticky="we") + + ttk.Button(frm, text="Book", style='Accent.TButton', + command=self._on_book).grid( + row=6, column=0, pady=20, padx=80 + ) + ttk.Button(frm, text="Logout", style='Accent.TButton', + command=self._logout).grid( + row=7, column=0, pady=10, padx=80 + ) + + def _enter_book(self): + types = sorted({r.room_type for r in self.hm.rooms.values()}) + self.cb_type['values'] = types + self.cb_type.set('') + self.cb_rooms.set('') + self.e_days.delete(0, 'end') + self._show(self.book_frame) + + def _refresh_rooms(self): + sel = self.cb_type.get().lower().strip() + avail = [ + r.room_number for r in self.hm.rooms.values() + if r.room_type.lower()==sel and r.availability + ] + self.cb_rooms['values'] = avail + self.cb_rooms.set('') + + def _on_book(self): + try: + room_no = int(self.cb_rooms.get()) + days = int(self.e_days.get()) + if days<1: raise ValueError + except: + messagebox.showwarning( + "Input Error", + "Pick a room and enter a positive number of days." + ) + return + try: + self.hm.make_reservation(self.current_guest, room_no, days) + messagebox.showinfo("Success", f"Booked room {room_no} for {days} days.") + except Exception as e: + messagebox.showerror("Booking Failed", str(e)) + return + self._refresh_rooms() + + + # ─── View Bookings Screen ─────────────────────────────────────── + + def _build_view(self): + frm = self.view_frame + frm.columnconfigure(0, weight=1) + + cols = ("Room","Check-in","Check-out") + self.tv_view = ttk.Treeview( + frm, columns=cols, show="headings", + style='Custom.Treeview', height=10 + ) + for c in cols: + self.tv_view.heading(c, text=c) + self.tv_view.column(c, width=100, anchor="center") + self.tv_view.grid(row=0, column=0, pady=(30,10), padx=10) + + ttk.Button(frm, text="Back", style='Accent.TButton', + command=self._enter_menu).grid(row=1, column=0, pady=20, padx=100) + + def _enter_view(self): + self.tv_view.delete(*self.tv_view.get_children()) + for res in self.hm.reservations: + if res.guest.guest_id == self.current_guest.guest_id and res.is_active: + self.tv_view.insert("", "end", values=( + res.room.room_number, + res.check_in_date.strftime("%d/%m/%Y"), + res.check_out_date.strftime("%d/%m/%Y") + )) + self._show(self.view_frame) + + + # ─── Modify Bookings Screen ──────────────────────────────────── + + def _build_modify(self): + frm = self.modify_frame + frm.columnconfigure(0, weight=1) + + cols = ("Room","Check-in","Check-out") + self.tv_mod = ttk.Treeview( + frm, columns=cols, show="headings", + style='Custom.Treeview', height=8 + ) + for c in cols: + self.tv_mod.heading(c, text=c) + self.tv_mod.column(c, width=100, anchor="center") + self.tv_mod.grid(row=0, column=0, pady=(30,10), padx=10) + + btn_frame = tk.Frame(frm, bg="gray") + btn_frame.grid(row=1, column=0, pady=20) + ttk.Button(btn_frame, text="Back", style='Accent.TButton', + command=self._enter_menu).grid(row=0, column=0, padx=5) + ttk.Button(btn_frame, text="Modify", style='Accent.TButton', + command=self._on_modify_popup).grid(row=0, column=1, padx=5) + + def _enter_modify(self): + self.tv_mod.delete(*self.tv_mod.get_children()) + for idx, res in enumerate(self.hm.reservations): + if res.guest.guest_id == self.current_guest.guest_id and res.is_active: + self.tv_mod.insert("", "end", iid=str(idx), values=( + res.room.room_number, + res.check_in_date.strftime("%d/%m/%Y"), + res.check_out_date.strftime("%d/%m/%Y") + )) + self._show(self.modify_frame) + + def _on_modify_popup(self): + sel = self.tv_mod.selection() + if not sel: + messagebox.showwarning("Select Reservation", "Please select one.") + return + idx = int(sel[0]) + res = self.hm.reservations[idx] + + win = tk.Toplevel(self.root) + win.title("Modify Reservation") + win.geometry("280x200") + win.resizable(False, False) + win.configure(bg="gray") + + ttk.Label(win, text=f"Room {res.room.room_number}", + background="gray", font=('Helvetica',14))\ + .grid(row=0, column=0, columnspan=2, pady=10) + + ttk.Label(win, text="New Stay (days):", + background="gray")\ + .grid(row=1, column=0, sticky="e", padx=10) + e_days = ttk.Entry(win) + e_days.grid(row=1, column=1, padx=10, pady=5) + + ttk.Button(win, text="Update", style='Accent.TButton', + command=lambda: self._modify_confirm(res, e_days, win))\ + .grid(row=2, column=0, pady=15, padx=5) + ttk.Button(win, text="Cancel Reservation", style='Accent.TButton', + command=lambda: self._cancel_confirm(res, win))\ + .grid(row=2, column=1, pady=15, padx=5) + + def _modify_confirm(self, res, e_days, win): + try: + newd = int(e_days.get()) + if newd < 1: + raise ValueError + except: + messagebox.showwarning("Input Error", "Enter a positive number.") + return + try: + self.hm.modify_reservation(res, newd) + messagebox.showinfo("Updated", "Reservation updated.") + win.destroy() + self._enter_modify() + except Exception as e: + messagebox.showerror("Error", str(e)) + + def _cancel_confirm(self, res, win): + if messagebox.askyesno("Confirm Cancel", + "Are you sure you want to cancel?"): + try: + self.hm.cancel_reservation(res) + messagebox.showinfo("Canceled", "Reservation canceled.") + win.destroy() + self._enter_modify() + except Exception as e: + messagebox.showerror("Error", str(e)) + + +def main(): + root = tk.Tk() + app = HotelApp(root) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/__pycache__/guest.cpython-312.pyc b/__pycache__/guest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ef6359dc6c1129f420d58ff268cbda45daa91c0 GIT binary patch literal 2061 zcmbtV&1)N15PxsKv{Lku*o_s-kt^9v({*to(ljpV2Nb8cElLs~!h2f#njlCsCVvyK5D^!Om~ zp$65$j=WGeEj#TMGjwqEYB$g}DHZ{MY*um@Tp)-lSHP z5}Sg^S3l!*53jx+cSJcxHi$m3ZBUT#OujY*kOt8jL=)1_5JF96_>Cj3nKvE3-cQVt zb)I)fA5L789LX|EE6fKj4BeR@h=u1SV7*+Pznr^nnas*v{bXTrDV?}BmrA8FxlE(5 zmS^P*tFD^`+sY-DmNK)MhP_s;d~8|8)k@i{*s~W>sjIW;>uF#vyqn{xoU1h)RR`3H zwyrnB9Mf}sS;5$E1G!DQ3YmQKQT)9hPwq$WPwY)Rxb`5w`}!T}oA5zA30vSGuHBIy ztFaz@P8@}qcAZrU186~sJVMcn0Ri~QI_!tnGTEbPjj|9J_9#0Jn}v=Eb^<6zqn~9T zO8nFc0=Z3oi;RAI>+Y?N8fT-R_%Gqv7&t-#Vc1v{$Z&YPmS(^O39k=x8VcRK+w7?0 z|2uT#sLyeFK_pC%c`6?MeLn!}$5hQz_)-lQJI5-h)OFQB1a3^~92e$jh z{h7U)-P4`OrH*>()e`W&_??{sW^3rB+93PhXe|@3V;vDfAMRM3ZD!UmrNL~wGQ#S$ z?yr%N&5K_*I_fE&fqPyUU9rb?-3jP=xoXr)$Om=(t9riVT37;3Vb~jN1c;;7ShZ#{ zyTJ$b?^n!`@OIg0{19ncOr3;+NC literal 0 HcmV?d00001 diff --git a/__pycache__/hotel_management.cpython-312.pyc b/__pycache__/hotel_management.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07f462eb8ae8ec4abd4d2e879aaac4c8217d674d GIT binary patch literal 7588 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 G2mS|?$OB&h literal 0 HcmV?d00001 diff --git a/guest.py b/guest.py new file mode 100644 index 0000000..db20d24 --- /dev/null +++ b/guest.py @@ -0,0 +1,36 @@ +from uuid import uuid4 +from typing import Optional + +class Guest: + + def __init__( + self,name: str,contact_info: str,guest_id: Optional[str] = None + ): + self._guest_id = guest_id or uuid4().hex[:8] + self._name = name + self._contact_info = contact_info + + @property + def guest_id(self) -> str: + return self._guest_id + + @property + def name(self) -> str: + return self._name + + @property + def contact_info(self) -> str: + return self._contact_info + + def to_dict(self) -> dict: + return { + "Guest ID": self._guest_id, + "Name": self._name, + "Contact Info": self._contact_info + } + + def __str__(self) -> str: + return f"Guest[{self._guest_id}] {self._name}" + + def __repr__(self) -> str: + return str(self) diff --git a/guests.csv b/guests.csv new file mode 100644 index 0000000..6c8446b --- /dev/null +++ b/guests.csv @@ -0,0 +1,4 @@ +Guest ID,Name,Contact Info +4888c255,Muhammad,20231725 +21fa4ece,Lele,20231940 +f43b215d,Almukhtar,20230101 diff --git a/hotel_management.py b/hotel_management.py new file mode 100644 index 0000000..2d167e5 --- /dev/null +++ b/hotel_management.py @@ -0,0 +1,149 @@ +import csv +from datetime import date +from room import Room +from guest import Guest +from reservation import Reservation + +class HotelManagement: + ROOMS_FILE = "rooms.csv" + GUESTS_FILE = "guests.csv" + RESERVATIONS_FILE = "reservations.csv" + + def __init__(self): + self.rooms : dict[int,Room] = {} + self.guests : dict[str,Guest] = {} + self.reservations : list[Reservation] = [] + + # ─── Loading ─────────────────────────────────────────────────────── + + def load_data(self) -> None: + self._load_rooms() + self._load_guests() + self._load_reservations() + + def _load_rooms(self) -> None: + try: + with open(self.ROOMS_FILE, newline="") as f: + reader = csv.DictReader(f) + for row in reader: + num = int(row["Room Number"]) + room = Room( + room_number = num, + room_type = row["Room Type"], + price = float(row["Price"]), + availability= row["Availability"].lower()=="true" + ) + self.rooms[num] = room + except FileNotFoundError: + pass + + def _load_guests(self) -> None: + try: + with open(self.GUESTS_FILE, newline="") as f: + reader = csv.DictReader(f) + for row in reader: + guest = Guest( + name = row["Name"], + contact_info = row["Contact Info"], + guest_id = row["Guest ID"] + ) + self.guests[guest.guest_id] = guest + except FileNotFoundError: + pass + + def _load_reservations(self) -> None: + try: + with open(self.RESERVATIONS_FILE, newline="") as f: + reader = csv.DictReader(f) + for row in reader: + gid = row["Guest ID"] + rid = int(row["Room Number"]) + ci = date.fromisoformat(row["Check In Date"]) + co = date.fromisoformat(row["Check Out Date"]) + guest = self.guests.get(gid) + room = self.rooms.get(rid) + if not guest or not room: + continue + res = Reservation( + guest = guest, + room = room, + check_in_date = ci, + check_out_date = co + ) + if row["Is Active"].lower() == "true": + res._is_active = True + res.room.availability = False + self.reservations.append(res) + except FileNotFoundError: + pass + + # ─── Saving ──────────────────────────────────────────────────────── + + def save_data(self) -> None: + # Rooms + with open(self.ROOMS_FILE, 'w', newline='') as f: + writer = csv.DictWriter( + f, + fieldnames=["Room Number","Room Type","Price","Availability"] + ) + writer.writeheader() + for r in self.rooms.values(): + writer.writerow(r.to_dict()) + + # Guests + with open(self.GUESTS_FILE, 'w', newline='') as f: + writer = csv.DictWriter( + f, + fieldnames=["Guest ID","Name","Contact Info"] + ) + writer.writeheader() + for g in self.guests.values(): + writer.writerow(g.to_dict()) + + # Reservations + with open(self.RESERVATIONS_FILE, 'w', newline='') as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "Guest ID","Guest Name", + "Room Number","Check In Date","Check Out Date","Is Active" + ] + ) + writer.writeheader() + for res in self.reservations: + # only active or we want to keep history? + writer.writerow(res.to_dict()) + + # ─── Guest Operations ───────────────────────────────────────────── + + def add_guest(self, guest: Guest) -> None: + self.guests[guest.guest_id] = guest + self.save_data() + + # ─── Reservation Operations ──────────────────────────────────────── + + def make_reservation( + self,guest: Guest,room_number: int,stay_duration_days: int + ) -> Reservation: + if room_number not in self.rooms: + raise KeyError(f"Room {room_number} does not exist.") + # prevent double-booking of same room: + room = self.rooms[room_number] + if not room.availability: + raise RuntimeError("Room is not available.") + + res = Reservation(guest, room, stay_duration_days=stay_duration_days) + res.book() + self.reservations.append(res) + self.save_data() + return res + + def modify_reservation(self, reservation: Reservation, new_days: int) -> None: + reservation.update_stay_duration(new_days) + self.save_data() + + def cancel_reservation(self, reservation: Reservation) -> None: + reservation.cancel() + # remove it from our list + self.reservations = [r for r in self.reservations if r is not reservation] + self.save_data() diff --git a/reservation.py b/reservation.py new file mode 100644 index 0000000..158c505 --- /dev/null +++ b/reservation.py @@ -0,0 +1,85 @@ +from datetime import date, timedelta +from typing import Any +from room import Room +from guest import Guest + +class Reservation: + + + def __init__( + self, + guest: Guest,room: Room,stay_duration_days: int | None = None, + check_in_date: date | None = None,check_out_date: date | None = None): + if check_in_date and check_out_date: + # loaded from CSV + self._check_in_date = check_in_date + self._check_out_date = check_out_date + elif stay_duration_days is not None: + today = date.today() + self._check_in_date = today + self._check_out_date = today + timedelta(days=stay_duration_days) + else: + raise ValueError("Must supply either stay_duration_days or explicit dates") + + self._guest = guest + self._room = room + self._is_active = False + + @property + def guest(self) -> Guest: + return self._guest + + @property + def room(self) -> Room: + return self._room + + @property + def check_in_date(self) -> date: + return self._check_in_date + + @property + def check_out_date(self) -> date: + return self._check_out_date + + @property + def is_active(self) -> bool: + return self._is_active + + def book(self) -> None: + if not self._room.availability: + raise RuntimeError(f"Room {self._room.room_number} is not available.") + self._room.availability = False + self._is_active = True + + def cancel(self) -> None: + if not self._is_active: + raise RuntimeError("Cannot cancel an inactive reservation.") + self._room.availability = True + self._is_active = False + + def update_stay_duration(self, new_days: int) -> None: + if not self._is_active: + raise RuntimeError("Cannot modify an inactive reservation.") + self._check_out_date = self._check_in_date + timedelta(days=new_days) + + def to_dict(self) -> dict[str, Any]: + return { + "Guest ID": self._guest.guest_id, + "Guest Name": self._guest.name, + "Room Number": self._room.room_number, + "Check In Date": self._check_in_date.isoformat(), + "Check Out Date": self._check_out_date.isoformat(), + "Is Active": self._is_active + } + + def __str__(self) -> str: + ci = self._check_in_date.strftime("%d/%m/%Y") + co = self._check_out_date.strftime("%d/%m/%Y") + status = "Active" if self._is_active else "Inactive" + return ( + f"Reservation[{self._guest.guest_id}→{self._room.room_number}] " + f"{ci} to {co} ({status})" + ) + + def __repr__(self) -> str: + return str(self) diff --git a/reservations.csv b/reservations.csv new file mode 100644 index 0000000..9b4a783 --- /dev/null +++ b/reservations.csv @@ -0,0 +1,5 @@ +Guest ID,Guest Name,Room Number,Check In Date,Check Out Date,Is Active +4888c255,Muhammad,290,2025-05-15,2025-05-17,True +f43b215d,Almukhtar,30,2025-05-15,2025-05-20,True +21fa4ece,Lele,270,2025-05-15,2025-05-18,True +21fa4ece,Lele,240,2025-05-16,2025-05-18,True diff --git a/room.py b/room.py new file mode 100644 index 0000000..c97d1e9 --- /dev/null +++ b/room.py @@ -0,0 +1,43 @@ +class Room: + + + def __init__(self, room_number: int, room_type: str, price: float, availability: bool = True): + self._room_number = room_number + self._room_type = room_type + self._price = price + self._availability = availability + + @property + def room_number(self) -> int: + return self._room_number + + @property + def room_type(self) -> str: + return self._room_type + + @property + def price(self) -> float: + return self._price + + @property + def availability(self) -> bool: + return self._availability + + @availability.setter + def availability(self, val: bool): + self._availability = val + + def to_dict(self) -> dict: + return { + "Room Number": self._room_number, + "Room Type": self._room_type, + "Price": self._price, + "Availability": self._availability + } + + def __str__(self) -> str: + status = "Available" if self._availability else "Occupied" + return f"Room {self._room_number} ({self._room_type}) — {status} @ ${self._price:.2f}" + + def __repr__(self) -> str: + return str(self) diff --git a/rooms.csv b/rooms.csv new file mode 100644 index 0000000..4fa5e21 --- /dev/null +++ b/rooms.csv @@ -0,0 +1,51 @@ +Room Number,Room Type,Price,Availability +0,Standard,250.0,True +5,Standard,250.0,True +10,Standard,250.0,True +15,Standard,250.0,True +20,Standard,250.0,True +25,Standard,250.0,True +30,Standard,250.0,False +35,Standard,250.0,True +40,Standard,250.0,True +45,Standard,250.0,True +50,Standard,250.0,True +55,Standard,250.0,True +60,Standard,250.0,True +65,Standard,250.0,True +70,Standard,250.0,True +75,Standard,250.0,True +80,Standard,250.0,True +85,Standard,250.0,True +90,Standard,250.0,True +95,Standard,250.0,True +100,Family,450.0,True +105,Family,450.0,True +110,Family,450.0,True +115,Family,450.0,True +120,Family,450.0,True +125,Family,450.0,True +130,Family,450.0,True +135,Family,450.0,True +140,Family,450.0,True +145,Family,450.0,True +150,Deluxe,700.0,True +155,Deluxe,700.0,True +160,Deluxe,700.0,True +165,Deluxe,700.0,True +170,Deluxe,700.0,True +175,Deluxe,700.0,True +180,Deluxe,700.0,True +185,Deluxe,700.0,True +190,Deluxe,700.0,True +195,Deluxe,700.0,True +200,Penthouse,1500.0,True +210,Penthouse,1500.0,True +220,Penthouse,1500.0,True +230,Penthouse,1500.0,True +240,Penthouse,1500.0,False +250,Penthouse,1500.0,True +260,Penthouse,1500.0,True +270,Penthouse,1500.0,False +280,Penthouse,1500.0,True +290,Penthouse,1500.0,False From 82a4a29581df7c75b4c52a6379181258a254b127 Mon Sep 17 00:00:00 2001 From: Muhammad Tasiu Date: Fri, 16 May 2025 02:46:55 +0100 Subject: [PATCH 2/3] Create README.md --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..693a649 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +============================================================= +HOTEL MANAGEMENT SYSTEM – Python 3 + Tkinter +============================================================= + +A phone-sized desktop app (360 x 640 window) for front-desk staff. + + • Register or log-in guests (fixed 8-character IDs). + • Search available rooms and create new bookings. + • View current bookings for the logged-in guest. + • Modify a booking’s stay length or cancel it. + • Data is persisted in three CSV files (rooms / guests / reservations). + • No external libraries required – just Python 3 and the Tkinter that ships + with CPython. + +------------------------------------------------------------- +1. PROJECT STRUCTURE +------------------------------------------------------------- + hotel-management/ + ├── GUI.py Tkinter front-end (5 stacked frames) + ├── hotel_management.py Data manager + ├── room.py Room entity + ├── guest.py Guest entity + ├── reservation.py Reservation entity + ├── rooms.csv Seeded list of 50 rooms + ├── guests.csv Starts with header only + └── reservations.csv Starts with header only + +------------------------------------------------------------- +2. CSV LAYOUTS +------------------------------------------------------------- + rooms.csv + Room Number , Room Type , Price , Availability (True/False) + + guests.csv + Guest ID , Name , Contact Info + + reservations.csv + Guest ID , Guest Name , Room Number , + Check In Date , Check Out Date , Is Active (True/False) + + All dates are stored in ISO format YYYY-MM-DD. + +------------------------------------------------------------- +3. HOW TO RUN +------------------------------------------------------------- + 1. Install Python 3.10+ (Tkinter is included on Windows/macOS. + On Debian/Ubuntu: sudo apt install python3-tk) + + 2. From the project folder: + python GUI.py + + 3. A 360 x 640 sky-blue window appears. + + • Select an existing guest OR enter Name + Contact and click + “Register & Log In”. + • Main menu shows: + – Search and Book + – View Bookings + – Generate Receipt (modify / cancel booking) + – Logout + • All changes are saved immediately back to the CSV files. + +------------------------------------------------------------- +4. SCREEN SUMMARY +------------------------------------------------------------- + LOGIN + Choose existing guest or register a new one. + + MAIN MENU + Search and Book / View Bookings / Generate Receipt / Logout + + SEARCH & BOOK + Room Type -> Room # -> Stay days -> Book + + VIEW BOOKINGS + Table shows Room, Check-in, Check-out for current guest. + + MODIFY BOOKINGS + Same table + “Modify” button. + • Modify – change stay length. + • Cancel – delete booking and free room. + +------------------------------------------------------------- +5. CODE OVERVIEW +------------------------------------------------------------- + room.py Room(number, type, price, availability) + guest.py Guest(id, name, contact_info) + reservation.py Reservation(book, cancel, update) + hotel_management.py + – loads/saves CSV + – add_guest / make_reservation / modify_reservation / cancel_reservation + GUI.py + – five Tkinter frames (login, menu, book, view, modify) + – phone portrait size, grey theme, styled Treeview grids + +------------------------------------------------------------- From 76ad33e76d2bbcad140943ce7b21e9086c0c1ece Mon Sep 17 00:00:00 2001 From: Muhammad Tasiu Date: Fri, 16 May 2025 10:10:03 +0100 Subject: [PATCH 3/3] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 693a649..de9a1e9 100644 --- a/README.md +++ b/README.md @@ -94,3 +94,6 @@ A phone-sized desktop app (360 x 640 window) for front-desk staff. – phone portrait size, grey theme, styled Treeview grids ------------------------------------------------------------- +Muhammad Muntazar Tasiu 20231725 +Muhammad Lele 20231940 +Almuktar Sani 20230101 \ No newline at end of file