'required|email|max:255', 'selectedTableId' => 'nullable|string', 'selectedOccupiedTableId' => 'nullable|string', 'inputCustomerName' => 'nullable|string|max:255', ]; protected $listeners = [ 'paymentSuccess' => 'handlePaymentSuccess', 'tableStatusUpdated' => 'syncTablesFromFirebase', ]; public function mount($typeSlug = null) { $this->cart = session()->get('cart', []); $this->allTablesData = collect(); $this->syncTablesFromFirebase(); $this->tableIdFromUrl = request()->query('table_id'); $type = $typeSlug ? TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', [Str::lower($typeSlug)])->first() : TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', ['makanan'])->first(); $this->typeItemId = $type->id ?? null; } private function getFirebaseDatabase() { try { $path = config('services.firebase.credentials'); $path = file_exists($path) ? $path : storage_path('app/' . $path); throw_unless(file_exists($path), new \Exception("Firebase credentials not found at: $path")); return (new Factory) ->withServiceAccount($path) ->withDatabaseUri(config('services.firebase.database_url')) ->createDatabase(); } catch (\Exception $e) { logger()->error("Firebase init failed: " . $e->getMessage()); $this->dispatch('notify', message: 'Gagal koneksi Firebase: ' . $e->getMessage()); return null; } } public function syncTablesFromFirebase() { $db = $this->getFirebaseDatabase(); if (!$db) return $this->resetTables(); try { $data = $db->getReference('/')->getValue(); if (!$data) return $this->resetTables(); $currentHash = md5(json_encode($data)); if ($this->lastFirebaseDataHash['tables'] ?? null === $currentHash) { return; // Tidak berubah, jangan update properti } $this->lastFirebaseDataHash['tables'] = $currentHash; $available = $occupied = $all = []; foreach ($data as $id => $d) { $isActive = $d['sensors']['table_activation_sensor_active'] ?? 0; $reservedBy = $d['reserved_by'] ?? null; $table = [ 'id' => $id, 'device_id' => $d['device_id'] ?? $id, 'table_activation_sensor_active' => $isActive, 'reserved_by' => $reservedBy, 'is_available' => !$isActive, 'has_reserved_by' => !empty($reservedBy), 'firebase_data' => $d ]; $all[$id] = $table; $isActive ? ($reservedBy ? $occupied[] = $table : null) : $available[] = $table; } $this->availableTables = $available; $this->occupiedTables = $occupied; $this->allTablesData = collect($all); $this->attemptAutoSelectTable(); } catch (\Exception $e) { logger()->error("Sync Firebase failed: " . $e->getMessage()); $this->dispatch('notify', message: 'Gagal sinkron Firebase: ' . $e->getMessage()); $this->resetTables(); } } private function resetTables() { $this->availableTables = []; $this->occupiedTables = []; $this->allTablesData = collect(); } private function attemptAutoSelectTable() { if (!$this->tableIdFromUrl || $this->allTablesData->isEmpty()) return; $table = $this->allTablesData->firstWhere('device_id', $this->tableIdFromUrl); if (!$table) return $this->dispatch('notify', message: 'Meja tidak ditemukan.'); $isActive = $table['firebase_data']['sensors']['table_activation_sensor_active'] ?? 0; $reserved = $table['reserved_by'] ?? null; if (!$isActive) { $this->selectedTableId = $table['id']; $this->inputCustomerName = auth()->user()->name ?? 'Guest'; $this->dispatch('notify', message: 'Meja berhasil dipilih otomatis.'); } elseif ($reserved) { $this->selectedOccupiedTableId = $table['id']; $this->inputCustomerName = $reserved; $this->isExistingCustomer = true; $this->dispatch('notify', message: 'Meja sudah terisi oleh ' . $reserved); } else { $this->dispatch('notify', message: 'Meja tidak tersedia.'); } } public function showMap() { $this->showMapModal = true; } public function closeMap() { $this->showMapModal = false; } public function updatedSelectedTableId($value) { // Reset state jika pilihan meja tersedia berubah $this->selectedOccupiedTableId = null; $this->inputCustomerName = ''; $this->showCustomerNameInput = false; $this->isCustomerNameInputDisabled = false; $this->resetErrorBag(['inputCustomerName']); if ($value) { $this->inputCustomerName = auth()->user()->name ?? 'Guest'; $this->showCustomerNameInput = true; // $this->isCustomerNameInputDisabled tetap false } logger("Meja kosong dipilih: " . $value); } public function updatedSelectedOccupiedTableId($value) { // Reset state jika pilihan meja terreservasi berubah $this->selectedTableId = null; $this->inputCustomerName = ''; $this->showCustomerNameInput = false; $this->isCustomerNameInputDisabled = false; $this->resetErrorBag(['inputCustomerName']); if ($value) { $selectedTable = $this->allTablesData->get($value); // Menggunakan get() dari Collection if ($selectedTable && $selectedTable['has_reserved_by']) { $this->inputCustomerName = $selectedTable['reserved_by']; $this->showCustomerNameInput = true; $this->isCustomerNameInputDisabled = true; $this->dispatch('notify', message: 'Nama pemesan otomatis terisi: ' . $selectedTable['reserved_by'] . '.'); } else { $this->inputCustomerName = ''; $this->showCustomerNameInput = true; $this->isCustomerNameInputDisabled = false; $this->dispatch('notify', message: 'Meja ini terisi, namun nama pemesan belum terdaftar. Mohon masukkan nama Anda.'); } } logger("Meja terisi dipilih: " . $value); } public function updatedIsExistingCustomer($value) { $this->selectedTableId = null; $this->selectedOccupiedTableId = null; $this->inputCustomerName = ''; $this->showCustomerNameInput = false; $this->isCustomerNameInputDisabled = false; $this->resetErrorBag(['selectedTableId', 'selectedOccupiedTableId', 'inputCustomerName']); if (!$value) { // Jika user TIDAK mencentang "Saya sudah memesan meja" (pemesanan baru) $this->inputCustomerName = auth()->user()->name ?? 'Guest'; $this->showCustomerNameInput = true; // $this->isCustomerNameInputDisabled tetap false } } private function validateTableSelection() { $this->resetErrorBag(['selectedTableId', 'selectedOccupiedTableId', 'inputCustomerName']); $selectedTableId = $this->isExistingCustomer ? $this->selectedOccupiedTableId : $this->selectedTableId; $errorProperty = $this->isExistingCustomer ? 'selectedOccupiedTableId' : 'selectedTableId'; if (empty($selectedTableId)) { $this->addError($errorProperty, 'Pilih meja Anda.'); return false; } $selectedTable = $this->allTablesData->get($selectedTableId); if (!$selectedTable) { $this->addError($errorProperty, 'Meja yang dipilih tidak valid.'); return false; } $isSensorActive = ($selectedTable['firebase_data']['sensors']['table_activation_sensor_active'] ?? 0) === 1; if ($this->isExistingCustomer) { // Mengisi meja yang sudah terisi if (!$isSensorActive) { // Jika meja dipilih sebagai "terisi" tapi sensornya 0 (tersedia) $this->addError($errorProperty, 'Meja yang dipilih sebagai "sudah terisi" ternyata masih tersedia.'); return false; } if (!$selectedTable['has_reserved_by']) { // Jika meja terisi tapi tidak ada nama pemesan $this->addError($errorProperty, 'Meja ini tidak tersedia untuk penambahan pesanan tanpa nama pemesan yang terdaftar. Pilih meja kosong atau meja terisi lain.'); return false; } // Validasi nama pemesan untuk meja terisi $this->validateOnly('inputCustomerName', ['inputCustomerName' => 'required|string|max:255'], [ 'inputCustomerName.required' => 'Masukkan nama pemesan yang sesuai dengan meja ini.', ]); if ($this->getErrorBag()->has('inputCustomerName')) { return false; } if (Str::lower($this->inputCustomerName) !== Str::lower($selectedTable['reserved_by'])) { $this->addError('inputCustomerName', 'Nama pemesan tidak cocok dengan data meja ini: "' . $selectedTable['reserved_by'] . '".'); return false; } } else { // Mengisi meja yang tersedia if ($isSensorActive) { // Jika meja dipilih sebagai "tersedia" tapi sensornya 1 (tidak tersedia) $this->addError($errorProperty, 'Meja yang dipilih tidak tersedia.'); return false; } // Pastikan nama pemesan tidak kosong saat memilih meja kosong $this->validateOnly('inputCustomerName', ['inputCustomerName' => 'required|string|max:255'], [ 'inputCustomerName.required' => 'Nama pemesan wajib diisi untuk meja baru.', ]); if ($this->getErrorBag()->has('inputCustomerName')) { return false; } } return true; } public function checkout() { if (!$this->validateTableSelection()) { return; } $this->validate(['customerEmail' => 'required|email']); logger("Checkout method called."); $cartItems = $this->getCartItemsProperty(); if (empty($cartItems)) { logger("Cart is empty"); $this->dispatch('notify', message: 'Keranjang kosong'); return; } try { Config::$serverKey = config('services.midtrans.server_key'); Config::$isProduction = config('services.midtrans.is_production', false); Config::$isSanitized = true; Config::$is3ds = true; $selectedTableData = $this->getSelectedTableForTransaction(); $params = [ 'transaction_details' => [ 'order_id' => 'ORDER-' . time() . '-' . uniqid(), 'gross_amount' => $this->getCartTotalProperty(), ], 'customer_details' => [ 'first_name' => $this->inputCustomerName ?: (auth()->user()->name ?? 'Guest'), 'email' => auth()->user()->email ?? $this->customerEmail ?? 'guest@example.com', ], 'item_details' => array_map(function($item) { return [ 'id' => $item['id'], 'price' => $item['price'], 'quantity' => $item['qty'], 'name' => $item['name'], ]; }, $cartItems), 'custom_field1' => 'Firebase Table ID: ' . ($selectedTableData['id'] ?? 'N/A'), 'custom_field2' => 'Nama Meja: ' . ($selectedTableData['device_id'] ?? 'N/A'), 'custom_field3' => 'Nama Pemesan: ' . $this->inputCustomerName, ]; $snapToken = Snap::getSnapToken($params); $this->dispatch('midtransSnapToken', token: $snapToken); } catch (\Exception $e) { logger("Midtrans error: " . $e->getMessage()); logger("Error trace: " . $e->getTraceAsString()); $this->dispatch('notify', message: 'Gagal memproses pembayaran: ' . $e->getMessage()); } } public function sendReceiptEmail() { if (!$this->validateTableSelection()) { return; } $this->validate(['customerEmail' => 'required|email|max:255']); $cartItems = $this->getCartItemsProperty(); $cartTotal = $this->getCartTotalProperty(); $mejaDetail = $this->getSelectedTableForTransaction(); try { Mail::to($this->customerEmail)->send(new OrderReceipt($cartItems, $cartTotal, $mejaDetail)); $this->dispatch('notify', message: 'Detail pesanan telah dikirim ke email Anda.'); $this->updateTableAndResetState(); // Menggunakan fungsi baru } catch (\Exception $e) { logger("Failed to send receipt email: " . $e->getMessage()); $this->dispatch('notify', message: 'Gagal mengirim email. Silakan coba lagi.'); } } public function skipPayment() { if (!$this->validateTableSelection()) { return; } $this->dispatch('notify', message: 'Pesanan Anda telah diproses tanpa pembayaran.'); $this->updateTableAndResetState(); // Menggunakan fungsi baru } public function handlePaymentSuccess() { $this->dispatch('notify', message: 'Pembayaran berhasil!'); $this->updateTableAndResetState(); // Menggunakan fungsi baru } // Fungsi Pembantu Baru private function getSelectedTableForTransaction(): ?array { $tableId = $this->selectedTableId ?? $this->selectedOccupiedTableId; return $this->allTablesData->get($tableId); } // Fungsi Pembantu Baru untuk mengupdate meja dan mereset state private function updateTableAndResetState() { $tableData = $this->getSelectedTableForTransaction(); if ($tableData) { $updates = [ 'sensors/table_activation_sensor_active' => 1, 'reserved_by' => $this->inputCustomerName ]; $this->updateFirebaseTableStatus($tableData['id'], $updates); } $this->clearCartAndResetState(); } public function updateFirebaseTableStatus(string $firebaseTableId, array $updates, bool $refresh = true) { $database = $this->getFirebaseDatabase(); if (!$database) return; try { $database->getReference($firebaseTableId)->update($updates); logger("Firebase: Updated {$firebaseTableId} with " . json_encode($updates)); $this->dispatch('notify', message: "Status meja '{$firebaseTableId}' berhasil diperbarui di Firebase."); if ($refresh) { $this->syncTablesFromFirebase(); } } catch (\Exception $e) { logger()->error("Failed to update Firebase for table ID {$firebaseTableId}: " . $e->getMessage()); $this->dispatch('notify', message: 'Gagal memperbarui status meja di Firebase: ' . $e->getMessage()); } } private function clearCartAndResetState() { $this->cart = []; session()->forget('cart'); $this->showCart = false; $this->showEmailInput = false; $this->customerEmail = ''; $this->selectedTableId = null; $this->selectedOccupiedTableId = null; $this->inputCustomerName = ''; $this->resetValidation(); } public function render() { return view('livewire.food-order', [ 'foodItems' => $this->getFilteredFoodItemsProperty(), 'cartItems' => $this->getCartItemsProperty(), 'cartTotal' => $this->getCartTotalProperty(), 'pageTitle' => $this->getPageTitleProperty(), 'typeItemId' => $this->typeItemId, 'allTypeItems' => $this->getAllTypeItemsProperty(), 'availableTables' => $this->availableTables, 'occupiedTables' => $this->occupiedTables, 'allTablesData' => $this->allTablesData->toArray(), // Kirim sebagai array ke view 'tableIdFromUrl' => $this->tableIdFromUrl, ])->layout('components.layouts.front'); } // --- Helper Properties --- public function getFoodItemsProperty() { return Items::all(); } public function getAllTypeItemsProperty() // Menggabungkan getTypeItemsProperty { return TypeItems::all(); } public function getFilteredFoodItemsProperty() { if ($this->typeItemId) { return Items::where('type_item_id', $this->typeItemId)->get(); } return Items::all(); } public function getCartItemsProperty() { $items = []; foreach ($this->cart as $itemId => $qty) { $item = Items::find($itemId); if ($item) { $items[] = [ 'id' => $item->id, 'name' => $item->name, 'price' => $item->price, 'qty' => $qty, 'total_price' => $item->price * $qty, ]; } } return $items; } public function getCartTotalProperty() { return collect($this->getCartItemsProperty())->sum('total_price'); } public function getPageTitleProperty() { if ($this->typeItemId) { $type = TypeItems::find($this->typeItemId); return $type ? $type->name : 'Semua Menu'; } return 'Semua Menu'; } public function addToCart($itemId) { $item = Items::find($itemId); if (!$item) { $this->dispatch('notify', message: 'Item tidak ditemukan.'); return; } $this->cart[$itemId] = ($this->cart[$itemId] ?? 0) + 1; session()->put('cart', $this->cart); $this->dispatch('notify', message: $item->name . ' ditambahkan ke keranjang.'); } public function removeFromCart($itemId) { if (isset($this->cart[$itemId])) { $item = Items::find($itemId); // Get item before potential unset if ($this->cart[$itemId] > 1) { $this->cart[$itemId]--; } else { unset($this->cart[$itemId]); } session()->put('cart', $this->cart); $this->dispatch('notify', message: $item->name . ' dihapus dari keranjang.'); if (empty($this->cart)) { $this->showCart = false; } } } public function openCart() { $this->showCart = true; } public function closeCart() { $this->showCart = false; } public function filterByType($typeId) { $this->typeItemId = $typeId; $type = TypeItems::find($typeId); if ($type) { $typeSlug = Str::slug($type->name); $newUrl = route('menu.byType', ['typeSlug' => $typeSlug], false); $this->dispatch('updateUrl', url: $newUrl); } } }