# JOGLO KOI RESTAURANT MANAGEMENT SYSTEM
## Architecture Plan v1.0

---

## 1. BRANDING & DESIGN SYSTEM

### Color Palette
```
Primary:        #1B4332 (Deep Forest Green) — sidebar, header, buttons
Primary Light:  #2D6A4F (Medium Green) — hover states
Secondary:      #D4AF37 (Gold) — accents, logo text, highlights
Background:     #F8F9FA (Light Gray) — main content area
Surface:        #FFFFFF (White) — cards, modals, tables
Text Primary:   #1A1A2E (Near Black) — headings, body
Text Secondary: #6C757D (Gray) — labels, descriptions
Border:         #E9ECEF (Light Border)
Success:        #28A745
Warning:        #FFC107
Danger:         #DC3545
Info:           #17A2B8
```

### Typography
```
Headings:   'Playfair Display' (serif) — elegan, premium
Body Text:  'Inter' (sans-serif) — clean, readable
Monospace:  'JetBrains Mono' — untuk nomor order, kode
```

### UI Principles
- Light mode only, tidak monoton
- Card-based layout dengan subtle shadows
- Warm neutral tones (cream/ivory accents)
- Generous white space
- Touch-friendly buttons (min 44px tap target)
- Consistent icon set: Lucide Icons

---

## 2. FOLDER STRUCTURE

