# Dejo Hotel — Management & POS System

A production-grade, offline-capable hotel management system with a public booking website, admin backend, reception module, multi-outlet POS (offline-first), central store/inventory, reporting engine, and thermal receipt printing.

---

## Quick Start

### Prerequisites
- PHP 8.2+ (with `pdo_mysql`, `gd`, `sodium` extensions)
- MySQL 8.0+
- Node.js 20+ / npm 10+
- Composer 2+

### 1. Clone & Install

```bash
cd Dejo-Hotel

# Backend
composer install
cp .env.example .env
# Edit .env: set DB_DATABASE, DB_USERNAME, DB_PASSWORD
php artisan key:generate
php artisan migrate:fresh --seed

# Admin frontend (Laravel + Inertia)
npm install
npm run build

# POS SPA (separate Vue 3 app)
cd pos-spa
npm install
npm run build
cd ..
```

### 2. Serve

**Option A:** Two terminal windows:
```bash
# Terminal 1: Laravel backend
php artisan serve

# Terminal 2: POS SPA (dev mode with API proxy)
cd pos-spa && npm run dev
```

**Option B:** Production mode (serve POS SPA from Laravel):
```bash
php artisan serve
# POS SPA is at http://localhost:8000/pos (serve pos-spa/dist via Laravel)
```

### 3. Login Credentials (seeded)

| Role            | Email                       | Password  |
|-----------------|-----------------------------|-----------|
| Super Admin     | admin@dejohotel.com         | password  |
| Hotel Manager   | manager@dejohotel.com       | password  |
| Receptionist    | reception@dejohotel.com     | password  |
| Store Keeper    | store@dejohotel.com         | password  |
| Restaurant Staff| restaurant@dejohotel.com    | password  |
| Bar Staff       | bar@dejohotel.com           | password  |
| Lounge Staff    | lounge@dejohotel.com        | password  |
| Club Staff      | club@dejohotel.com          | password  |
| Accountant      | accountant@dejohotel.com    | password  |

---

## Architecture

### Stack
- **Backend:** Laravel (latest LTS) + MySQL 8+ + Sanctum (API auth) + Spatie Permissions
- **Admin/Public Frontend:** Laravel + Inertia.js (Vue 3) via one Vite build
- **POS Frontend:** Separate Vue 3 SPA with Vite + Pinia + Dexie.js (offline IndexedDB) + PWA
- **Print:** Browser-native `window.print()` with `@media print` templates (80mm/58mm)

### Directory Structure

```
Dejo-Hotel/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Admin/         # Admin panel controllers
│   │   │   ├── Api/           # POS API controllers
│   │   │   ├── BookingController.php
│   │   │   └── ReceptionController.php
│   ├── Models/                # Eloquent models
│   ├── Observers/             # Push notification observers
│   └── Services/
│       └── PushNotificationService.php
├── config/
│   └── services.php           # WebPush VAPID config
├── database/
│   ├── migrations/            # 32 migrations
│   └── seeders/               # RolePermission, Store, POS, Database
├── resources/
│   ├── js/Pages/              # Inertia Vue pages (Admin, Reception, Reports, Store)
│   └── views/                 # Blade: app layout, PDF export template
├── routes/
│   ├── api.php                # Sanctum-protected API routes
│   └── web.php                # Web routes (public, admin, reception, store)
├── tests/
│   └── Feature/
│       ├── GuestJourneyTest.php   # Full guest lifecycle E2E
│       ├── StockJourneyTest.php   # Full stock lifecycle E2E
│       └── OfflinePOSTest.php     # POS idempotency + payment methods
├── pos-spa/                   # Separate POS SPA project
│   ├── src/
│   │   ├── components/        # Vue components (ReceiptTemplate, InstallPrompt)
│   │   ├── services/api.js    # API client with token management
│   │   ├── stores/            # Pinia stores (auth, pos, shift, offline)
│   │   ├── views/POSView.vue  # Main POS interface
│   │   └── sw.js              # Custom service worker (injectManifest)
│   └── vite.config.js         # PWA + injectManifest config
├── public/
│   ├── sw.js                  # Laravel admin service worker (static)
│   ├── manifest.webmanifest   # Laravel admin PWA manifest
│   ├── icons/                 # SVG icons for admin app PWA
│   └── pos-spa/               # Symlink or copy of built POS SPA
├── lighthouserc.js            # Lighthouse CI config
├── HANDOVER.md                # This file
└── PLAN.md                    # Agent build plan (history)
```

---

## Running Tests

```bash
# Run all E2E tests (3 test files, 6 tests total)
php artisan test --testsuite=Feature

# Run specific journeys
php artisan test --filter=GuestJourneyTest
php artisan test --filter=StockJourneyTest
php artisan test --filter=OfflinePOSTest

# Run all tests including unit tests
php artisan test
```

### What the E2E Tests Cover

| Test | Assertions | What It Proves |
|------|-----------|----------------|
| **GuestJourneyTest** | 34 | Book online → Confirm → Check-in → POS room charge → Check-out + Night Club room charge blocked |
| **StockJourneyTest** | 30 | Goods received → Requisition → Approve → POS sale → Full reconciliation math |
| **OfflinePOSTest** | 38 (4 tests) | UUID idempotency (retry rejected), Night Club blocked, Cash sale receipt (14 fields), Bar transfer sale |

---

