import os import json import secrets import shutil from datetime import datetime from typing import Dict, Any, Optional, Tuple import gradio as gr import qrcode from PIL import Image # ============================ # Procelevate Branding (UI) # ============================ PROCELEVATE_BLUE = "#0F2C59" CUSTOM_CSS = f""" /* Primary buttons */ .gr-button.gr-button-primary, button.primary {{ background: {PROCELEVATE_BLUE} !important; border-color: {PROCELEVATE_BLUE} !important; color: white !important; font-weight: 650 !important; }} .gr-button.gr-button-primary:hover, button.primary:hover {{ filter: brightness(0.92); }} /* Tabs: selected tab underline + text color */ button[data-testid="tab-button"][aria-selected="true"] {{ border-bottom: 3px solid {PROCELEVATE_BLUE} !important; color: {PROCELEVATE_BLUE} !important; font-weight: 750 !important; }} /* Links / accent text */ a, .procelevate-accent {{ color: {PROCELEVATE_BLUE} !important; }} /* Subtle modern rounding */ .block, .gr-box, .gr-panel {{ border-radius: 14px !important; }} """ # ============================ # Settings (Demo / Prototype) # ============================ FRONT_DESK_PIN = os.environ.get("FRONT_DESK_PIN", "2580") DATA_DIR = "data" DATA_FILE = os.path.join(DATA_DIR, "checkins.json") UPLOAD_DIR = os.path.join(DATA_DIR, "uploads") # Sample bookings (replace later with PMS integration / uploaded CSV) BOOKINGS = [ { "booking_ref": "RZQ12345", "last_name": "Khan", "checkin_date": "2026-02-10", "first_name": "Aamir", "email": "aamir.khan@example.com", "phone": "+673-8000000", "room_type": "Deluxe King", "nights": 2, }, { "booking_ref": "RZQ67890", "last_name": "Tan", "checkin_date": "2026-02-11", "first_name": "Mei", "email": "mei.tan@example.com", "phone": "+673-8111111", "room_type": "Executive Twin", "nights": 3, }, ] # ============================ # Persistence helpers # ============================ def _ensure_dirs() -> None: os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True) def _load_records() -> Dict[str, Dict[str, Any]]: _ensure_dirs() if not os.path.exists(DATA_FILE): return {} try: with open(DATA_FILE, "r", encoding="utf-8") as f: data = json.load(f) return data if isinstance(data, dict) else {} except Exception: return {} def _save_records(records: Dict[str, Dict[str, Any]]) -> None: _ensure_dirs() with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2) # Load persisted records at startup PRECHECKIN_RECORDS: Dict[str, Dict[str, Any]] = _load_records() # ============================ # Core helpers # ============================ def _find_booking(booking_ref: str, last_name: str, checkin_date: str) -> Optional[Dict[str, Any]]: booking_ref = (booking_ref or "").strip().upper() last_name = (last_name or "").strip().lower() checkin_date = (checkin_date or "").strip() for b in BOOKINGS: if ( b["booking_ref"].upper() == booking_ref and b["last_name"].lower() == last_name and b["checkin_date"] == checkin_date ): return b return None def _generate_code(prefix="RZQ") -> str: alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # avoids I,O,0,1 confusion token = "".join(secrets.choice(alphabet) for _ in range(6)) return f"{prefix}-{token}" def _now() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M") def _qr_image_from_text(text: str) -> Image.Image: qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=2, ) qr.add_data(text) qr.make(fit=True) return qr.make_image(fill_color="black", back_color="white").convert("RGB") def _get_file_path(file_obj) -> str: """ Gradio File may return: - a tempfile-like object with .name - or a string path (depending on Gradio version/config) """ if not file_obj: return "" if isinstance(file_obj, str): return file_obj return getattr(file_obj, "name", "") or "" def _safe_ext(path: str) -> str: _, ext = os.path.splitext(path) ext = (ext or "").lower().strip() if ext in [".png", ".jpg", ".jpeg", ".webp", ".pdf"]: return ext return ".bin" def _save_uploaded_id(code: str, id_file) -> Tuple[bool, str, str]: """ Copies uploaded ID file into persistent folder. Returns: (saved_ok, saved_path, original_name) """ src = _get_file_path(id_file) if not src or not os.path.exists(src): return False, "", "" _ensure_dirs() original_name = os.path.basename(src) ext = _safe_ext(src) dest_name = f"{code}{ext}" dest_path = os.path.join(UPLOAD_DIR, dest_name) try: shutil.copy(src, dest_path) return True, dest_path, original_name except Exception: return False, "", original_name # ============================ # Guest actions # ============================ def verify_booking(booking_ref, last_name, checkin_date): b = _find_booking(booking_ref, last_name, checkin_date) if not b: msg = ( "❌ Booking not found.\n\n" "Please re-check:\n" "• Booking reference\n" "• Last name\n" "• Check-in date\n\n" "If you need help, please contact the front desk." ) return ( gr.update(visible=True, value=msg), gr.update(visible=False), gr.update(value=None), ) summary = ( "✅ Booking verified successfully\n\n" f"Guest: {b['first_name']} {b['last_name']}\n" f"Booking Ref: {b['booking_ref']}\n" f"Check-in Date: {b['checkin_date']}\n" f"Room Type: {b['room_type']}\n" f"Stay Length: {b['nights']} night(s)\n\n" "Proceed to Step 2 to confirm details." ) return ( gr.update(visible=True, value=summary), gr.update(visible=True), gr.update(value=b), ) def complete_checkin(mode, booking_state, arrival_time, bed_pref, special_request, id_type, id_file): if not booking_state: return "❌ Please verify your booking first.", None, "", gr.update(visible=False) id_type = (id_type or "Not provided").strip() has_file = bool(id_file) # Require matching ID type + file if either is provided if id_type != "Not provided" and not has_file: return "❌ Please upload the selected ID proof file to continue.", None, "", gr.update(visible=False) if has_file and id_type == "Not provided": return "❌ Please select the ID proof type (e.g., Passport) to continue.", None, "", gr.update(visible=False) code = _generate_code("RZQ") qr_img = _qr_image_from_text(code) # Save ID to persistent folder (so staff can open it later) saved_ok, saved_path, original_name = (False, "", "") if has_file: saved_ok, saved_path, original_name = _save_uploaded_id(code, id_file) if not saved_ok: # Still allow check-in to proceed, but tell guest staff couldn't store the file # (In a real system you might block; for demo it's okay.) pass record = { "code": code, "mode": mode, "status": "🟢 Pre-Checked-In" if mode == "Pre-Arrival" else "🟢 Checked-In (On-Arrival)", "created_at": _now(), "guest_name": f"{booking_state['first_name']} {booking_state['last_name']}", "booking_ref": booking_state["booking_ref"], "checkin_date": booking_state["checkin_date"], "room_type": booking_state["room_type"], "arrival_time": (arrival_time or "").strip(), "bed_pref": (bed_pref or "").strip(), "special_request": (special_request or "").strip(), # ID proof fields "id_type": id_type if id_type != "Not provided" else "", "id_provided": has_file, "id_original_name": original_name if has_file else "", "id_saved_path": saved_path if has_file else "", "id_saved_ok": bool(saved_ok) if has_file else False, } PRECHECKIN_RECORDS[code] = record _save_records(PRECHECKIN_RECORDS) id_line = "" if record["id_provided"]: if record["id_saved_ok"]: id_line = f"\nID Proof: {record['id_type']} uploaded ✅" else: id_line = f"\nID Proof: {record['id_type']} uploaded (storage issue ⚠️)" guest_msg = ( f"✅ {mode} Check-In Successful\n\n" f"Your Express Check-In Code:\n{code}\n" f"{id_line}\n\n" "Next step:\n" "• Show this code (or QR) at the front desk for quick room key collection.\n" "• Estimated counter time: under 1 minute.\n\n" "Thank you — we look forward to welcoming you." ) staff_view = ( "✅ FRONT DESK SUMMARY\n\n" f"Code: {record['code']}\n" f"Status: {record['status']}\n" f"Guest: {record['guest_name']}\n" f"Booking Ref: {record['booking_ref']}\n" f"Check-in Date: {record['checkin_date']}\n" f"Room Type: {record['room_type']}\n" f"Arrival Time: {record['arrival_time']}\n" f"Preference: {record['bed_pref']}\n" f"Special Request: {record['special_request']}\n" f"ID Proof: {record['id_type'] if record['id_provided'] else 'Not provided'}\n" f"Submitted At: {record['created_at']}\n" ) return guest_msg, qr_img, staff_view, gr.update(visible=True) def reset_form(): return ( "", "", "", gr.update(value="", visible=False), gr.update(visible=False), None, "Pre-Arrival", "", "No preference", "", "Not provided", None, "", None, "", gr.update(visible=False), ) # ============================ # Front desk actions (PIN gated) # ============================ def staff_unlock(entered_pin: str): entered_pin = (entered_pin or "").strip() if entered_pin == FRONT_DESK_PIN: return gr.update(visible=False), gr.update(visible=True), "✅ Access granted." return gr.update(visible=True), gr.update(visible=False), "❌ Incorrect PIN." def staff_lookup(code): code = (code or "").strip().upper() rec = PRECHECKIN_RECORDS.get(code) if not rec: return "❌ Code not found. Please check the code or ask guest to re-submit.", None id_path = rec.get("id_saved_path", "") id_visible = id_path if (id_path and os.path.exists(id_path)) else None view = ( "✅ Record found\n\n" f"Code: {rec.get('code','')}\n" f"Status: {rec.get('status','')}\n" f"Guest: {rec.get('guest_name','')}\n" f"Booking Ref: {rec.get('booking_ref','')}\n" f"Check-in Date: {rec.get('checkin_date','')}\n" f"Room Type: {rec.get('room_type','')}\n" f"Arrival Time: {rec.get('arrival_time','')}\n" f"Preference: {rec.get('bed_pref','')}\n" f"Special Request: {rec.get('special_request','')}\n" f"ID Proof: {rec.get('id_type','') if rec.get('id_provided') else 'Not provided'}\n" f"ID File Stored: {'Yes' if id_visible else 'No'}\n" f"Submitted At: {rec.get('created_at','')}\n" ) return view, id_visible def staff_clear_all(entered_pin: str): entered_pin = (entered_pin or "").strip() if entered_pin != FRONT_DESK_PIN: return "❌ Incorrect PIN. Cannot clear records." # Clear JSON records PRECHECKIN_RECORDS.clear() _save_records(PRECHECKIN_RECORDS) # Optional: clear uploads _ensure_dirs() try: for fn in os.listdir(UPLOAD_DIR): fp = os.path.join(UPLOAD_DIR, fn) if os.path.isfile(fp): os.remove(fp) except Exception: pass return "✅ All stored check-in records and uploaded IDs cleared." # ============================ # UI # ============================ with gr.Blocks(title="Smart Self Check-In (Prototype)", css=CUSTOM_CSS) as demo: gr.Markdown( """ # 🏨 Smart Self Check-In (Prototype) Complete your check-in in under **2 minutes** — **Pre-Arrival** or **On-Arrival**.