```
joglo-koi-rms/
├── public/                          # Document root
│   ├── index.php                    # Entry point / router
│   ├── .htaccess                    # URL rewriting
│   ├── assets/
│   │   ├── css/
│   │   │   ├── app.css              # Main styles
│   │   │   ├── pos.css              # POS-specific styles
│   │   │   └── responsive.css       # Responsive overrides
│   │   ├── js/
│   │   │   ├── app.js               # Main JS (auth, CSRF, utils)
│   │   │   ├── pos.js               # POS logic (including offline)
│   │   │   ├── kitchen.js           # Kitchen display polling
│   │   │   └── dashboard.js         # Dashboard charts
│   │   ├── images/
│   │   │   └── logo.png
│   │   └── fonts/
│   └── uploads/                     # User uploads (menu photos, etc.)
│       ├── menu/
│       ├── employees/
│       └── .htaccess                 # Deny PHP execution
│
├── app/
│   ├── config/
│   │   ├── database.php             # DB connection (PDO)
│   │   ├── app.php                  # App settings
│   │   └── constants.php            # App constants
│   │
│   ├── core/
│   │   ├── Router.php               # Simple router
│   │   ├── Controller.php           # Base controller
│   │   ├── Model.php                # Base model (PDO wrapper)
│   │   ├── Middleware.php            # Base middleware
│   │   ├── Request.php              # Request handler
│   │   ├── Response.php             # JSON/HTML response
│   │   ├── Session.php              # Session manager
│   │   └── Database.php             # PDO singleton
│   │
│   ├── middleware/
│   │   ├── AuthMiddleware.php        # Login check
│   │   ├── RoleMiddleware.php        # Role check
│   │   ├── PermissionMiddleware.php  # Granular permission check
│   │   ├── CSRFMiddleware.php        # CSRF token validation
│   │   └── RateLimitMiddleware.php   # Rate limiting
│   │
│   ├── controllers/
│   │   ├── AuthController.php
│   │   ├── DashboardController.php
│   │   ├── MenuController.php
│   │   ├── CategoryController.php
│   │   ├── TableController.php
│   │   ├── ReservationController.php
│   │   ├── POSController.php
│   │   ├── OrderController.php
│   │   ├── KitchenController.php
│   │   ├── PaymentController.php
│   │   ├── InventoryController.php
│   │   ├── RecipeController.php
│   │   ├── SupplierController.php
│   │   ├── PurchaseController.php
│   │   ├── ExpenseController.php
│   │   ├── EmployeeController.php
│   │   ├── AttendanceController.php
│   │   ├── PayrollController.php
│   │   ├── ReportController.php
│   │   ├── UserController.php
│   │   ├── SettingController.php
│   │   ├── NotificationController.php
│   │   └── ExportController.php
│   │
│   ├── models/
│   │   ├── User.php
│   │   ├── Role.php
│   │   ├── Permission.php
│   │   ├── Employee.php
│   │   ├── Menu.php
│   │   ├── MenuCategory.php
│   │   ├── MenuModifier.php
│   │   ├── MenuVariant.php
│   │   ├── RestaurantTable.php
│   │   ├── TableArea.php
│   │   ├── Reservation.php
│   │   ├── Order.php
│   │   ├── OrderItem.php
│   │   ├── Payment.php
│   │   ├── Ingredient.php
│   │   ├── IngredientCategory.php
│   │   ├── Recipe.php
│   │   ├── RecipeItem.php
│   │   ├── Inventory.php
│   │   ├── InventoryMovement.php
│   │   ├── StockOpname.php
│   │   ├── StockOpnameItem.php
│   │   ├── Waste.php
│   │   ├── Supplier.php
│   │   ├── PurchaseOrder.php
│   │   ├── PurchaseOrderItem.php
│   │   ├── GoodsReceipt.php
│   │   ├── GoodsReceiptItem.php
│   │   ├── Expense.php
│   │   ├── ExpenseCategory.php
│   │   ├── Customer.php
│   │   ├── Notification.php
│   │   ├── AuditLog.php
│   │   └── Setting.php
│   │
│   ├── services/
│   │   ├── AuthService.php
│   │   ├── OrderService.php
│   │   ├── PaymentService.php
│   │   ├── InventoryService.php
│   │   ├── RecipeService.php
│   │   ├── StockService.php
│   │   ├── PayrollService.php
│   │   ├── ReportService.php
│   │   ├── ExportService.php
│   │   └── NotificationService.php
│   │
│   ├── helpers/
│   │   ├── url.php                  # URL helper
│   │   ├── format.php               # Number, currency, date formatting
│   │   ├── validation.php           # Input validation
│   │   ├── sanitize.php             # XSS prevention
│   │   ├── csrf.php                 # CSRF token
│   │   ├── auth.php                 # Auth helper functions
│   │   └── upload.php               # File upload handler
│   │
│   └── views/
│       ├── layouts/
│       │   ├── auth.php             # Login layout
│       │   ├── admin.php            # Main admin layout (sidebar + topbar)
│       │   └── pos.php              # POS layout (minimal)
│       │
│       ├── auth/
│       │   └── login.php
│       │
│       ├── dashboard/
│       │   └── index.php
│       │
│       ├── menu/
│       │   ├── index.php
│       │   ├── form.php
│       │   └── category.php
│       │
│       ├── table/
│       │   ├── index.php            # Visual table layout
│       │   └── form.php
│       │
│       ├── reservation/
│       │   ├── index.php
│       │   ├── calendar.php
│       │   └── form.php
│       │
│       ├── pos/
│       │   └── index.php            # Full-screen POS
│       │
│       ├── order/
│       │   ├── index.php
│       │   └── detail.php
│       │
│       ├── kitchen/
│       │   └── index.php            # Kitchen Display System
│       │
│       ├── inventory/
│       │   ├── index.php
│       │   ├── movement.php
│       │   ├── opname.php
│       │   └── waste.php
│       │
│       ├── purchase/
│       │   ├── index.php
│       │   ├── form.php
│       │   └── receiving.php
│       │
│       ├── supplier/
│       │   ├── index.php
│       │   └── form.php
│       │
│       ├── expense/
│       │   ├── index.php
│       │   └── form.php
│       │
│       ├── employee/
│       │   ├── index.php
│       │   ├── form.php
│       │   └── attendance.php
│       │
│       ├── payroll/
│       │   ├── index.php
│       │   ├── process.php
│       │   └── payslip.php
│       │
│       ├── report/
│       │   ├── sales.php
│       │   ├── expense.php
│       │   ├── profit.php
│       │   ├── inventory.php
│       │   └── payroll.php
│       │
│       ├── user/
│       │   ├── index.php
│       │   └── form.php
│       │
│       ├── setting/
│       │   └── index.php
│       │
│       ├── notification/
│       │   └── index.php
│       │
│       ├── audit/
│       │   └── index.php
│       │
│       └── components/
│           ├── sidebar.php
│           ├── topbar.php
│           ├── modal.php
│           ├── table.php
│           ├── pagination.php
│           ├── toast.php
│           ├── badge.php
│           ├── empty_state.php
│           └── loading.php
│
├── routes/
│   └── web.php                      # Route definitions
│
├── storage/
│   ├── logs/
│   │   └── app.log
│   ├── backups/
│   └── cache/
│
├── database/
│   ├── schema.sql                   # Full database schema
│   └── seed.sql                     # Demo/seed data
│
├── tests/
│   ├── manual/
│   │   └── TEST_CHECKLIST.md        # Manual testing checklist
│   └── scripts/
│       ├── test_auth.php            # Auth test script
│       ├── test_order.php           # Order flow test
│       ├── test_inventory.php       # Inventory test
│       └── test_payment.php         # Payment test
│
├── .env.example                     # Environment template
├── .htaccess                        # Root .htaccess
├── composer.json                    # PhpSpreadsheet dependency
├── config.php                       # Bootstrap config
├── index.php                        # Root entry (redirect to public)
└── README.md
```

---

## 3. DATABASE SCHEMA (ERD)

### Core Tables