## Key API Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/login` | Public | Get Sanctum token |
| GET | `/api/pos/outlets` | `create_sales` | List POS outlets |
| GET | `/api/pos/outlets/{outlet}/menu` | `create_sales` | Menu with prices |
| POST | `/api/pos/transactions` | `create_sales` | Create POS sale (idempotent via `client_uuid`) |
| POST | `/api/pos/outlets/{outlet}/shift/open` | `create_sales` | Open till shift |
| POST | `/api/folio/room-charge` | `post_room_charges` | Post charge to guest folio |
| GET | `/api/reception/search` | `check_in_guests` | Search bookings |
| POST | `/api/push/subscribe` | `auth:sanctum` | Subscribe to push notifications |

### POS Transaction Flow

1. **Client** generates UUID → saves transaction to IndexedDB (Dexie.js)
2. **Client** POSTs to `/api/pos/transactions` with `client_uuid`
3. **Server** validates `client_uuid` is unique → creates transaction → deducts stock → returns receipt
4. If **network fails**, transaction stays in IndexedDB queue
5. On **reconnect**, `online` event listener re-pushes queued transactions
6. If `client_uuid` already exists (retry), **server returns 422** — client discards from queue
7. If **room charge** at Night Club, **server returns 422** (enforced)

---

## PWA & Offline

### POS SPA (pos-spa/)
- **Strategy:** `injectManifest` — custom `sw.js` handles all caching
- **Service Worker:** Background Sync plugin for POS API, NetworkFirst for API, CacheFirst for images, StaleWhileRevalidate for static assets
- **Offline:** IndexedDB queue (Dexie.js) + client UUID idempotency
- **Install:** `InstallPrompt.vue` captures `beforeinstallprompt` event
- **iOS:** Meta tags for `apple-mobile-web-app-capable`, touch icons, startup image

### Admin App (Laravel)
- **Strategy:** Static `sw.js` + `manifest.webmanifest` in `public/` (no VitePWA plugin)
- **Service Worker:** NetworkFirst for API, CacheFirst for static, offline fallback for navigation
- **InstallPrompt:** Component in `AuthenticatedLayout.vue`

### Lighthouse CI
```bash
# Install and run
npm install -g @lhci/cli
lhci autorun
```
Config: `lighthouserc.js` in project root.

---

## Push Notifications

### Setup VAPID Keys
```bash
# Generate VAPID keys (writes to .env automatically)
php artisan app:generate-vapid-keys
```

### What Triggers Notifications
| Event | Target Permission | What's Sent |
|-------|-------------------|------------|
| New reservation created | `check_in_guests` | "New Booking: DJ-..." |
| Booking confirmed | `check_in_guests` | "Booking confirmed — guest arriving" |
| Outlet stock below reorder | `manage_inventory` | "⚠️ Low Stock Alert: Item Name @ Outlet" |

---

## Default Permissions (per role)

| Role | Key Permissions |
|------|----------------|
| Super Admin | All 53 permissions |
| Hotel Manager | Bookings, rooms, guests, store, reports, users |
| Receptionist | Bookings (approve/reject), check-in/out, POS sales, room charges |
| Store Keeper | Inventory CRUD, suppliers, goods received, requisitions (approve), stock movements |
| Restaurant Staff | POS sales, open/close shift, room charge posting |
| Bar Staff | Same as Restaurant |
| Lounge Staff | Same as Restaurant |
| Club Staff | POS sales only — **NO room charge** |
| Accountant | Read-only reports (sales, bookings, inventory, audit) |

---

## Seed Data Summary

- **9 user accounts** (see credentials table above)
- **9 roles** with 53 granular permissions
- **6 outlets:** Central Store, Mini Mart, Restaurant, VIP Bar, Lounge, Night Club
- **37 menu items** across 4 categories with per-outlet pricing (markups: 1.0x–1.3x)
- **25 stock items** linked to menu items with central (100) and outlet (50) stock
- **5 suppliers** with contact info and payment terms
- **Night Club** explicitly forbids room charge (`allows_room_charge = false`)

---

## Deployment Notes

### Environment Variables (.env)
```env
APP_URL=https://dejohotel.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dejo_hotel
DB_USERNAME=root
DB_PASSWORD=

# VAPID keys for push notifications (run after deployment)
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:admin@dejohotel.com
```

### HTTPS (Required for Service Workers)
- Both apps require HTTPS to register service workers
- For local development, use `php artisan serve` with a proxy or `ngrok`

### Serving POS SPA in Production
```bash
# Build POS SPA
cd pos-spa && npm run build && cd ..

# Copy dist to Laravel public (or symlink)
cp -r pos-spa/dist/* public/pos-spa/

# Laravel route serves /pos from public/pos-spa
```

### Database Migrations
```bash
# Fresh install
php artisan migrate:fresh --seed

# Production with data preservation
php artisan migrate
```

---

## Troubleshooting

### "Nights" column issue
The `reservations.nights` column was changed from a MySQL virtual generated column (`DATEDIFF`) to a regular integer (default 0) for SQLite test compatibility. No application code reads `nights`.

### Vite Plugin PWA removed from Laravel
`vite-plugin-pwa` was removed from the Laravel Inertia app due to a broken transitive dependency (`es-abstract/2024/Call`). The admin app uses static `sw.js` and `manifest.webmanifest` files in `public/` instead.

### RoomType Media type hint
The `RoomType::registerMediaConversions()` method was updated to use the correct `Spatie\MediaLibrary\MediaCollections\Models\Media` type hint. This was causing fatal errors on class load.
