551 lines
20 KiB
PHP
551 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use Livewire\Component;
|
|
use App\Models\Items;
|
|
use App\Models\TypeItems;
|
|
use Illuminate\Support\Str;
|
|
use Midtrans\Config;
|
|
use Midtrans\Snap;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use App\Mail\OrderReceipt;
|
|
use Kreait\Firebase\Factory;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class FoodOrder extends Component
|
|
{
|
|
public array $cart = [];
|
|
public bool $showCart = false;
|
|
public ?int $typeItemId = null;
|
|
public bool $showEmailInput = false;
|
|
public string $customerEmail = '';
|
|
public bool $isExistingCustomer = false;
|
|
public ?string $selectedTableId = null;
|
|
public ?string $selectedOccupiedTableId = null;
|
|
public string $inputCustomerName = '';
|
|
public bool $showCustomerNameInput = false;
|
|
public bool $isCustomerNameInputDisabled = false;
|
|
public array $availableTables = [];
|
|
public array $occupiedTables = [];
|
|
public Collection $allTablesData;
|
|
public bool $showMapModal = false;
|
|
public ?string $tableIdFromUrl = null;
|
|
|
|
private array $lastFirebaseDataHash = [];
|
|
|
|
protected array $rules = [
|
|
'customerEmail' => '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);
|
|
}
|
|
}
|
|
}
|