```sql
-- =============================================
-- AUTH & USERS
-- =============================================

CREATE TABLE roles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL UNIQUE,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE permissions (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL UNIQUE,  -- e.g. 'orders.create', 'inventory.view'
    module VARCHAR(50) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE role_permissions (
    role_id INT NOT NULL,
    permission_id INT NOT NULL,
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    avatar VARCHAR(255),
    role_id INT NOT NULL,
    is_active TINYINT(1) DEFAULT 1,
    last_login_at TIMESTAMP NULL,
    login_attempts INT DEFAULT 0,
    locked_until TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- =============================================
-- RESTAURANT STRUCTURE
-- =============================================

CREATE TABLE table_areas (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,  -- 'Area Joglo', 'Area Kolam Koi', 'Area Taman'
    description TEXT,
    sort_order INT DEFAULT 0,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE restaurant_tables (
    id INT PRIMARY KEY AUTO_INCREMENT,
    area_id INT NOT NULL,
    number VARCHAR(10) NOT NULL UNIQUE,
    name VARCHAR(100),  -- 'Meja Joglo 1', 'Meja Koi VIP'
    capacity INT NOT NULL DEFAULT 4,
    position_x INT DEFAULT 0,  -- for visual layout
    position_y INT DEFAULT 0,
    status ENUM('available','reserved','occupied','cleaning','maintenance') DEFAULT 'available',
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (area_id) REFERENCES table_areas(id)
);

-- =============================================
-- MENU
-- =============================================

CREATE TABLE menu_categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    icon VARCHAR(50),
    sort_order INT DEFAULT 0,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

CREATE TABLE menus (
    id INT PRIMARY KEY AUTO_INCREMENT,
    category_id INT NOT NULL,
    sku VARCHAR(50) UNIQUE,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(12,2) NOT NULL DEFAULT 0,
    cost_price DECIMAL(12,2) DEFAULT 0,  -- HPP
    photo VARCHAR(255),
    is_available TINYINT(1) DEFAULT 1,
    is_active TINYINT(1) DEFAULT 1,
    sort_order INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    FOREIGN KEY (category_id) REFERENCES menu_categories(id)
);

CREATE TABLE menu_modifiers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    menu_id INT NOT NULL,
    name VARCHAR(100) NOT NULL,  -- 'Level Pedas', 'Topping'
    type ENUM('single','multiple') DEFAULT 'single',
    is_required TINYINT(1) DEFAULT 0,
    sort_order INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (menu_id) REFERENCES menus(id) ON DELETE CASCADE
);

CREATE TABLE menu_modifier_options (
    id INT PRIMARY KEY AUTO_INCREMENT,
    modifier_id INT NOT NULL,
    name VARCHAR(100) NOT NULL,  -- 'Level 1', 'Level 2', 'Extra Cheese'
    price_adjustment DECIMAL(12,2) DEFAULT 0,
    is_default TINYINT(1) DEFAULT 0,
    sort_order INT DEFAULT 0,
    FOREIGN KEY (modifier_id) REFERENCES menu_modifiers(id) ON DELETE CASCADE
);

-- =============================================
-- CUSTOMERS
-- =============================================

CREATE TABLE customers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    email VARCHAR(100),
    notes TEXT,
    total_orders INT DEFAULT 0,
    total_spending DECIMAL(14,2) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

-- =============================================
-- RESERVATIONS
-- =============================================

CREATE TABLE reservations (
    id INT PRIMARY KEY AUTO_INCREMENT,
    reservation_number VARCHAR(20) NOT NULL UNIQUE,  -- RES-YYYYMMDD-XXXX
    customer_id INT,
    customer_name VARCHAR(100) NOT NULL,
    customer_phone VARCHAR(20),
    reservation_date DATE NOT NULL,
    reservation_time TIME NOT NULL,
    duration_minutes INT DEFAULT 120,
    party_size INT NOT NULL,
    table_id INT,
    notes TEXT,
    special_requests TEXT,
    status ENUM('pending','confirmed','arrived','seated','completed','cancelled','no_show') DEFAULT 'pending',
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(id),
    FOREIGN KEY (table_id) REFERENCES restaurant_tables(id),
    FOREIGN KEY (created_by) REFERENCES users(id)
);

-- =============================================
-- ORDERS
-- =============================================

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_number VARCHAR(20) NOT NULL UNIQUE,  -- KOI-YYYYMMDD-XXXX
    customer_id INT,
    customer_name VARCHAR(100),
    table_id INT,
    user_id INT NOT NULL,  -- cashier
    order_type ENUM('dine_in','takeaway','delivery') DEFAULT 'dine_in',
    status ENUM('draft','pending','preparing','ready','served','completed','cancelled') DEFAULT 'draft',
    subtotal DECIMAL(14,2) DEFAULT 0,
    discount_amount DECIMAL(14,2) DEFAULT 0,
    discount_type ENUM('fixed','percentage') NULL,
    tax_amount DECIMAL(14,2) DEFAULT 0,
    service_charge DECIMAL(14,2) DEFAULT 0,
    total DECIMAL(14,2) DEFAULT 0,
    notes TEXT,
    is_synced TINYINT(1) DEFAULT 1,  -- for offline mode
    synced_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id),
    FOREIGN KEY (table_id) REFERENCES restaurant_tables(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE order_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    menu_id INT NOT NULL,
    menu_name VARCHAR(200) NOT NULL,  -- snapshot
    menu_price DECIMAL(12,2) NOT NULL,  -- snapshot
    quantity INT NOT NULL DEFAULT 1,
    modifier_text TEXT,  -- 'Level 3, Extra Cheese'
    modifier_price DECIMAL(12,2) DEFAULT 0,
    subtotal DECIMAL(14,2) NOT NULL,
    notes TEXT,
    status ENUM('pending','preparing','ready','served','cancelled') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (menu_id) REFERENCES menus(id)
);

-- =============================================
-- PAYMENTS
-- =============================================

CREATE TABLE payments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    payment_number VARCHAR(20) NOT NULL UNIQUE,
    order_id INT NOT NULL,
    amount DECIMAL(14,2) NOT NULL,
    method ENUM('cash','qris','bank_transfer','debit','credit','ewallet') NOT NULL,
    reference_number VARCHAR(100),  -- transfer reference, QRIS ID
    status ENUM('pending','paid','partial','refunded','failed') DEFAULT 'pending',
    paid_at TIMESTAMP NULL,
    created_by INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (created_by) REFERENCES users(id)
);

CREATE TABLE refunds (
    id INT PRIMARY KEY AUTO_INCREMENT,
    refund_number VARCHAR(20) NOT NULL UNIQUE,
    order_id INT NOT NULL,
    payment_id INT NOT NULL,
    amount DECIMAL(14,2) NOT NULL,
    reason TEXT NOT NULL,
    status ENUM('pending','approved','rejected','completed') DEFAULT 'pending',
    approved_by INT,
    approved_at TIMESTAMP NULL,
    created_by INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (payment_id) REFERENCES payments(id),
    FOREIGN KEY (created_by) REFERENCES users(id),
    FOREIGN KEY (approved_by) REFERENCES users(id)
);

-- =============================================
-- INVENTORY
-- =============================================

CREATE TABLE ingredient_categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

CREATE TABLE units (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,  -- Kg, Gram, Liter, Ml, Pcs, etc.
    abbreviation VARCHAR(10) NOT NULL,
    base_unit VARCHAR(10),  -- for conversion
    conversion_factor DECIMAL(10,4) DEFAULT 1
);

CREATE TABLE ingredients (
    id INT PRIMARY KEY AUTO_INCREMENT,
    code VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(200) NOT NULL,
    category_id INT,
    unit_id INT NOT NULL,
    supplier_id INT,
    purchase_price DECIMAL(12,2) DEFAULT 0,
    minimum_stock DECIMAL(10,2) DEFAULT 0,
    maximum_stock DECIMAL(10,2) DEFAULT 0,
    current_stock DECIMAL(10,2) DEFAULT 0,
    warehouse_location VARCHAR(100),
    expiry_date DATE,
    batch_number VARCHAR(100),
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL,
    FOREIGN KEY (category_id) REFERENCES ingredient_categories(id),
    FOREIGN KEY (unit_id) REFERENCES units(id)
);

CREATE TABLE warehouses (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,  -- 'Gudang Utama', 'Dapur', 'Bar'
    description TEXT,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE inventory_movements (
    id INT PRIMARY KEY AUTO_INCREMENT,
    ingredient_id INT NOT NULL,
    warehouse_id INT DEFAULT 1,
    type ENUM('purchase','usage','waste','adjustment','opname','transfer_in','transfer_out') NOT NULL,
    reference_type VARCHAR(50),  -- 'purchase_order', 'order', 'waste', 'opname'
    reference_id INT,
    quantity_in DECIMAL(10,2) DEFAULT 0,
    quantity_out DECIMAL(10,2) DEFAULT 0,
    balance_after DECIMAL(10,2) NOT NULL,
    unit_cost DECIMAL(12,2) DEFAULT 0,
    notes TEXT,
    user_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (ingredient_id) REFERENCES ingredients(id),
    FOREIGN KEY (warehouse_id) REFERENCES warehouses(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- =============================================
-- RECIPES
-- =============================================

CREATE TABLE recipes (
    id INT PRIMARY KEY AUTO_INCREMENT,
    menu_id INT NOT NULL UNIQUE,
    description TEXT,
    estimated_cost DECIMAL(12,2) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (menu_id) REFERENCES menus(id) ON DELETE CASCADE
);

CREATE TABLE recipe_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    recipe_id INT NOT NULL,
    ingredient_id INT NOT NULL,
    quantity DECIMAL(10,4) NOT NULL,
    unit_id INT NOT NULL,
    notes TEXT,
    FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
    FOREIGN KEY (ingredient_id) REFERENCES ingredients(id),
    FOREIGN KEY (unit_id) REFERENCES units(id)
);

-- =============================================
-- STOCK OPNAME
-- =============================================

CREATE TABLE stock_opnames (
    id INT PRIMARY KEY AUTO_INCREMENT,
    opname_number VARCHAR(20) NOT NULL UNIQUE,
    warehouse_id INT DEFAULT 1,
    opname_date DATE NOT NULL,
    status ENUM('draft','completed','approved') DEFAULT 'draft',
    notes TEXT,
    created_by INT NOT NULL,
    approved_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (warehouse_id) REFERENCES warehouses(id),
    FOREIGN KEY (created_by) REFERENCES users(id),
    FOREIGN KEY (approved_by) REFERENCES users(id)
);

CREATE TABLE stock_opname_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    opname_id INT NOT NULL,
    ingredient_id INT NOT NULL,
    system_stock DECIMAL(10,2) NOT NULL,
    actual_stock DECIMAL(10,2) NOT NULL,
    variance DECIMAL(10,2) GENERATED ALWAYS AS (actual_stock - system_stock) STORED,
    reason TEXT,
    FOREIGN KEY (opname_id) REFERENCES stock_opnames(id) ON DELETE CASCADE,
    FOREIGN KEY (ingredient_id) REFERENCES ingredients(id)
);

-- =============================================
-- WASTE
-- =============================================

CREATE TABLE wastes (
    id INT PRIMARY KEY AUTO_INCREMENT,
    waste_number VARCHAR(20) NOT NULL UNIQUE,
    ingredient_id INT NOT NULL,
    quantity DECIMAL(10,2) NOT NULL,
    reason ENUM('expired','damaged','spoiled','wrong_order','spilled','other') NOT NULL,
    notes TEXT,
    estimated_loss DECIMAL(12,2) DEFAULT 0,
    status ENUM('pending','approved') DEFAULT 'pending',
    user_id INT NOT NULL,
    approved_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (ingredient_id) REFERENCES ingredients(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (approved_by) REFERENCES users(id)
);

-- =============================================
-- SUPPLIER & PURCHASE
-- =============================================

CREATE TABLE suppliers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    contact_person VARCHAR(100),
    phone VARCHAR(20),
    whatsapp VARCHAR(20),
    email VARCHAR(100),
    address TEXT,
    npwp VARCHAR(30),
    bank_name VARCHAR(50),
    bank_account VARCHAR(50),
    bank_account_name VARCHAR(100),
    notes TEXT,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

CREATE TABLE purchase_orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    po_number VARCHAR(20) NOT NULL UNIQUE,
    supplier_id INT NOT NULL,
    order_date DATE NOT NULL,
    expected_date DATE,
    status ENUM('draft','ordered','partial','received','completed','cancelled') DEFAULT 'draft',
    subtotal DECIMAL(14,2) DEFAULT 0,
    tax_amount DECIMAL(14,2) DEFAULT 0,
    total DECIMAL(14,2) DEFAULT 0,
    notes TEXT,
    created_by INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (supplier_id) REFERENCES suppliers(id),
    FOREIGN KEY (created_by) REFERENCES users(id)
);

CREATE TABLE purchase_order_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    po_id INT NOT NULL,
    ingredient_id INT NOT NULL,
    quantity DECIMAL(10,2) NOT NULL,
    unit_price DECIMAL(12,2) NOT NULL,
    subtotal DECIMAL(14,2) NOT NULL,
    received_quantity DECIMAL(10,2) DEFAULT 0,
    FOREIGN KEY (po_id) REFERENCES purchase_orders(id) ON DELETE CASCADE,
    FOREIGN KEY (ingredient_id) REFERENCES ingredients(id)
);

CREATE TABLE goods_receipts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    receipt_number VARCHAR(20) NOT NULL UNIQUE,
    po_id INT NOT NULL,
    received_date DATE NOT NULL,
    notes TEXT,
    received_by INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (po_id) REFERENCES purchase_orders(id),
    FOREIGN KEY (received_by) REFERENCES users(id)
);

-- =============================================
-- EXPENSES
-- =============================================

CREATE TABLE expense_categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,  -- Listrik, Air, Gas, Internet, etc.
    description TEXT,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE expenses (
    id INT PRIMARY KEY AUTO_INCREMENT,
    expense_number VARCHAR(20) NOT NULL UNIQUE,
    category_id INT NOT NULL,
    amount DECIMAL(14,2) NOT NULL,
    description TEXT,
    expense_date DATE NOT NULL,
    receipt_photo VARCHAR(255),
    payment_method ENUM('cash','bank_transfer','qris') DEFAULT 'cash',
    status ENUM('pending','approved','rejected') DEFAULT 'pending',
    user_id INT NOT NULL,
    approved_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES expense_categories(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (approved_by) REFERENCES users(id)
);

-- =============================================
-- EMPLOYEE & HR
-- =============================================

CREATE TABLE employees (
    id INT PRIMARY KEY AUTO_INCREMENT,
    employee_code VARCHAR(20) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    photo VARCHAR(255),
    phone VARCHAR(20),
    address TEXT,
    position VARCHAR(100),
    join_date DATE NOT NULL,
    status ENUM('active','inactive','resigned') DEFAULT 'active',
    basic_salary DECIMAL(14,2) DEFAULT 0,
    salary_type ENUM('monthly','weekly','daily') DEFAULT 'monthly',
    bank_name VARCHAR(50),
    bank_account VARCHAR(50),
    bank_account_name VARCHAR(100),
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

CREATE TABLE attendance (
    id INT PRIMARY KEY AUTO_INCREMENT,
    employee_id INT NOT NULL,
    attendance_date DATE NOT NULL,
    clock_in TIME,
    clock_out TIME,
    status ENUM('present','late','absent','leave','holiday') DEFAULT 'present',
    overtime_hours DECIMAL(4,2) DEFAULT 0,
    notes TEXT,
    device_info VARCHAR(200),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_attendance (employee_id, attendance_date),
    FOREIGN KEY (employee_id) REFERENCES employees(id)
);

CREATE TABLE payroll (
    id INT PRIMARY KEY AUTO_INCREMENT,
    payroll_number VARCHAR(20) NOT NULL UNIQUE,
    period_month INT NOT NULL,
    period_year INT NOT NULL,
    status ENUM('draft','calculated','approved','paid') DEFAULT 'draft',
    total_gross DECIMAL(14,2) DEFAULT 0,
    total_deduction DECIMAL(14,2) DEFAULT 0,
    total_net DECIMAL(14,2) DEFAULT 0,
    processed_by INT,
    approved_by INT,
    paid_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (processed_by) REFERENCES users(id),
    FOREIGN KEY (approved_by) REFERENCES users(id)
);

CREATE TABLE payroll_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    payroll_id INT NOT NULL,
    employee_id INT NOT NULL,
    basic_salary DECIMAL(14,2) DEFAULT 0,
    attendance_allowance DECIMAL(14,2) DEFAULT 0,
    overtime_pay DECIMAL(14,2) DEFAULT 0,
    bonus DECIMAL(14,2) DEFAULT 0,
    commission DECIMAL(14,2) DEFAULT 0,
    meal_allowance DECIMAL(14,2) DEFAULT 0,
    transport_allowance DECIMAL(14,2) DEFAULT 0,
    deduction DECIMAL(14,2) DEFAULT 0,
    loan_deduction DECIMAL(14,2) DEFAULT 0,
    absence_deduction DECIMAL(14,2) DEFAULT 0,
    net_salary DECIMAL(14,2) DEFAULT 0,
    status ENUM('pending','paid') DEFAULT 'pending',
    FOREIGN KEY (payroll_id) REFERENCES payroll(id) ON DELETE CASCADE,
    FOREIGN KEY (employee_id) REFERENCES employees(id)
);

-- =============================================
-- SYSTEM
-- =============================================

CREATE TABLE notifications (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,  -- NULL = broadcast to all
    title VARCHAR(200) NOT NULL,
    message TEXT NOT NULL,
    type ENUM('info','warning','danger','success') DEFAULT 'info',
    link VARCHAR(255),
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE audit_logs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    action VARCHAR(50) NOT NULL,
    module VARCHAR(50) NOT NULL,
    record_id INT,
    old_data JSON,
    new_data JSON,
    ip_address VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE settings (
    id INT PRIMARY KEY AUTO_INCREMENT,
    setting_key VARCHAR(100) NOT NULL UNIQUE,
    setting_value TEXT,
    setting_type ENUM('text','number','boolean','json','file') DEFAULT 'text',
    description TEXT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE cash_sessions (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    closed_at TIMESTAMP NULL,
    opening_cash DECIMAL(14,2) DEFAULT 0,
    closing_cash DECIMAL(14,2),
    expected_cash DECIMAL(14,2),
    variance DECIMAL(14,2),
    status ENUM('open','closed') DEFAULT 'open',
    notes TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
```

