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**.
Step 1: Verify Booking → Step 2: Confirm Details → Step 3: Get Express Code
**Note:** This is a demonstration prototype using sample data to showcase experience and feasibility. """ ) # ---------------------------- # Guest Tab # ---------------------------- with gr.Tab("Guest Self Check-In"): mode = gr.Radio(["Pre-Arrival", "On-Arrival"], value="Pre-Arrival", label="Check-In Mode") gr.Markdown("### Step 1: Verify your booking") with gr.Row(): booking_ref = gr.Textbox(label="Booking Reference", placeholder="e.g., RZQ12345") last_name = gr.Textbox(label="Last Name", placeholder="e.g., Khan") checkin_date = gr.Textbox(label="Check-in Date (YYYY-MM-DD)", placeholder="e.g., 2026-02-10") verify_btn = gr.Button("Verify & Continue →", variant="primary") verify_result = gr.Textbox(label="Verification Result", visible=False, lines=7) booking_state = gr.State(None) details_box = gr.Accordion("Step 2: Confirm check-in details", open=True, visible=False) with details_box: arrival_time = gr.Textbox(label="Expected Arrival Time (optional)", placeholder="e.g., 18:30") bed_pref = gr.Dropdown( ["No preference", "King", "Twin", "High floor", "Near elevator (if available)"], value="No preference", label="Preference (optional)", ) special_request = gr.Textbox( label="Special Requests (optional)", placeholder="e.g., baby cot, late check-out request, allergy notes", lines=3, ) gr.Markdown("#### ID Proof (recommended for realistic flow)") with gr.Row(): id_type = gr.Dropdown( ["Not provided", "Passport", "National ID", "Driving License", "Other"], value="Not provided", label="ID Proof Type", ) id_file = gr.File(label="Upload ID Proof (image or PDF)", file_types=["image", "pdf"]) submit_btn = gr.Button("Submit Check-In", variant="primary") gr.Markdown("### Step 3: Receive your Express Check-In Code") guest_msg = gr.Textbox(label="Guest Confirmation", lines=8) qr_display = gr.Image(label="Express Check-In QR Code", type="pil", height=220) staff_preview_container = gr.Column(visible=False) with staff_preview_container: staff_preview = gr.Textbox(label="(Demo) Front Desk Preview", lines=13) reset_btn = gr.Button("Reset") verify_btn.click( verify_booking, inputs=[booking_ref, last_name, checkin_date], outputs=[verify_result, details_box, booking_state], ) submit_btn.click( complete_checkin, inputs=[mode, booking_state, arrival_time, bed_pref, special_request, id_type, id_file], outputs=[guest_msg, qr_display, staff_preview, staff_preview_container], ) reset_btn.click( reset_form, inputs=[], outputs=[ booking_ref, last_name, checkin_date, verify_result, details_box, booking_state, mode, arrival_time, bed_pref, special_request, id_type, id_file, guest_msg, qr_display, staff_preview, staff_preview_container ], ) gr.Markdown( """ #### Sample bookings to test - Booking Ref: **RZQ12345**, Last Name: **Khan**, Date: **2026-02-10** - Booking Ref: **RZQ67890**, Last Name: **Tan**, Date: **2026-02-11** """ ) # ---------------------------- # Front Desk Tab (PIN gated) # ---------------------------- with gr.Tab("Front Desk Validation"): gr.Markdown("### Staff access (PIN protected)") pin_box = gr.Textbox(label="Enter Front Desk PIN", placeholder="PIN", type="password") unlock_btn = gr.Button("Unlock Staff Tools", variant="primary") unlock_status = gr.Markdown("") staff_tools = gr.Column(visible=False) with staff_tools: gr.Markdown("Enter the guest's **Express Check-In Code** to retrieve their record and ID proof.") staff_code = gr.Textbox(label="Express Check-In Code", placeholder="e.g., RZQ-ABC123") staff_lookup_btn = gr.Button("Lookup", variant="primary") staff_view = gr.Textbox(label="Front Desk Result", lines=14) # ✅ This displays the uploaded ID file for staff (download + preview depending on file type) staff_id_file = gr.File(label="Guest ID Proof (uploaded file)", interactive=False) gr.Markdown("—") clear_btn = gr.Button("Clear All Demo Records + Uploaded IDs (PIN required)") clear_result = gr.Markdown("") unlock_btn.click( staff_unlock, inputs=[pin_box], outputs=[pin_box, staff_tools, unlock_status], ) # Lookup returns both text + file path staff_lookup_btn.click( staff_lookup, inputs=[staff_code], outputs=[staff_view, staff_id_file], ) clear_btn.click(staff_clear_all, inputs=[pin_box], outputs=[clear_result]) demo.launch()