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/README.md b/README.md new file mode 100644 index 0000000..de9a1e9 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +============================================================= +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 + +------------------------------------------------------------- +Muhammad Muntazar Tasiu 20231725 +Muhammad Lele 20231940 +Almuktar Sani 20230101 \ No newline at end of file diff --git a/__pycache__/guest.cpython-312.pyc b/__pycache__/guest.cpython-312.pyc new file mode 100644 index 0000000..6ef6359 Binary files /dev/null and b/__pycache__/guest.cpython-312.pyc differ diff --git a/__pycache__/hotel_management.cpython-312.pyc b/__pycache__/hotel_management.cpython-312.pyc new file mode 100644 index 0000000..07f462e Binary files /dev/null and b/__pycache__/hotel_management.cpython-312.pyc differ diff --git a/__pycache__/reservation.cpython-312.pyc b/__pycache__/reservation.cpython-312.pyc new file mode 100644 index 0000000..176f39f Binary files /dev/null and b/__pycache__/reservation.cpython-312.pyc differ diff --git a/__pycache__/room.cpython-312.pyc b/__pycache__/room.cpython-312.pyc new file mode 100644 index 0000000..b969a71 Binary files /dev/null and b/__pycache__/room.cpython-312.pyc differ 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