---

## 4. AUTHENTICATION FLOW

```
User → Login Form (username + password)
  ↓
AuthController::login()
  ↓
Check: is account locked? (login_attempts >= 5)
  ↓ NO
Verify password with password_verify()
  ↓
IF success:
  → Regenerate session ID
  → Set session data (user_id, role, permissions)
  → Reset login_attempts
  → Update last_login_at
  → Log audit: LOGIN
  → Redirect to Dashboard
  ↓
IF fail:
  → Increment login_attempts
  → IF attempts >= 5: lock account for 15 minutes
  → Log audit: LOGIN_FAILED
  → Return error
```

### Session Data Structure
```php
$_SESSION = [
    'user_id' => 1,
    'username' => 'superadmin',
    'full_name' => 'Super Admin',
    'role_id' => 1,
    'role_name' => 'Super Admin',
    'permissions' => ['dashboard.view', 'orders.create', ...],
    'csrf_token' => 'random_token',
    'last_activity' => timestamp,
    'ip_address' => '...',
]
```

### Middleware Flow
```
Request → CSRFMiddleware (validates token for POST/PUT/DELETE)
       → AuthMiddleware (checks session exists + valid)
       → RoleMiddleware (checks role if route requires specific role)
       → PermissionMiddleware (checks granular permission)
       → Controller
```

