Spaces:
Sleeping
Sleeping
| 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**. | |
| <div class="procelevate-accent" style="font-weight:750; margin-top:8px;"> | |
| Step 1: Verify Booking → Step 2: Confirm Details → Step 3: Get Express Code | |
| </div> | |
| **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() |