959 lines
38 KiB
PHP
959 lines
38 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use Livewire\Component;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
use App\Models\Items;
|
|
use App\Models\TypeItems;
|
|
use App\Models\Order;
|
|
use App\Models\OrderItem;
|
|
use Midtrans\Config;
|
|
use Midtrans\Snap;
|
|
use App\Mail\OrderReceiptMail;
|
|
use Carbon\Carbon;
|
|
use Kreait\Firebase\Factory;
|
|
|
|
class FoodOrder extends Component
|
|
{
|
|
public array $cart = [];
|
|
public bool $showCart = false;
|
|
public ?int $typeItemId = null;
|
|
public bool $showEmailInput = true;
|
|
public string $customerEmail = '';
|
|
public $isExistingCustomer = '0';
|
|
|
|
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;
|
|
public $showQrisPayment = false;
|
|
public $qrisImageUrl = 'img/qr-code.png';
|
|
public bool $paymentInProgress = false;
|
|
|
|
public bool $showCheckoutDetailsModal = false;
|
|
public ?string $selectedPaymentMethod = null;
|
|
|
|
private array $lastFirebaseDataHash = [];
|
|
|
|
protected function rules()
|
|
{
|
|
return [
|
|
'inputCustomerName' => 'required|string|min:3',
|
|
'customerEmail' => 'nullable|email|max:255',
|
|
'selectedTableId' => 'required_without:selectedOccupiedTableId',
|
|
'selectedOccupiedTableId' => 'required_without:selectedTableId',
|
|
'selectedPaymentMethod' => 'required|in:midtrans,cash,qris',
|
|
];
|
|
}
|
|
|
|
protected function messages()
|
|
{
|
|
return [
|
|
'inputCustomerName.required' => 'Nama pemesan wajib diisi.',
|
|
'inputCustomerName.min' => 'Nama pemesan minimal 3 karakter.',
|
|
'selectedTableId.required_without' => 'Mohon pilih meja untuk pesanan.',
|
|
'selectedOccupiedTableId.required_without' => 'Mohon pilih meja untuk pesanan.',
|
|
'customerEmail.email' => 'Format email tidak valid.',
|
|
'selectedPaymentMethod.required' => 'Mohon pilih metode pembayaran.',
|
|
'selectedPaymentMethod.in' => 'Metode pembayaran tidak valid.',
|
|
];
|
|
}
|
|
protected $listeners = [
|
|
'paymentSuccess' => 'handlePaymentSuccess', // misalnya untuk pembayaran tunai/manual
|
|
'midtransPaymentSuccess' => 'handleMidtransSuccess', // khusus untuk callback Snap Midtrans
|
|
'tableStatusUpdated' => 'syncTablesFromFirebase', // untuk update status meja via Firebase
|
|
'resetCartAfterOrder' => 'clearCartAndResetState'
|
|
];
|
|
|
|
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;
|
|
|
|
if (empty($this->customerEmail)) {
|
|
$this->customerEmail = 'guest@gmail.com';
|
|
}
|
|
}
|
|
|
|
public function getIsExistingCustomerBoolProperty(): bool
|
|
{
|
|
return $this->isExistingCustomer === '1';
|
|
}
|
|
|
|
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) {
|
|
Log::error("Firebase init failed: " . $e->getMessage());
|
|
$this->dispatch('notify', message: 'Gagal koneksi Firebase: ' . $e->getMessage(), type: 'error');
|
|
$this->dispatch('logToConsole', message: 'ERROR: Firebase init failed: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function syncTablesFromFirebase()
|
|
{
|
|
$db = $this->getFirebaseDatabase();
|
|
if (!$db) {
|
|
return $this->resetTables();
|
|
}
|
|
|
|
try {
|
|
$data = $db->getReference('/')->getValue();
|
|
|
|
if (empty($data)) {
|
|
$this->dispatch('logToConsole', message: 'Firebase data is empty, resetting tables.');
|
|
return $this->resetTables();
|
|
}
|
|
|
|
$currentHash = md5(json_encode($data));
|
|
if (($this->lastFirebaseDataHash['tables'] ?? null) === $currentHash) {
|
|
return;
|
|
}
|
|
|
|
// Only process if data changed significantly
|
|
$this->processFirebaseData($data);
|
|
$this->lastFirebaseDataHash['tables'] = $currentHash;
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Sync Firebase failed: " . $e->getMessage());
|
|
$this->dispatch('notify', message: 'Gagal sinkron Firebase: ' . $e->getMessage(), type: 'error');
|
|
$this->resetTables();
|
|
}
|
|
}
|
|
|
|
private function processFirebaseData($data)
|
|
{
|
|
$available = $occupied = $all = [];
|
|
|
|
foreach ($data as $id => $d) {
|
|
$isActive = (bool)($d['sensors']['table_activation_sensor_active'] ?? false);
|
|
$reservedBy = $d['reserved_by'] ?? null;
|
|
$reservedBy = ($reservedBy === 'N/A' || empty($reservedBy)) ? null : $reservedBy;
|
|
|
|
$table = [
|
|
'id' => $id,
|
|
'device_id' => $d['device_id'] ?? $id,
|
|
'table_activation_sensor_active' => $isActive,
|
|
'reserved_by' => $reservedBy,
|
|
'is_available' => !$isActive && empty($reservedBy),
|
|
'is_occupied_by_someone' => $isActive && !empty($reservedBy),
|
|
'is_occupied_but_empty_reserved' => $isActive && empty($reservedBy),
|
|
'firebase_data' => $d
|
|
];
|
|
|
|
$all[$id] = $table;
|
|
|
|
if ($table['is_available']) {
|
|
$available[] = $table;
|
|
} elseif ($table['is_occupied_by_someone']) {
|
|
$occupied[] = $table;
|
|
}
|
|
}
|
|
|
|
$this->availableTables = $available;
|
|
$this->occupiedTables = $occupied;
|
|
$this->allTablesData = collect($all);
|
|
$this->attemptAutoSelectTable();
|
|
}
|
|
|
|
private function resetTables()
|
|
{
|
|
$this->availableTables = [];
|
|
$this->occupiedTables = [];
|
|
$this->allTablesData = collect();
|
|
$this->selectedTableId = null;
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->inputCustomerName = '';
|
|
$this->isExistingCustomer = false;
|
|
$this->showCustomerNameInput = false;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->tableIdFromUrl = null;
|
|
$this->dispatch('logToConsole', message: 'Tables reset.');
|
|
}
|
|
|
|
private function attemptAutoSelectTable()
|
|
{
|
|
if (!$this->tableIdFromUrl || $this->allTablesData->isEmpty()) {
|
|
$this->dispatch('logToConsole', message: 'No table_id from URL or tables data empty. Skipping auto-select.');
|
|
return;
|
|
}
|
|
|
|
$table = $this->allTablesData->firstWhere('device_id', $this->tableIdFromUrl);
|
|
if (!$table) {
|
|
$this->dispatch('notify', message: 'Meja dengan ID "' . $this->tableIdFromUrl . '" tidak ditemukan.', type: 'warning');
|
|
$this->tableIdFromUrl = null;
|
|
$this->dispatch('logToConsole', message: 'Auto-select aborted: Table ' . $this->tableIdFromUrl . ' not found.');
|
|
return;
|
|
}
|
|
|
|
$this->dispatch('logToConsole', message: 'Attempting to auto-select table: ' . $this->tableIdFromUrl . ' with data: ' . json_encode($table));
|
|
|
|
if ($table['is_available']) {
|
|
$this->selectedTableId = $table['id'];
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->isExistingCustomer = false;
|
|
$this->inputCustomerName = '';
|
|
$this->showCustomerNameInput = true;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->dispatch('notify', message: 'Meja ' . $table['device_id'] . ' berhasil dipilih otomatis sebagai meja baru.', type: 'success');
|
|
$this->dispatch('logToConsole', message: 'Auto-selected available table: ' . $table['device_id']);
|
|
} elseif ($table['is_occupied_by_someone']) {
|
|
$this->selectedOccupiedTableId = $table['id'];
|
|
$this->selectedTableId = null;
|
|
$this->isExistingCustomer = true;
|
|
$this->inputCustomerName = $table['reserved_by'];
|
|
$this->isCustomerNameInputDisabled = true;
|
|
$this->dispatch('notify', message: 'Meja ' . $table['device_id'] . ' sudah terisi oleh ' . $table['reserved_by'] . '.', type: 'info');
|
|
$this->dispatch('logToConsole', message: 'Auto-selected occupied table with reservation: ' . $table['device_id'] . ' by ' . $table['reserved_by']);
|
|
} elseif ($table['is_occupied_but_empty_reserved']) {
|
|
$this->selectedOccupiedTableId = $table['id'];
|
|
$this->selectedTableId = null;
|
|
$this->isExistingCustomer = true;
|
|
$this->inputCustomerName = '';
|
|
$this->showCustomerNameInput = true;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->dispatch('notify', message: 'Meja ' . $table['device_id'] . ' terisi tetapi nama pemesan belum terdaftar. Mohon masukkan nama Anda.', type: 'warning');
|
|
$this->dispatch('logToConsole', message: 'Auto-selected occupied table without reservation: ' . $table['device_id'] . '. Customer needs to input name.');
|
|
} else {
|
|
$this->dispatch('notify', message: 'Meja ' . $table['device_id'] . ' tidak tersedia atau tidak dikenal.', type: 'warning');
|
|
$this->dispatch('logToConsole', message: 'Auto-select failed for table: ' . $table['device_id'] . '. Not available or unknown state.');
|
|
}
|
|
$this->tableIdFromUrl = null;
|
|
}
|
|
|
|
public function showMap()
|
|
{
|
|
$this->showMapModal = true;
|
|
$this->dispatch('logToConsole', message: 'Showing map modal.');
|
|
}
|
|
|
|
public function closeMap()
|
|
{
|
|
$this->showMapModal = false;
|
|
$this->dispatch('logToConsole', message: 'Closing map modal.');
|
|
}
|
|
|
|
public function updatedSelectedTableId($value)
|
|
{
|
|
$this->resetErrorBag(['selectedTableId', 'selectedOccupiedTableId', 'inputCustomerName']);
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->isExistingCustomer = false;
|
|
|
|
if ($value) {
|
|
$this->showCustomerNameInput = true;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->inputCustomerName = '';
|
|
$this->dispatch('logToConsole', message: "Meja kosong dipilih: " . $value . ". Nama default: " . ($this->inputCustomerName ?: 'Kosong'));
|
|
} else {
|
|
$this->showCustomerNameInput = false;
|
|
$this->inputCustomerName = '';
|
|
$this->dispatch('logToConsole', message: "Meja kosong tidak dipilih.");
|
|
}
|
|
$this->showEmailInput = true;
|
|
}
|
|
|
|
public function updatedSelectedOccupiedTableId($value)
|
|
{
|
|
$this->resetErrorBag(['selectedTableId', 'selectedOccupiedTableId', 'inputCustomerName']);
|
|
$this->selectedTableId = null;
|
|
$this->isExistingCustomer = true;
|
|
|
|
if ($value) {
|
|
$selectedTable = $this->allTablesData->get($value);
|
|
if ($selectedTable) {
|
|
$this->showCustomerNameInput = true;
|
|
if ($selectedTable['is_occupied_by_someone']) {
|
|
$this->inputCustomerName = $selectedTable['reserved_by'];
|
|
$this->isCustomerNameInputDisabled = true;
|
|
$this->dispatch('notify', message: 'Nama pemesan otomatis terisi: ' . $selectedTable['reserved_by'] . '.', type: 'info');
|
|
$this->dispatch('logToConsole', message: "Meja terisi dipilih dengan reservasi: " . $value . ". Nama: " . $this->inputCustomerName);
|
|
} elseif ($selectedTable['is_occupied_but_empty_reserved']) {
|
|
$this->inputCustomerName = '';
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->dispatch('notify', message: 'Meja ini terisi, namun nama pemesan belum terdaftar. Mohon masukkan nama Anda.', type: 'warning');
|
|
$this->dispatch('logToConsole', message: "Meja terisi dipilih tanpa reservasi: " . $value . ". Customer needs to input name.");
|
|
} else {
|
|
$this->inputCustomerName = '';
|
|
$this->showCustomerNameInput = false;
|
|
$this->isExistingCustomer = false;
|
|
$this->dispatch('notify', message: 'Kesalahan: Meja yang dipilih tidak valid sebagai meja terisi. Silakan pilih ulang.', type: 'error');
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->dispatch('logToConsole', message: "Invalid occupied table selected: " . $value . ". Resetting selection.");
|
|
}
|
|
} else {
|
|
$this->dispatch('notify', message: 'Meja yang dipilih tidak ditemukan.', type: 'error');
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->inputCustomerName = '';
|
|
$this->showCustomerNameInput = false;
|
|
$this->isExistingCustomer = false;
|
|
$this->dispatch('logToConsole', message: "Selected occupied table not found: " . $value . ". Resetting selection.");
|
|
}
|
|
} else {
|
|
$this->showCustomerNameInput = false;
|
|
$this->inputCustomerName = '';
|
|
$this->isExistingCustomer = false;
|
|
$this->dispatch('logToConsole', message: "Meja terisi tidak dipilih.");
|
|
}
|
|
$this->showEmailInput = true;
|
|
}
|
|
|
|
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) {
|
|
$this->showCustomerNameInput = true;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->inputCustomerName = '';
|
|
$this->dispatch('logToConsole', message: "isExistingCustomer changed to NEW. Input name displayed.");
|
|
} else {
|
|
$this->showCustomerNameInput = false;
|
|
$this->inputCustomerName = '';
|
|
$this->dispatch('logToConsole', message: "isExistingCustomer changed to EXISTING. Input name hidden until table selected.");
|
|
}
|
|
$this->showEmailInput = true;
|
|
}
|
|
|
|
public function goToCheckoutDetails()
|
|
{
|
|
if (empty($this->cart)) {
|
|
$this->dispatch('notify', message: 'Keranjang belanja Anda kosong. Silakan tambahkan item.');
|
|
return;
|
|
}
|
|
$this->showCheckoutDetailsModal = true;
|
|
$this->dispatch('logToConsole', message: 'Proceeding to checkout details.');
|
|
}
|
|
|
|
public function closeCheckoutDetailsModal()
|
|
{
|
|
$this->showCheckoutDetailsModal = false;
|
|
$this->dispatch('logToConsole', message: 'Checkout details modal closed.');
|
|
}
|
|
|
|
public function processOrderConfirmation()
|
|
{
|
|
$this->dispatch('logToConsole', message: 'Memulai proses konfirmasi order...');
|
|
$cartItems = $this->getCartItemsProperty();
|
|
|
|
if (empty($cartItems) && !$this->paymentInProgress) return;
|
|
|
|
try {
|
|
$this->validate();
|
|
} catch (ValidationException $e) {
|
|
foreach ($e->errors() as $field => $messages) {
|
|
foreach ($messages as $message) {
|
|
$this->dispatch('notify', message: $message, type: 'error');
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
$selectedTableData = $this->getSelectedTableForTransaction();
|
|
if (!$selectedTableData) {
|
|
$this->dispatch('notify', message: 'Detail meja tidak ditemukan.', type: 'error');
|
|
return;
|
|
}
|
|
|
|
$isSensorActive = (bool)($selectedTableData['firebase_data']['sensors']['table_activation_sensor_active'] ?? false);
|
|
$reservedBy = $selectedTableData['reserved_by'];
|
|
|
|
if ($this->isExistingCustomer) {
|
|
if (!$isSensorActive) {
|
|
$this->dispatch('notify', message: 'Meja ini belum terisi.', type: 'warning');
|
|
return;
|
|
}
|
|
if (!empty($reservedBy) && Str::lower($this->inputCustomerName) !== Str::lower($reservedBy)) {
|
|
$this->dispatch('notify', message: 'Nama tidak cocok dengan reservasi: ' . $reservedBy, type: 'warning');
|
|
return;
|
|
}
|
|
} else {
|
|
if ($isSensorActive) {
|
|
$this->dispatch('notify', message: 'Meja sudah terisi, pilih meja lain.', type: 'warning');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (empty($this->customerEmail)) {
|
|
$this->customerEmail = 'guest@gmail.com';
|
|
}
|
|
|
|
$totalAmount = $this->getCartTotalProperty();
|
|
|
|
if ($this->selectedPaymentMethod === 'midtrans' && $totalAmount < 10000) {
|
|
$this->dispatch('notify', message: 'Minimum Midtrans Rp 10.000', type: 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$waktuWIB = now()->timezone('Asia/Jakarta');
|
|
if ($this->selectedPaymentMethod === 'midtrans'){
|
|
$midtransTransactionId = 'MIDTRANS-' . now()->timestamp . '-' . rand(1000, 9999);
|
|
}
|
|
|
|
// Buat order terlebih dahulu (baik untuk midtrans maupun metode lain)
|
|
$order = Order::create([
|
|
'customer_name' => $this->inputCustomerName,
|
|
'customer_email' => $this->customerEmail,
|
|
'table_id' => $selectedTableData['id'],
|
|
'total_amount' => $totalAmount,
|
|
'transaction_status' => 'pending',
|
|
'payment_method' => $this->selectedPaymentMethod,
|
|
'midtrans_transaction_id' => $midtransTransactionId ?? null,
|
|
'created_at' => $waktuWIB,
|
|
'updated_at' => $waktuWIB,
|
|
]);
|
|
|
|
foreach ($cartItems as $item) {
|
|
OrderItem::create([
|
|
'order_id' => $order->id,
|
|
'item_id' => $item['id'],
|
|
'item_name' => $item['name'],
|
|
'item_price' => $item['price'],
|
|
'quantity' => $item['qty'],
|
|
'total_price' => $item['total_price'],
|
|
'created_at' => $waktuWIB,
|
|
'updated_at' => $waktuWIB,
|
|
]);
|
|
}
|
|
|
|
if ($this->selectedPaymentMethod === 'midtrans') {
|
|
$this->paymentInProgress = true;
|
|
|
|
$this->clearCartAndResetState();
|
|
return redirect()->route('midtrans.payment', [
|
|
'midtrans_transaction_id' => $midtransTransactionId,
|
|
]);
|
|
|
|
} elseif ($this->selectedPaymentMethod === 'qris') {
|
|
$this->processQrisPayment($order->id);
|
|
} else {
|
|
$this->processManualPaymentInternal($order->id);
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("Gagal membuat order: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
|
$this->dispatch('notify', message: 'Gagal memproses pesanan: ' . $e->getMessage(), type: 'error');
|
|
}
|
|
}
|
|
|
|
public function updateFirebaseTableStatus(string $firebaseTableId, array $updates, bool $refresh = true)
|
|
{
|
|
$database = $this->getFirebaseDatabase();
|
|
if (!$database) return;
|
|
|
|
try {
|
|
$database->getReference($firebaseTableId)->update($updates);
|
|
$this->dispatch('logToConsole', message: "Firebase: Updated {$firebaseTableId} with " . json_encode($updates));
|
|
$this->dispatch('notify', message: "Status meja '{$firebaseTableId}' berhasil diperbarui di Firebase.", type: 'success');
|
|
|
|
if ($refresh) {
|
|
$this->syncTablesFromFirebase();
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to update Firebase for table ID {$firebaseTableId}: " . $e->getMessage());
|
|
$this->dispatch('logToConsole', message: 'ERROR: Failed to update Firebase for table ID ' . $firebaseTableId . ': ' . $e->getMessage());
|
|
$this->dispatch('notify', message: 'Gagal memperbarui status meja di Firebase: ' . $e->getMessage(), type: 'error');
|
|
}
|
|
}
|
|
|
|
protected function initiateMidtransPayment($midtransTransactionId, $totalAmount, array $selectedTableData, array $cartItems): Redirector|null
|
|
{
|
|
$this->dispatch('logToConsole', message: 'Memulai pembayaran Midtrans...');
|
|
|
|
try {
|
|
if (empty($midtransTransactionId)) {
|
|
throw new \Exception('ID transaksi Midtrans tidak boleh kosong.');
|
|
}
|
|
|
|
if ($totalAmount < 10000) {
|
|
throw new \Exception('Minimum pembayaran Midtrans adalah Rp 10.000');
|
|
}
|
|
|
|
if (empty($cartItems)) {
|
|
throw new \Exception('Keranjang belanja kosong');
|
|
}
|
|
|
|
// Data customer
|
|
$customerDetails = [
|
|
'first_name' => substr($this->inputCustomerName ?: 'Customer', 0, 50),
|
|
'email' => $this->customerEmail ?: 'guest@gmail.com',
|
|
'phone' => '08123456789',
|
|
];
|
|
|
|
// Item
|
|
$itemDetails = array_map(fn($item) => [
|
|
'id' => $item['id'],
|
|
'price' => $item['price'],
|
|
'quantity' => $item['qty'],
|
|
'name' => substr($item['name'], 0, 50),
|
|
], $cartItems);
|
|
|
|
$params = [
|
|
'transaction_details' => [
|
|
'order_id' => (string) $midtransTransactionId,
|
|
'gross_amount' => $totalAmount,
|
|
],
|
|
'customer_details' => $customerDetails,
|
|
'item_details' => $itemDetails,
|
|
'enabled_payments' => [
|
|
'credit_card',
|
|
'bca_va',
|
|
'bni_va',
|
|
'bri_va'
|
|
],
|
|
'callbacks' => [
|
|
'finish' => url('/midtrans/finish'),
|
|
'error' => url('/midtrans/error'),
|
|
'pending' => url('/midtrans/pending'),
|
|
]
|
|
];
|
|
session([
|
|
'order_data' => [
|
|
'customer_name' => $this->inputCustomerName,
|
|
'customer_email' => $this->customerEmail,
|
|
'table_id' => $selectedTableData['id'],
|
|
'midtrans_transaction_id' => $midtransTransactionId,
|
|
'total_amount' => $totalAmount,
|
|
'payment_method' => 'midtrans',
|
|
'cart_items' => $cartItems,
|
|
]
|
|
]);
|
|
|
|
// Konfigurasi Midtrans
|
|
Config::$serverKey = config('services.midtrans.server_key');
|
|
Config::$isProduction = config('services.midtrans.is_production', false);
|
|
Config::$isSanitized = true;
|
|
Config::$is3ds = true;
|
|
|
|
// Snap Token
|
|
$snapToken = Snap::getSnapToken($params);
|
|
|
|
if (empty($snapToken)) {
|
|
throw new \Exception('Gagal mendapatkan token pembayaran dari Midtrans');
|
|
}
|
|
|
|
// Redirect ke halaman Snap Midtrans
|
|
return $this->redirectRoute('midtrans.payment.page', ['token' => $snapToken]);
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Error Midtrans: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
|
$this->dispatch('notify', message: 'Gagal memproses pembayaran: ' . $e->getMessage(), type: 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
protected function updateTableStatusToReserved(array $orderData): void
|
|
{
|
|
$firebase = $this->getFirebaseDatabase();
|
|
if (!$firebase) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$firebase->getReference($orderData['table_device_id'])->update([
|
|
'reserved_by' => $orderData['customer_name'],
|
|
'sensors/table_activation_sensor_active' => 1,
|
|
'table_occupied' => 1,
|
|
]);
|
|
\Log::info("Firebase: Meja {$orderData['table_device_id']} diupdate sebagai reserved oleh {$orderData['customer_name']}.");
|
|
} catch (\Exception $e) {
|
|
\Log::error("Gagal update status meja di Firebase: " . $e->getMessage());
|
|
$this->dispatch('notify', message: 'Gagal update status meja di Firebase.', type: 'error');
|
|
}
|
|
}
|
|
|
|
public function handleMidtransSuccess($result)
|
|
{
|
|
$selectedTableData = $this->getSelectedTableForTransaction();
|
|
$totalAmount = $this->getCartTotalProperty();
|
|
$cartItems = $this->getCartItemsProperty();
|
|
|
|
// Simpan ke database
|
|
$order = Order::create([
|
|
'customer_name' => $this->inputCustomerName,
|
|
'customer_email' => $this->customerEmail,
|
|
'table_id' => $selectedTableData['id'],
|
|
'total_amount' => $totalAmount,
|
|
'payment_method' => 'midtrans',
|
|
'midtrans_transaction_id' => $result['transaction_id'],
|
|
'status' => 'confirmed',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
foreach ($cartItems as $item) {
|
|
OrderItem::create([
|
|
'order_id' => $order->id,
|
|
'item_id' => $item['id'],
|
|
'item_name' => $item['name'],
|
|
'item_price' => $item['price'],
|
|
'quantity' => $item['qty'],
|
|
'total_price' => $item['total_price'],
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
// ✅ Tambahkan update ke Firebase di sini
|
|
try {
|
|
$firebase = (new \Kreait\Firebase\Factory)
|
|
->withServiceAccount(storage_path('app/firebase/.firebase_credentials.json'))
|
|
->createDatabase();
|
|
|
|
$firebase->getReference($selectedTableData['device_id'])->update([
|
|
'sensors/table_activation_sensor_active' => 1,
|
|
'reserved_by' => $this->inputCustomerName,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
// Logging atau tindakan jika gagal update ke Firebase
|
|
\Log::error('Firebase update failed: ' . $e->getMessage());
|
|
}
|
|
|
|
// Bersihkan keranjang
|
|
$this->resetCart();
|
|
|
|
// Notifikasi
|
|
$this->dispatch('notify', [
|
|
'type' => 'success',
|
|
'message' => 'Pembayaran berhasil!'
|
|
]);
|
|
}
|
|
|
|
protected function processManualPaymentInternal($orderDbId)
|
|
{
|
|
$paymentTypes = [
|
|
'cash' => 'Pembayaran tunai berhasil. Silakan tunggu konfirmasi dari kasir.',
|
|
'transfer' => 'Transfer berhasil. Mohon tunjukkan bukti ke kasir.',
|
|
'qris' => 'QRIS berhasil. Mohon tunjukkan bukti ke kasir.',
|
|
];
|
|
|
|
$message = $paymentTypes[$this->selectedPaymentMethod]
|
|
?? 'Pembayaran berhasil. Mohon tunggu konfirmasi dari kasir.';
|
|
|
|
try {
|
|
// Aktifkan flag agar tidak muncul "Keranjang kosong"
|
|
$this->paymentInProgress = true;
|
|
|
|
// Tutup modal checkout
|
|
$this->closeCheckoutDetailsModal();
|
|
|
|
// Tampilkan notifikasi sukses
|
|
$this->dispatch('notify',
|
|
message: $message,
|
|
type: 'success'
|
|
);
|
|
|
|
// Reset tombol "Processing"
|
|
$this->dispatch('resetProcessingButton');
|
|
|
|
// Kosongkan keranjang setelah notifikasi
|
|
$this->clearCartAndResetState();
|
|
|
|
$order = \App\Models\Order::find($orderDbId);
|
|
if ($order && $order->customer_email) {
|
|
Mail::to($order->customer_email)->send(new OrderReceiptMail($order));
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
// Tangani error jika terjadi
|
|
$this->dispatch('notify', [
|
|
'message' => 'Terjadi kesalahan saat memproses pembayaran.',
|
|
'type' => 'error',
|
|
]);
|
|
$this->dispatch('resetProcessingButton');
|
|
} finally {
|
|
// Matikan flag walaupun error
|
|
$this->paymentInProgress = false;
|
|
}
|
|
}
|
|
|
|
protected function processQrisPayment($orderId)
|
|
{
|
|
$this->dispatch('logToConsole', message: 'Memulai pembayaran QRIS...');
|
|
|
|
try {
|
|
// Generate or get your static QRIS image URL
|
|
// This could be from your payment gateway or a static image
|
|
$this->qrisImageUrl = url('img/qr-code.png'); // Replace with actual QRIS image
|
|
|
|
// Show the QRIS payment modal
|
|
$this->showQrisPayment = true;
|
|
|
|
$this->dispatch('notify',
|
|
message: 'Silakan scan QRIS untuk melakukan pembayaran',
|
|
type: 'info'
|
|
);
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Error QRIS: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
|
$this->dispatch('notify',
|
|
message: 'Gagal memproses pembayaran QRIS: ' . $e->getMessage(),
|
|
type: 'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
public function confirmQrisPayment()
|
|
{
|
|
try {
|
|
// Here you would typically verify the payment with your payment gateway
|
|
// For now we'll just simulate successful payment
|
|
|
|
$this->showQrisPayment = false;
|
|
$this->clearCartAndResetState();
|
|
|
|
$this->dispatch('notify',
|
|
message: 'Pembayaran QRIS berhasil dikonfirmasi!',
|
|
type: 'success'
|
|
);
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("QRIS confirmation error: " . $e->getMessage());
|
|
$this->dispatch('notify',
|
|
message: 'Gagal mengkonfirmasi pembayaran QRIS: ' . $e->getMessage(),
|
|
type: 'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
private function getSelectedTableData()
|
|
{
|
|
$tableId = $this->selectedTableId ?? $this->selectedOccupiedTableId;
|
|
|
|
return $this->allTablesData->firstWhere('id', $tableId)?->toArray() ?? [
|
|
'id' => null,
|
|
'device_id' => null,
|
|
];
|
|
}
|
|
|
|
public function handlePaymentSuccess()
|
|
{
|
|
$this->clearCartAndResetState();
|
|
$this->dispatch('notify', message: 'Pembayaran berhasil! Menunggu konfirmasi tolong konfirmasi ke kasir...', type: 'success');
|
|
}
|
|
|
|
protected function sendReceiptEmail($order)
|
|
{
|
|
try {
|
|
if ($order->customer_email && filter_var($order->customer_email, FILTER_VALIDATE_EMAIL)) {
|
|
Mail::to($order->customer_email)->send(new OrderReceipt($order));
|
|
$this->dispatch('logToConsole', message: 'Order receipt email sent to ' . $order->customer_email);
|
|
} else {
|
|
$this->dispatch('logToConsole', message: 'No valid email provided for receipt.');
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to send order receipt email: " . $e->getMessage());
|
|
$this->dispatch('notify', message: 'Gagal mengirim struk ke email.', type: 'error');
|
|
}
|
|
}
|
|
|
|
private function getSelectedTableForTransaction(): ?array
|
|
{
|
|
$tableId = $this->selectedTableId ?? $this->selectedOccupiedTableId;
|
|
if (empty($tableId)) {
|
|
$this->dispatch('logToConsole', message: 'No table ID selected for transaction.');
|
|
return null;
|
|
}
|
|
$table = $this->allTablesData->get($tableId);
|
|
if (!$table) {
|
|
$this->dispatch('logToConsole', message: 'Table data not found for ID: ' . $tableId);
|
|
} else {
|
|
$this->dispatch('logToConsole', message: 'Selected table data for transaction: ' . json_encode($table));
|
|
}
|
|
return $table;
|
|
}
|
|
|
|
private function clearCartAndResetState()
|
|
{
|
|
$this->cart = [];
|
|
session()->forget('cart');
|
|
$this->showCart = false;
|
|
$this->showEmailInput = true;
|
|
$this->customerEmail = '';
|
|
$this->selectedTableId = null;
|
|
$this->selectedOccupiedTableId = null;
|
|
$this->inputCustomerName = '';
|
|
$this->isExistingCustomer = false;
|
|
$this->showCustomerNameInput = false;
|
|
$this->isCustomerNameInputDisabled = false;
|
|
$this->resetValidation();
|
|
$this->selectedPaymentMethod = null;
|
|
$this->dispatch('logToConsole', message: 'Cart cleared and state reset.');
|
|
}
|
|
|
|
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(),
|
|
'tableIdFromUrl' => $this->tableIdFromUrl,
|
|
])->layout('components.layouts.front');
|
|
}
|
|
|
|
public function getFoodItemsProperty()
|
|
{
|
|
return Cache::remember('food_items_'.$this->typeItemId, now()->addHours(1), function() {
|
|
if ($this->typeItemId) {
|
|
return Items::where('type_item_id', $this->typeItemId)->get();
|
|
}
|
|
return Items::all();
|
|
});
|
|
}
|
|
|
|
public function getAllTypeItemsProperty()
|
|
{
|
|
return Cache::remember('all_type_items', now()->addHours(6), function() {
|
|
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.', type: 'error');
|
|
$this->dispatch('logToConsole', message: 'Failed to add to cart: Item ' . $itemId . ' not found.');
|
|
return;
|
|
}
|
|
|
|
$this->cart[$itemId] = ($this->cart[$itemId] ?? 0) + 1;
|
|
session()->put('cart', $this->cart);
|
|
$this->dispatch('notify', message: $item->name . ' ditambahkan ke keranjang.', type: 'success');
|
|
$this->dispatch('logToConsole', message: 'Added ' . $item->name . ' to cart. Current cart: ' . json_encode($this->cart));
|
|
}
|
|
|
|
public function removeFromCart($itemId)
|
|
{
|
|
if (isset($this->cart[$itemId])) {
|
|
$item = Items::find($itemId);
|
|
if ($this->cart[$itemId] > 1) {
|
|
$this->cart[$itemId]--;
|
|
$message = ($item->name ?? 'Item') . ' dikurangi dari keranjang.';
|
|
} else {
|
|
unset($this->cart[$itemId]);
|
|
$message = ($item->name ?? 'Item') . ' dihapus dari keranjang.';
|
|
}
|
|
session()->put('cart', $this->cart);
|
|
$this->dispatch('notify', message: $message, type: 'info');
|
|
|
|
$this->dispatch('logToConsole', message: $message . '. Current cart: ' . json_encode($this->cart));
|
|
|
|
if (empty($this->cart)) {
|
|
$this->showCart = false;
|
|
$this->dispatch('logToConsole', message: 'Cart is now empty. Closing cart view.');
|
|
}
|
|
} else {
|
|
$this->dispatch('logToConsole', message: 'Attempted to remove non-existent item ' . $itemId . ' from cart.');
|
|
$this->dispatch('notify', message: 'Item tidak ditemukan di keranjang.', type: 'warning');
|
|
}
|
|
}
|
|
|
|
public function openCart()
|
|
{
|
|
$this->showCart = true;
|
|
$this->dispatch('logToConsole', message: 'Cart opened.');
|
|
}
|
|
|
|
public function closeCart()
|
|
{
|
|
$this->showCart = false;
|
|
$this->dispatch('logToConsole', message: 'Cart closed.');
|
|
}
|
|
|
|
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);
|
|
$this->dispatch('logToConsole', message: 'Filtered by type: ' . $type->name . ' (ID: ' . $typeId . '). URL updated.');
|
|
} else {
|
|
$this->dispatch('logToConsole', message: 'Attempted to filter by non-existent type ID: ' . $typeId);
|
|
$this->dispatch('notify', message: 'Tipe menu tidak ditemukan.', type: 'warning');
|
|
}
|
|
}
|
|
}
|