---

## 5. PERMISSION MATRIX

```php
// Permission naming convention: module.action

$permissions = [
    // Dashboard
    'dashboard.view',

    // Orders
    'orders.view', 'orders.create', 'orders.update', 'orders.cancel', 'orders.refund',

    // POS
    'pos.access', 'pos.process_payment', 'pos.split_bill',

    // Menu
    'menus.view', 'menus.create', 'menus.update', 'menus.delete',
    'categories.view', 'categories.manage',
    'modifiers.view', 'modifiers.manage',

    // Table
    'tables.view', 'tables.manage', 'tables.update_status',

    // Reservation
    'reservations.view', 'reservations.create', 'reservations.update', 'reservations.cancel',

    // Kitchen
    'kitchen.view', 'kitchen.update_status',

    // Inventory
    'inventory.view', 'inventory.receive', 'inventory.adjust', 'inventory.waste',
    'inventory.transfer', 'inventory.opname',

    // Purchase
    'purchases.view', 'purchases.create', 'purchases.update', 'purchases.approve',

    // Supplier
    'suppliers.view', 'suppliers.manage',

    // Expense
    'expenses.view', 'expenses.create', 'expenses.approve',

    // Employee
    'employees.view', 'employees.manage',

    // Attendance
    'attendance.view', 'attendance.manage',

    // Payroll
    'payroll.view', 'payroll.process', 'payroll.approve',

    // Reports
    'reports.sales', 'reports.expense', 'reports.profit', 'reports.inventory', 'reports.payroll',
    'reports.export',

    // System
    'users.manage', 'settings.manage', 'audit.view', 'notifications.manage',
];

// Role → Permission mapping
$rolePermissions = [
    'Super Admin' => array_keys($permissions), // ALL
    'Kasir' => [
        'dashboard.view',
        'orders.view', 'orders.create', 'orders.cancel',
        'pos.access', 'pos.process_payment', 'pos.split_bill',
        'menus.view', 'tables.view', 'tables.update_status',
        'reservations.view', 'reservations.create',
        'kitchen.view',
        'reports.sales',
    ],
    'Admin Gudang' => [
        'dashboard.view',
        'inventory.view', 'inventory.receive', 'inventory.adjust', 'inventory.waste',
        'inventory.transfer', 'inventory.opname',
        'purchases.view', 'purchases.create', 'purchases.update',
        'suppliers.view', 'suppliers.manage',
        'reports.inventory',
    ],
    'Manager' => [
        'dashboard.view',
        'reservations.view', 'reservations.manage',
        'employees.view', 'employees.manage',
        'attendance.view', 'attendance.manage',
        'payroll.view', 'payroll.process', 'payroll.approve',
        'expenses.view', 'expenses.create', 'expenses.approve',
        'reports.sales', 'reports.expense', 'reports.profit', 'reports.payroll',
        'reports.export',
        'orders.view', 'orders.refund',
    ],
];
```

---

## 6. OFFLINE POS ARCHITECTURE

```
ONLINE FLOW:
  POS → Fetch menu from server → Display
  POS → Add to cart → Submit order → Server processes → Success

OFFLINE FLOW:
  POS → Menu cached in localStorage (last fetched)
  POS → Add to cart → Save order to localStorage
  POS → Show "Order tersimpan offline"
  POS → When online: Show SYNC button
  POS → User taps SYNC → Orders sent to server one by one
  POS → Server validates → Creates orders → Returns success
  POS → Remove from localStorage
```

### localStorage Schema
```javascript
// Key: 'joglo_koi_offline_orders'
[
  {
    local_id: "offline_1693000000000",
    order_type: "dine_in",
    table_id: 3,
    customer_name: "Budi",
    items: [
      { menu_id: 1, menu_name: "Nasi Goreng", price: 25000, qty: 2, modifiers: [], subtotal: 50000 }
    ],
    subtotal: 50000,
    tax: 5000,
    total: 55000,
    payment_method: "cash",
    payment_amount: 60000,
    created_at: "2026-08-25T19:30:00",
    synced: false
  }
]

// Key: 'joglo_koi_menu_cache'
{
  last_fetch: "2026-08-25T08:00:00",
  categories: [...],
  menus: [...]
}
```

---

## 7. ORDER NUMBER GENERATION

```php
function generateOrderNumber($prefix = 'KOI') {
    $date = date('Ymd');
    $sql = "SELECT COUNT(*) + 1 as seq FROM orders WHERE DATE(created_at) = CURDATE()";
    $seq = DB::select($sql)[0]['seq'];
    return sprintf("%s-%s-%04d", $prefix, $date, $seq);
    // Example: KOI-20260825-0001
}

// Prefixes:
// KOI  → Orders
// RES  → Reservations
// PO   → Purchase Orders
// PAY  → Payroll
// EXP  → Expenses
// REC  → Goods Receipts
// WST  → Waste
// OPN  → Stock Opname
// RFD  → Refunds
// PMT  → Payments
```

---

## 8. DEVELOPMENT ROADMAP

### PHASE 1: Foundation (Week 1-2)
- [ ] Database schema + seed data
- [ ] Folder structure
- [ ] Core framework (Router, Controller, Model, DB)
- [ ] Authentication system
- [ ] Role & permission system
- [ ] Base UI layout (sidebar, topbar, responsive)
- [ ] Component system (modal, table, toast, etc.)
- [ ] Settings module

### PHASE 2: Core Operations (Week 3-4)
- [ ] Dashboard (super admin + cashier + warehouse)
- [ ] Menu management
- [ ] Menu category management
- [ ] Table management (visual layout)
- [ ] Reservation system

### PHASE 3: POS & Orders (Week 5-6)
- [ ] POS interface (desktop + mobile)
- [ ] Offline mode (localStorage + sync)
- [ ] Order management
- [ ] Kitchen Display System
- [ ] Payment processing
- [ ] Split bill
- [ ] Receipt printing

### PHASE 4: Inventory (Week 7-8)
- [ ] Ingredient management
- [ ] Recipe / BOM
- [ ] Stock management
- [ ] Stock ledger
- [ ] Stock opname
- [ ] Waste management
- [ ] Supplier management
- [ ] Purchase order
- [ ] Goods receiving

### PHASE 5: HR (Week 9)
- [ ] Employee management
- [ ] Attendance
- [ ] Payroll processing
- [ ] Payslip

### PHASE 6: Finance & Reports (Week 10)
- [ ] Expense management
- [ ] Cash management
- [ ] Sales reports
- [ ] Expense reports
- [ ] Profit/loss
- [ ] Inventory reports
- [ ] Payroll reports

### PHASE 7: Polish (Week 11-12)
- [ ] Export Excel (PhpSpreadsheet)
- [ ] Export PDF
- [ ] Audit log
- [ ] Notification system
- [ ] Security hardening
- [ ] Performance optimization
- [ ] Testing (manual + scripts)
- [ ] Documentation (README)
- [ ] Deployment guide

---

## 9. KEY BUSINESS RULES

1. **Server-side price calculation** — Frontend never determines final price
2. **Recipe-driven stock deduction** — Order completion triggers automatic stock reduction
3. **Atomic transactions** — Payment + stock deduction + order update in single DB transaction
4. **Immutable audit trail** — Stock movements and financial records cannot be edited, only reversed
5. **Soft delete everywhere** — Never hard delete business data
6. **Pre-payment model** — Customer pays at cashier before order goes to kitchen
7. **Granular permissions** — Every sensitive action checked against permission system
8. **Input validation server-side** — Never trust frontend validation alone

---

## 10. PRINTING (USB Thermal)

```
Format: ESC/POS compatible (Epson TM-T20, Mini POS 58mm/80mm)
Implementation: PHP output to browser → Print dialog
Fallback: HTML receipt → window.print() with CSS @media print

Receipt Layout (58mm):
┌─────────────────────────┐
│     JOGLO KOI           │
│     RESTAURANT          │
│  Lembu, Jimus, Klaten   │
│  Telp: 0878-1810-9887   │
├─────────────────────────┤
│ Order: KOI-20260825-001 │
│ Date:  25/08/2026 19:30 │
│ Cashier: Andi            │
│ Table: Meja Koi 03       │
├─────────────────────────┤
│ 2x Nasi Goreng   50.000 │
│ 1x Ikan Bakar    45.000 │
│ 2x Juice         30.000 │
├─────────────────────────┤
│ Subtotal:       125.000 │
│ Tax (10%):       12.500 │
│ Total:          137.500 │
│ Bayar:          150.000 │
│ Kembalian:       12.500 │
├─────────────────────────┤
│    Terima kasih!        │
│   Selamat menikmati     │
└─────────────────────────┘
```
