This commit is contained in:
DandhiAri 2025-06-29 23:51:53 +07:00
parent dc25732b43
commit 9da05619c9
57 changed files with 146640 additions and 1001 deletions

View File

@ -1,97 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Bundles; // Asumsikan ada model Bundle
use Illuminate\Http\Request;
class BundlesController extends Controller
{
/**
* Menampilkan daftar semua Bundles.
*/
public function index()
{
// Mengambil semua data bundle dari database
$bundles = Bundles::all();
// Mengembalikan view dengan data bundles
return view('pages.back.bundles.index', compact('bundles'));
}
/**
* Menampilkan form untuk membuat Bundle baru.
*/
public function create()
{
// Mengembalikan view form pembuatan bundle
return view('pages.back.bundles.create');
}
/**
* Menyimpan Bundle baru ke database.
*/
public function store(Request $request)
{
// Validasi data input dari form
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
// Tambahkan aturan validasi lain sesuai kolom tabel bundles Anda
]);
// Membuat instance Bundle baru dan menyimpannya
Bundle::create($validatedData);
// Redirect ke halaman index bundles dengan pesan sukses
return redirect()->route('pages.back.bundles.index')->with('success', 'Bundle berhasil ditambahkan!');
}
/**
* Menampilkan detail satu Bundle tertentu.
*/
public function show(Bundle $bundle) // Route model binding
{
// Mengembalikan view detail bundle dengan data bundle yang ditemukan
return view('pages.back.bundles.show', compact('bundle'));
}
/**
* Menampilkan form untuk mengedit Bundle yang sudah ada.
*/
public function edit(Bundle $bundle) // Route model binding
{
// Mengembalikan view form edit bundle dengan data bundle yang ditemukan
return view('pages.back.bundles.edit', compact('bundle'));
}
/**
* Memperbarui data Bundle di database.
*/
public function update(Request $request, Bundle $bundle) // Route model binding
{
// Validasi data input yang diperbarui
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
// Tambahkan aturan validasi lain
]);
// Memperbarui data bundle
$bundle->update($validatedData);
// Redirect ke halaman index bundles dengan pesan sukses
return redirect()->route('pages.back.bundles.index')->with('success', 'Bundle berhasil diperbarui!');
}
/**
* Menghapus Bundle dari database.
*/
public function destroy(Bundle $bundle) // Route model binding
{
// Menghapus bundle
$bundle->delete();
// Redirect ke halaman index bundles dengan pesan sukses
return redirect()->route('pages.back.bundles.index')->with('success', 'Bundle berhasil dihapus!');
}
}

View File

@ -2,93 +2,86 @@
namespace App\Http\Controllers;
use App\Models\Items; // Asumsikan ada model Item (untuk makanan/minuman)
use App\Models\Items;
use App\Models\TypeItems;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ItemsController extends Controller
{
/**
* Menampilkan daftar semua Items (makanan/minuman).
*/
public function index()
{
$items = Items::all(); // Atau bisa juga Item::with('category', 'typeItem')->get();
$items = Items::with('typeItem')->get();
return view('pages.back.items.index', compact('items'));
}
/**
* Menampilkan form untuk membuat Item baru.
*/
public function create()
{
// Jika ada relasi ke kategori atau type, Anda mungkin perlu mengirim data itu juga
// $categories = Category::all();
// $typeItems = TypeItem::all();
return view('pages.back.items.create'); // , compact('categories', 'typeItems')
$typeItems = TypeItems::all();
return view('pages.back.items.create', compact('typeItems'));
}
/**
* Menyimpan Item baru ke database.
*/
public function store(Request $request)
{
$validatedData = $request->validate([
$rules = [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'category_id' => 'nullable|exists:categories,id', // Jika ada relasi
'type_item_id' => 'nullable|exists:type_items,id', // Jika ada relasi
// ... kolom lain seperti image_url, is_available
]);
'image_url' => 'nullable|url|max:255',
'is_available' => 'boolean',
'type_item_id' => 'required|exists:type_items,id',
];
$validatedData = $request->validate($rules);
Items::create($validatedData);
return redirect()->route('items.index')->with('success', 'Item berhasil ditambahkan!');
}
/**
* Menampilkan detail satu Item tertentu.
*/
public function show(Item $item)
public function show(Items $item)
{
$item->load('typeItem');
return view('pages.back.items.show', compact('item'));
}
/**
* Menampilkan form untuk mengedit Item yang sudah ada.
*/
public function edit(Item $item)
public function edit(Items $item)
{
// $categories = Category::all();
// $typeItems = TypeItem::all();
return view('pages.back.items.edit', compact('item')); // , compact('item', 'categories', 'typeItems')
$typeItems = TypeItems::all();
return view('pages.back.items.edit', compact('item', 'typeItems'));
}
/**
* Memperbarui data Item di database.
*/
public function update(Request $request, Item $item)
public function update(Request $request, Items $item)
{
$validatedData = $request->validate([
'name' => 'required|string|max:255',
$rules = [
'name' => [
'required',
'string',
'max:255',
// Rule::unique('items')->ignore($item->id),
],
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'category_id' => 'nullable|exists:categories,id',
'type_item_id' => 'nullable|exists:type_items,id',
// ...
]);
'image_url' => 'nullable|url|max:255',
'is_available' => 'boolean',
'type_item_id' => 'required|exists:type_items,id',
];
$validatedData = $request->validate($rules);
$item->update($validatedData);
return redirect()->route('items.index')->with('success', 'Item berhasil diperbarui!');
}
/**
* Menghapus Item dari database.
*/
public function destroy(Item $item)
public function destroy(Items $item)
{
$item->delete();
return redirect()->route('items.index')->with('success', 'Item berhasil dihapus!');
}
}

View File

@ -1,205 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Bundles; // Gunakan singular Bundle
use App\Models\Items; // Gunakan singular Item
use Livewire\Component;
use Livewire\WithPagination;
class BundleTable extends Component
{
use WithPagination;
public $search = '';
public $sortBy = 'name';
public $sortDirection = 'asc';
// Properti untuk form Create/Edit Bundle
public $bundleId;
public $name;
public $description;
public $price;
public $is_active = true;
// Untuk fitur mengelola item dalam bundle (penting untuk Many-to-Many)
public $selectedItems = []; // Array ID item yang dipilih untuk bundle ini
public $allItems = [];// Semua item yang tersedia untuk dipilih di form
public $isModalOpen = false;
public $isDeleteModalOpen = false;
public $bundleToDeleteId;
// Aturan validasi
protected $rules = [
'name' => 'required|string|max:255|unique:bundles,name',
'description' => 'nullable|string',
'price' => 'nullable|numeric|min:0',
'is_active' => 'boolean',
'selectedItems' => 'nullable|array', // selectedItems sekarang adalah array
'selectedItems.*' => 'exists:items,id', // Setiap ID dalam array harus ada di tabel items
];
// Mounted lifecycle hook untuk mengambil data dropdown
public function mount()
{
$this->allItems = Items::all(['id', 'name']); // Ambil semua item untuk pilihan di form
}
// Reset halaman pagination setiap kali properti pencarian berubah
public function updatingSearch()
{
$this->resetPage();
}
// Metode untuk mengubah kolom sorting
public function sortBy($field)
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
}
// Metode untuk membuka modal Create Bundle
public function create()
{
$this->resetInputFields();
$this->is_active = true;
// Tidak perlu mengambil allItems lagi karena sudah di mount(),
// tapi pastikan selectedItems direset
$this->selectedItems = [];
$this->isModalOpen = true;
}
// Metode untuk mengisi form edit dan membuka modal
public function edit($id)
{
$bundle = Bundles::findOrFail($id); // Gunakan Bundle
$this->bundleId = $bundle->id;
$this->name = $bundle->name;
$this->description = $bundle->description;
$this->price = $bundle->price;
$this->is_active = $bundle->is_active;
// === PENTING: Ambil ID item yang sudah terkait dengan bundle ini ===
$this->selectedItems = $bundle->items->pluck('id')->toArray();
$this->isModalOpen = true;
}
// Metode untuk menyimpan atau memperbarui Bundle
public function store()
{
$rules = $this->rules;
if ($this->bundleId) {
// Untuk update, 'name' harus unique kecuali jika itu adalah nama dari bundle yang sedang diedit
$rules['name'] = 'required|string|max:255|unique:bundles,name,' . $this->bundleId;
}
$this->validate($rules);
$bundleData = [
'name' => $this->name,
'description' => $this->description,
'price' => $this->price,
'is_active' => $this->is_active,
];
$bundle = null; // Inisialisasi variabel $bundle
if ($this->bundleId) {
// Update Bundle
$bundle = Bundles::find($this->bundleId); // Gunakan Bundle
$bundle->update($bundleData);
session()->flash('message', 'Bundle berhasil diperbarui!');
} else {
// Create Bundle
$bundle = Bundles::create($bundleData); // Gunakan Bundle
session()->flash('message', 'Bundle berhasil ditambahkan!');
}
// === PENTING: Sinkronkan relasi many-to-many setelah bundle dibuat/diperbarui ===
if ($bundle && !empty($this->selectedItems)) {
$bundle->items()->sync($this->selectedItems);
} elseif ($bundle) {
// Jika tidak ada item yang dipilih, detach semua relasi item yang ada
$bundle->items()->detach();
}
$this->closeModal();
$this->resetInputFields();
}
// Metode untuk membuka modal konfirmasi hapus
public function confirmDelete($id)
{
$this->bundleToDeleteId = $id;
$this->isDeleteModalOpen = true;
}
// Metode untuk menghapus Bundle
public function delete()
{
if ($this->bundleToDeleteId) {
$bundle = Bundles::find($this->bundleToDeleteId); // Gunakan Bundle
if ($bundle) {
// Relasi many-to-many secara otomatis akan di-cascade oleh foreign key ON DELETE CASCADE
// di tabel pivot (bundle_item) ketika bundle dihapus.
// Oleh karena itu, $bundle->items()->detach(); sebenarnya tidak mutlak diperlukan di sini
// jika DB constraint sudah diatur dengan benar.
// Namun, secara eksplisit detach juga aman dilakukan.
// $bundle->items()->detach();
$bundle->delete();
session()->flash('message', 'Bundle berhasil dihapus!');
}
}
$this->closeDeleteModal();
}
// Tutup modal create/edit
public function closeModal()
{
$this->isModalOpen = false;
$this->resetValidation();
}
// Tutup modal konfirmasi hapus
public function closeDeleteModal()
{
$this->isDeleteModalOpen = false;
$this->bundleToDeleteId = null;
}
// Reset semua input form
private function resetInputFields()
{
$this->bundleId = null;
$this->name = '';
$this->description = '';
$this->price = '';
$this->is_active = true;
$this->selectedItems = []; // Reset selectedItems ke array kosong
}
// Metode render komponen Livewire
public function render()
{
$bundles = Bundles::query() // Gunakan Bundle
->when($this->search, function ($query) {
$query->where('name', 'like', '%' . $this->search . '%')
->orWhere('description', 'like', '%' . $this->search . '%');
})
->orderBy($this->sortBy, $this->sortDirection)
// === EAGER LOAD RELASI ITEMS UNTUK MENAMPILKANNYA DI VIEW ===
->with('items') // Eager load relasi items untuk menampilkan di tabel
->paginate(10);
return view('livewire.bundle-table', [
'bundles' => $bundles,
// allItems akan tersedia secara otomatis karena public property
]);
}
}

View File

@ -11,8 +11,8 @@
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderReceipt;
use Kreait\Firebase\Factory;
use Kreait\Firebase\ServiceAccount;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class FoodOrder extends Component
{
@ -21,181 +21,137 @@ class FoodOrder extends Component
public ?int $typeItemId = null;
public bool $showEmailInput = false;
public string $customerEmail = '';
public bool $isExistingCustomer = false; // Meja sudah dipesan
public ?string $selectedTableId = null; // Untuk meja yang tersedia (table_activation_sensor_active = 0)
public ?string $selectedOccupiedTableId = null; // Untuk meja yang sudah diduduki (table_activation_sensor_active = 1, dengan reserved_by)
public string $inputCustomerName = ''; // Input nama pemesan
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 = []; // Meja yang table_activation_sensor_active = 0
public array $occupiedTables = []; // Meja yang table_activation_sensor_active = 1 dan punya reserved_by
public Collection $allTablesData; // Menggunakan Collection untuk pencarian lebih cepat
public array $availableTables = [];
public array $occupiedTables = [];
public Collection $allTablesData;
public bool $showMapModal = false;
public ?string $tableIdFromUrl = null;
public ?string $tableIdFromUrl = null; // ID meja dari URL QR code
private array $lastFirebaseDataHash = [];
protected array $rules = [
'customerEmail' => 'required|email|max:255',
'selectedTableId' => 'nullable|string',
'selectedOccupiedTableId' => 'nullable|string',
'inputCustomerName' => 'nullable|string|max:255', // Validasi bersyarat
'inputCustomerName' => 'nullable|string|max:255',
];
protected $listeners = [
'paymentSuccess' => 'handlePaymentSuccess',
'tableStatusUpdated' => 'syncTablesFromFirebase', // Refresh table data after status update
'tableStatusUpdated' => 'syncTablesFromFirebase',
];
public function mount($typeSlug = null)
{
$this->cart = session()->get('cart', []);
$this->allTablesData = collect(); // Inisialisasi collection kosong
$this->allTablesData = collect();
$this->syncTablesFromFirebase();
if (request()->has('table_id')) {
$this->tableIdFromUrl = request()->query('table_id');
}
$this->tableIdFromUrl = request()->query('table_id');
$type = $typeSlug
? TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', [Str::lower($typeSlug)])->first()
: TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', ['makanan'])->first();
// Handle default typeSlug redirection only if not already on the correct path
if (empty($typeSlug) && (request()->route()->getName() !== 'menu.byType' || request()->segment(2) !== 'makanan')) {
return redirect()->route('menu.byType', ['typeSlug' => 'makanan']);
}
if (!empty($typeSlug)) {
$foundType = TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', [Str::lower($typeSlug)])->first();
$this->typeItemId = $foundType->id ?? null;
if (!$this->typeItemId) {
// If typeSlug is provided but not found, redirect to default 'makanan'
return redirect()->route('menu.byType', ['typeSlug' => 'makanan']);
}
} else {
// Default to 'makanan' if no typeSlug is provided
$defaultType = TypeItems::whereRaw('LOWER(REPLACE(name, " ", "-")) = ?', ['makanan'])->first();
$this->typeItemId = $defaultType->id ?? null;
}
}
private function attemptAutoSelectTable()
{
if ($this->tableIdFromUrl && $this->allTablesData->isNotEmpty()) {
$foundTable = $this->allTablesData->firstWhere('device_id', $this->tableIdFromUrl);
if ($foundTable) {
$isActive = $foundTable['firebase_data']['sensors']['table_activation_sensor_active'] ?? 0;
$hasReservedBy = isset($foundTable['reserved_by']) && !empty($foundTable['reserved_by']);
if ($isActive === 0) { // Meja tersedia
$this->selectedTableId = $foundTable['id'];
$this->selectedOccupiedTableId = null; // Pastikan kosong
$this->inputCustomerName = auth()->user()->name ?? 'Guest';
$this->isExistingCustomer = false;
$this->dispatch('notify', message: 'Meja ' . $foundTable['device_id'] . ' berhasil dipilih secara otomatis.');
} elseif ($hasReservedBy) { // Meja tidak tersedia dan punya nama pemesan
$this->selectedOccupiedTableId = $foundTable['id'];
$this->selectedTableId = null; // Pastikan kosong
$this->isExistingCustomer = true;
$this->dispatch('notify', message: 'Meja ' . $foundTable['device_id'] . ' sudah terisi oleh ' . $foundTable['reserved_by'] . '. Harap konfirmasi nama Anda.');
} else {
$this->dispatch('notify', message: 'Meja ' . $foundTable['device_id'] . ' saat ini tidak tersedia.');
$this->selectedTableId = null;
$this->selectedOccupiedTableId = null;
}
} else {
$this->dispatch('notify', message: 'Meja ' . $this->tableIdFromUrl . ' tidak ditemukan.');
$this->selectedTableId = null;
$this->selectedOccupiedTableId = null;
}
}
$this->typeItemId = $type->id ?? null;
}
private function getFirebaseDatabase()
{
try {
$firebaseCredentialsPath = config('services.firebase.credentials');
$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"));
// Periksa di lokasi default Laravel dan storage
if (!file_exists($firebaseCredentialsPath)) {
$firebaseCredentialsPath = storage_path('app/' . $firebaseCredentialsPath);
}
if (!file_exists($firebaseCredentialsPath)) {
throw new \Exception("Firebase credentials file not found at: " . $firebaseCredentialsPath);
}
$factory = (new Factory)
->withServiceAccount($firebaseCredentialsPath)
->withDatabaseUri(config('services.firebase.database_url'));
return $factory->createDatabase();
return (new Factory)
->withServiceAccount($path)
->withDatabaseUri(config('services.firebase.database_url'))
->createDatabase();
} catch (\Exception $e) {
logger()->error("Firebase initialization failed: " . $e->getMessage());
$this->dispatch('notify', message: 'Gagal terhubung ke sistem meja: ' . $e->getMessage());
logger()->error("Firebase init failed: " . $e->getMessage());
$this->dispatch('notify', message: 'Gagal koneksi Firebase: ' . $e->getMessage());
return null;
}
}
public function syncTablesFromFirebase()
public function syncTablesFromFirebase()
{
$database = $this->getFirebaseDatabase();
if (!$database) {
$this->availableTables = [];
$this->occupiedTables = [];
$this->allTablesData = collect();
return;
}
$db = $this->getFirebaseDatabase();
if (!$db) return $this->resetTables();
try {
$firebaseTablesData = $database->getReference('/')->getValue();
$processedAvailableTables = [];
$processedOccupiedTables = [];
$allProcessedTables = [];
$data = $db->getReference('/')->getValue();
if (!$data) return $this->resetTables();
if ($firebaseTablesData) {
foreach ($firebaseTablesData as $firebaseKey => $data) {
$isActive = $data['sensors']['table_activation_sensor_active'] ?? 0;
$isAvailable = ($isActive === 0);
$reservedBy = $data['reserved_by'] ?? null;
$hasReservedBy = !empty($reservedBy);
$currentHash = md5(json_encode($data));
if ($this->lastFirebaseDataHash['tables'] ?? null === $currentHash) {
return; // Tidak berubah, jangan update properti
}
$this->lastFirebaseDataHash['tables'] = $currentHash;
$tableDetail = [
'id' => $firebaseKey,
'device_id' => $data['device_id'] ?? $firebaseKey,
'table_activation_sensor_active' => $isActive,
'reserved_by' => $reservedBy,
'is_available' => $isAvailable,
'has_reserved_by' => $hasReservedBy,
'firebase_data' => $data // Simpan data mentah untuk referensi
];
$available = $occupied = $all = [];
$allProcessedTables[$firebaseKey] = $tableDetail; // Gunakan ID sebagai kunci untuk pencarian cepat
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
];
if ($isAvailable) {
$processedAvailableTables[] = $tableDetail;
} elseif ($hasReservedBy) {
$processedOccupiedTables[] = $tableDetail;
}
}
$all[$id] = $table;
$isActive ? ($reservedBy ? $occupied[] = $table : null) : $available[] = $table;
}
$this->availableTables = array_values($processedAvailableTables);
$this->occupiedTables = array_values($processedOccupiedTables);
$this->allTablesData = collect($allProcessedTables); // Convert to Collection
// Panggil attemptAutoSelectTable setelah data meja siap
$this->availableTables = $available;
$this->occupiedTables = $occupied;
$this->allTablesData = collect($all);
$this->attemptAutoSelectTable();
logger("Tables synced from Firebase. Total available tables: " . count($this->availableTables));
logger("Total occupied tables (with name): " . count($this->occupiedTables));
logger("Total tables for dropdown and map: " . $this->allTablesData->count());
} catch (\Exception $e) {
logger()->error("Failed to sync tables from Firebase: " . $e->getMessage());
$this->dispatch('notify', message: 'Gagal memperbarui status meja dari Firebase: ' . $e->getMessage());
$this->availableTables = [];
$this->occupiedTables = [];
$this->allTablesData = collect();
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.');
}
}
@ -441,18 +397,19 @@ private function updateTableAndResetState()
$this->clearCartAndResetState();
}
private function updateFirebaseTableStatus(string $firebaseTableId, array $updates)
public function updateFirebaseTableStatus(string $firebaseTableId, array $updates, bool $refresh = true)
{
$database = $this->getFirebaseDatabase();
if (!$database) {
return;
}
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.");
$this->syncTablesFromFirebase(); // Pertahankan ini untuk memastikan UI Livewire refresh
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());

View File

@ -4,12 +4,12 @@
use App\Models\Items;
use App\Models\TypeItems;
use App\Models\Bundles;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\File; // Tambahkan ini untuk operasi file
use Illuminate\Support\Facades\Storage; // Masih dipakai untuk temporary URL, tapi tidak untuk simpan/hapus di public_path
use Illuminate\Support\Facades\File;
use Intervention\Image\Laravel\Facades\Image;
// use Intervention\Image\ImageManager;
class ItemTable extends Component
{
@ -19,25 +19,22 @@ class ItemTable extends Component
public $search = '';
public $sortBy = 'name';
public $sortDirection = 'asc';
public $typeItemId = null;
public $itemId;
public $name;
public $description;
public $price;
public $image_url; // Ini akan menyimpan path relatif dari folder public, e.g., 'img/items/namafile.jpg'
public $new_image; // Ini adalah objek TemporaryUploadedFile dari Livewire
public $image_url;
public $new_image;
public $is_available = true;
public $type_item_id;
public $bundle_ids = [];
public $isModalOpen = false;
public $isDeleteModalOpen = false;
public $itemToDeleteId;
public $allTypeItems;
public $allBundles;
protected $queryString = [
'search' => ['except' => ''],
@ -52,15 +49,12 @@ class ItemTable extends Component
'price' => 'required|numeric|min:0',
'is_available' => 'boolean',
'type_item_id' => 'required|exists:type_items,id',
'bundle_ids' => 'nullable|array',
'bundle_ids.*' => 'exists:bundles,id',
'new_image' => 'nullable|image|max:1024', // Validasi tetap sama untuk file upload
'new_image' => 'nullable|image|max:1024',
];
public function mount()
{
$this->allTypeItems = TypeItems::all();
$this->allBundles = Bundles::all();
$this->typeItemId = request()->query('type_item_id');
}
@ -99,11 +93,10 @@ public function edit($id)
$this->name = $item->name;
$this->description = $item->description;
$this->price = $item->price;
$this->image_url = $item->image_url; // Path yang tersimpan di database
$this->image_url = $item->image_url;
$this->is_available = $item->is_available;
$this->type_item_id = $item->type_item_id;
$this->bundle_ids = $item->bundles->pluck('id')->toArray();
$this->new_image = null; // Reset new_image saat edit
$this->new_image = null;
$this->isModalOpen = true;
}
@ -119,46 +112,35 @@ public function store()
'type_item_id' => $this->type_item_id,
];
// LOGIKA PENYIMPANAN GAMBAR DIUBAH DI SINI
$targetPath = 'img/items/';
if (!File::isDirectory(public_path($targetPath))) {
File::makeDirectory(public_path($targetPath), 0755, true);
}
if ($this->new_image) {
// Hapus gambar lama jika ada dan file-nya masih ada di public
if ($this->itemId && $this->image_url) {
$oldImagePath = public_path($this->image_url);
if (File::exists($oldImagePath)) {
File::delete($oldImagePath);
}
if ($this->image_url && File::exists(public_path($this->image_url))) {
File::delete(public_path($this->image_url));
}
// Tentukan nama file baru (gunakan uniqid() atau timestamp untuk unik)
$fileName = uniqid() . '.' . $this->new_image->extension();
$targetPath = 'img/items/'; // Subfolder di dalam public
$webpName = uniqid('item_') . '.webp';
$image = Image::read($this->new_image->getRealPath())->toWebp(80);
$image->save(public_path($targetPath . $webpName));
// Pastikan folder target ada
if (!File::isDirectory(public_path($targetPath))) {
File::makeDirectory(public_path($targetPath), 0755, true, true);
}
// Pindahkan file dari temporary ke folder public yang diinginkan
$this->new_image->storeAs($targetPath, $fileName, 'public_path_disk'); // Menggunakan disk kustom 'public_path_disk'
$itemData['image_url'] = $targetPath . $fileName; // Simpan path relatif di database
}
$item = null;
if ($this->itemId) {
$itemData['image_url'] = $targetPath . $webpName;
} elseif ($this->itemId && !$this->image_url) {
$item = Items::find($this->itemId);
$item->update($itemData);
session()->flash('message', 'Item berhasil diperbarui!');
} else {
$item = Items::create($itemData);
session()->flash('message', 'Item berhasil ditambahkan!');
if ($item && $item->image_url && File::exists(public_path($item->image_url))) {
File::delete(public_path($item->image_url));
}
$itemData['image_url'] = null;
}
if ($item) {
$item->bundles()->sync($this->bundle_ids);
}
$item = Items::updateOrCreate(
['id' => $this->itemId],
$itemData
);
session()->flash('message', $this->itemId ? 'Item berhasil diperbarui!' : 'Item berhasil ditambahkan!');
$this->closeModal();
$this->resetInputFields();
}
@ -174,7 +156,6 @@ public function delete()
if ($this->itemToDeleteId) {
$item = Items::find($this->itemToDeleteId);
if ($item) {
// LOGIKA PENGHAPUSAN GAMBAR DIUBAH DI SINI
if ($item->image_url) {
$imagePath = public_path($item->image_url);
if (File::exists($imagePath)) {
@ -210,13 +191,6 @@ private function resetInputFields()
$this->new_image = null;
$this->is_available = true;
$this->type_item_id = '';
$this->bundle_ids = [];
}
public function setTypeItem($id = null)
{
$this->typeItemId = $id;
$this->resetPage();
}
public function render()
@ -230,14 +204,12 @@ public function render()
$query->where('type_item_id', $this->typeItemId)
)
->orderBy($this->sortBy, $this->sortDirection)
->with(['typeItem', 'bundles'])
->with(['typeItem'])
->paginate(10);
return view('livewire.item-table', [
'items' => $items,
'allTypeItems' => $this->allTypeItems,
'allBundles' => $this->allBundles,
'typeItemId' => $this->typeItemId,
]);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Bundles extends Model
{
use HasFactory;
protected $table = 'bundles';
protected $fillable = [
'name',
'description',
'price',
'is_active',
];
public function items(): BelongsToMany
{
return $this->belongsToMany(Items::class, 'bundle_items', 'bundle_id', 'item_id');
}
}

View File

@ -4,7 +4,6 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Items extends Model
{
@ -19,7 +18,6 @@ class Items extends Model
'image_url', // Tambahkan ini
'is_available',
'type_item_id',
'bundle_id',
];
/**
@ -30,11 +28,4 @@ public function typeItem()
return $this->belongsTo(TypeItems::class, 'type_item_id');
}
/**
* Relasi Many-to-One: Item ini milik satu Bundle.
*/
public function items(): BelongsToMany
{
return $this->belongsToMany(Items::class, 'bundle_items', 'bundle_id', 'item_id'); // Pastikan 'bundle_item'
}
}

View File

@ -24,9 +24,5 @@ public function boot(): void
View::composer('components.layouts.app.sidebar', function ($view) {
$view->with('typeitems', TypeItems::orderBy('name')->get());
});
View::composer('components.layouts.front', function ($view) {
$view->with('typeitems', TypeItems::has('items')->orderBy('name')->get());
});
}
}

View File

@ -11,6 +11,7 @@
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.9",
"intervention/image-laravel": "^1.5",
"jeroennoten/laravel-adminlte": "^3.15",
"kreait/laravel-firebase": "^6.1",
"laravel/fortify": "^1.25",

230
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e950c8c684cabe0d5943c715671e8406",
"content-hash": "3eb9fc05a96175f48f2098ecb75289fb",
"packages": [
{
"name": "almasaeed2010/adminlte",
@ -1999,6 +1999,234 @@
],
"time": "2025-02-03T10:55:03+00:00"
},
{
"name": "intervention/gif",
"version": "4.2.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/gif.git",
"reference": "5999eac6a39aa760fb803bc809e8909ee67b451a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a",
"reference": "5999eac6a39aa760fb803bc809e8909ee67b451a",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Gif\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Native PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"animation",
"gd",
"gif",
"image"
],
"support": {
"issues": "https://github.com/Intervention/gif/issues",
"source": "https://github.com/Intervention/gif/tree/4.2.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2025-03-29T07:46:21+00:00"
},
{
"name": "intervention/image",
"version": "3.11.3",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "d0f097b8a3fa8fb758efc9440b513aa3833cda17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/d0f097b8a3fa8fb758efc9440b513aa3833cda17",
"reference": "d0f097b8a3fa8fb758efc9440b513aa3833cda17",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"intervention/gif": "^4.2",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "PHP image manipulation",
"homepage": "https://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"resize",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/3.11.3"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2025-05-22T17:26:23+00:00"
},
{
"name": "intervention/image-laravel",
"version": "1.5.6",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image-laravel.git",
"reference": "056029431400a5cc56036172787a578f334683c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image-laravel/zipball/056029431400a5cc56036172787a578f334683c4",
"reference": "056029431400a5cc56036172787a578f334683c4",
"shasum": ""
},
"require": {
"illuminate/http": "^8|^9|^10|^11|^12",
"illuminate/routing": "^8|^9|^10|^11|^12",
"illuminate/support": "^8|^9|^10|^11|^12",
"intervention/image": "^3.11",
"php": "^8.1"
},
"require-dev": {
"ext-fileinfo": "*",
"orchestra/testbench": "^8.18 || ^9.9",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Image": "Intervention\\Image\\Laravel\\Facades\\Image"
},
"providers": [
"Intervention\\Image\\Laravel\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\Laravel\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Laravel Integration of Intervention Image",
"homepage": "https://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"resize",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image-laravel/issues",
"source": "https://github.com/Intervention/image-laravel/tree/1.5.6"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2025-04-04T15:09:55+00:00"
},
{
"name": "jeroennoten/laravel-adminlte",
"version": "v3.15.0",

46
config/image.php Normal file
View File

@ -0,0 +1,46 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports “GD Library” and “Imagick” to process images
| internally. Depending on your PHP setup, you can choose one of them.
|
| Included options:
| - \Intervention\Image\Drivers\Gd\Driver::class
| - \Intervention\Image\Drivers\Imagick\Driver::class
|
*/
'driver' => \Intervention\Image\Drivers\Gd\Driver::class,
/*
|--------------------------------------------------------------------------
| Configuration Options
|--------------------------------------------------------------------------
|
| These options control the behavior of Intervention Image.
|
| - "autoOrientation" controls whether an imported image should be
| automatically rotated according to any existing Exif data.
|
| - "decodeAnimation" decides whether a possibly animated image is
| decoded as such or whether the animation is discarded.
|
| - "blendingColor" Defines the default blending color.
|
| - "strip" controls if meta data like exif tags should be removed when
| encoding images.
*/
'options' => [
'autoOrientation' => true,
'decodeAnimation' => true,
'blendingColor' => 'ffffff',
'strip' => false,
]
];

View File

@ -41,10 +41,12 @@
],
'midtrans' => [
'server_key' => env('MIDTRANS_SERVER_KEY'),
'client_key' => env('MIDTRANS_CLIENT_KEY'),
'server_key' => env('MIDTRANS_SERVER_KEY'),
'is_production' => env('MIDTRANS_IS_PRODUCTION', false),
'snap_url' => env('MIDTRANS_SNAP_URL', 'https://app.sandbox.midtrans.com/snap/snap.js'),
'snap_url' => env('MIDTRANS_IS_PRODUCTION')
? 'https://app.midtrans.com/snap/snap.js'
: 'https://app.sandbox.midtrans.com/snap/snap.js',
],
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -31,8 +31,6 @@
<flux:navlist.group :heading="__('Other')" class="grid">
<flux:navlist.item icon="archive-box-arrow-down" :href="route('typeitems.index')"
:current="request()->routeIs('typeitems.index')" wire:navigate>{{ __('Type Items') }}</flux:navlist.item>
<flux:navlist.item icon="tag" :href="route('bundles.index')"
:current="request()->routeIs('bundles.index')" wire:navigate>{{ __('Bundles') }}</flux:navlist.item>
<flux:navlist.item icon="lock-closed" :href="route('tables.index')"
:current="request()->routeIs('tables.index')" wire:navigate>{{ __('Tabels') }}</flux:navlist.item>
</flux:navlist.group>

View File

@ -115,31 +115,26 @@
}
});
// Pindahkan setInterval untuk refresh CSRF di sini, di dalam livewire:initialized
setInterval(() => {
fetch('/refresh-csrf')
fetch('/refresh-csrf', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
if (csrfMeta) {
csrfMeta.setAttribute('content', data.csrf_token);
}
// Pastikan Livewire sudah ada sebelum mencoba mengaksesnya
if (window.Livewire && typeof window.Livewire.findComponents === 'function') {
window.Livewire.findComponents().forEach(component => {
if (component.canonical) {
component.canonical.csrf = data.csrf_token;
}
});
} else {
console.warn('Livewire.findComponents not available yet.');
}
// Tidak perlu akses internal Livewire jika ingin aman dari perubahan API internal
console.log('CSRF token refreshed:', data.csrf_token);
})
.catch(error => {
console.error('Error refreshing CSRF token:', error);
});
}, 1800000); // 30 menit (1800000 ms)
}, 1800000); // 30 menit
});
</script>
</body>

View File

@ -94,7 +94,7 @@ class="mr-2 form-checkbox h-5 w-5 text-blue-600">
<div class="mb-4">
<label for="available-table-select" class="block text-gray-700 text-sm font-bold mb-2">Meja
Tersedia:</label>
<select id="available-table-select" wire:model.live="selectedTableId"
<select id="available-table-select" wire:model.live="selectedTableId" wire:key="select-available"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
{{ $tableIdFromUrl && $selectedOccupiedTableId === null ? 'disabled' : '' }}>
<option value="">-- Pilih Meja Tersedia --</option>
@ -108,31 +108,25 @@ class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 lead
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
@else
{{-- Dropdown untuk Meja Sudah Terisi (dengan Nama Pemesan) --}}
@if (!empty($occupiedTables))
<div class="mb-4">
<label for="occupied-table-select"
class="block text-gray-700 text-sm font-bold mb-2">Meja Sudah Terisi (Konfirmasi
Nama):</label>
<select id="occupied-table-select" wire:model.live="selectedOccupiedTableId"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
{{ $tableIdFromUrl && $selectedTableId === null ? 'disabled' : '' }}>
<option value="">-- Pilih Meja Terisi --</option>
@foreach ($occupiedTables as $table)
<option value="{{ $table['id'] }}">
{{ $table['device_id'] }} || Oleh: {{ $table['reserved_by'] }}
</option>
@endforeach
</select>
@error('selectedOccupiedTableId')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
@else
<p class="text-gray-600 text-sm">Tidak ada meja yang saat ini terisi dengan nama pemesan.
</p>
@endif
@elseif ($isExistingCustomer)
<div class="mb-4">
<label for="occupied-table-select"
class="block text-gray-700 text-sm font-bold mb-2">Meja Sudah Terisi (Konfirmasi
Nama):</label>
<select id="occupied-table-select" wire:model.live="selectedOccupiedTableId" wire:key="select-occupied"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
{{ $tableIdFromUrl && $selectedTableId === null ? 'disabled' : '' }}>
<option value="">-- Pilih Meja Terisi --</option>
@foreach ($occupiedTables as $table)
<option value="{{ $table['id'] }}">
{{ $table['device_id'] }} || Oleh: {{ $table['reserved_by'] }}
</option>
@endforeach
</select>
@error('selectedOccupiedTableId')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
@endif
{{-- Input Nama Pemesan (TERBARU: Hanya muncul jika showCustomerNameInput TRUE) --}}
@ -247,8 +241,7 @@ class="bg-gray-600 hover:bg-gray-700 text-white py-2 rounded focus:outline-none
{{-- Overlay untuk sidebar keranjang --}}
@if ($showCart)
<div class="fixed inset-0 bg-black bg-opacity-50 z-20" wire:click="closeCart">
{{-- Ini adalah div overlay --}}
<div class="fixed inset-0 z-20" wire:click="closeCart">
<div class="absolute inset-0 blur-background"></div> {{-- Div blur palsu --}}
</div>
@endif

View File

@ -40,6 +40,10 @@ class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutra
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Gambar
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Deskripsi
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer"
wire:click="sortBy('price')">
@ -96,8 +100,11 @@ class="h-10 w-10 object-cover rounded">
<span class="text-neutral-400">Tidak ada gambar</span>
@endif
</td>
<td class="px-6 py-4 text-sm text-neutral-500 dark:text-neutral-300 max-w-xs truncate">
{{ $item->description ?? 'Tidak ada deskripsi' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-300">
Rp{{ number_format($item->price, 0, ',', '.') }}
Rp. {{ number_format($item->price, 0, ',', '.') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-300">
{{ $item->typeItem->name ?? 'N/A' }}
@ -124,7 +131,7 @@ class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-500
</tr>
@empty
<tr>
<td colspan="6"
<td colspan="7"
class="px-6 py-4 text-center text-sm text-neutral-500 dark:text-neutral-300">
Tidak ada data item yang ditemukan.
</td>
@ -210,7 +217,6 @@ class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Tipe It
<select id="type_item_id" wire:model.lazy="type_item_id"
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100">
<option value="">-- Pilih Tipe Item --</option>
{{-- --- UBAH: Gunakan $allTypeItems yang dilewatkan dari komponen Livewire --- --}}
@foreach ($allTypeItems as $typeItem)
<option value="{{ $typeItem->id }}">{{ $typeItem->name }}</option>
@endforeach
@ -220,29 +226,6 @@ class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 s
@enderror
</div>
{{-- --- BARU: Bagian untuk pemilihan Bundles (multi-select) --- --}}
<div class="mb-4">
<label for="bundle_ids"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Bundles</label>
{{-- --- UBAH: Tambahkan 'multiple' dan 'wire:model.live' untuk mendukung multi-select --- --}}
<select id="bundle_ids" wire:model.live="bundle_ids" multiple
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100 h-48">
<option value="">-- Pilih Bundles (Opsional) --</option>
{{-- --- UBAH: Gunakan $allBundles yang dilewatkan dari komponen Livewire --- --}}
@foreach ($allBundles as $bundle)
<option value="{{ $bundle->id }}">{{ $bundle->name }}</option>
@endforeach
</select>
{{-- --- BARU: Error messages untuk bundle_ids dan bundle_ids.* --- --}}
@error('bundle_ids')
<span class="text-red-500 text-xs">{{ $message }}</span>
@enderror
@error('bundle_ids.*')
<span class="text-red-500 text-xs">{{ $message }}</span>
@enderror
</div>
{{-- --- AKHIR BARU: Bagian untuk pemilihan Bundles --- --}}
<div class="flex justify-end gap-2">
<button type="button" wire:click="closeModal()"
class="inline-flex justify-center rounded-md border border-neutral-300 dark:border-neutral-700 shadow-sm px-4 py-2 bg-white dark:bg-neutral-700 text-base font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">

View File

@ -1,284 +0,0 @@
<div>
<style>
/* General Reset and Base */
* { box-sizing: border-box; }
body, html {
margin: 0; padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f1f3f5;
color: #333;
}
header {
background: #1d3557;
color: #fff;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 1.75rem; }
button.cart-btn {
background: transparent;
border: none;
color: white;
font-size: 1.5rem;
position: relative;
cursor: pointer;
}
span.cart-count {
position: absolute;
top: -8px;
right: -10px;
background: #e63946;
color: white;
border-radius: 50%;
font-size: 0.75rem;
padding: 2px 6px;
}
/* Hero Section */
.hero {
padding: 3rem 1rem;
text-align: center;
background: linear-gradient(to right, #f8c291, #f6e58d);
margin-bottom: 2rem;
}
.hero h2 {
margin: 0;
font-size: 2rem;
font-weight: bold;
}
/* Grid Layout for Items */
.menu-grid {
max-width: 1200px;
margin: 0 auto 4rem;
padding: 0 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
article.card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
overflow: hidden;
transition: 0.3s;
display: flex;
flex-direction: column;
}
article.card img {
width: 100%;
height: 160px;
object-fit: cover;
}
.card-body {
padding: 1rem;
display: flex;
flex-direction: column;
flex: 1;
}
.food-name {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.3rem;
flex: 1;
}
.food-price {
color: #2d6a4f;
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.8rem;
}
.quantity-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
input[type="number"] {
width: 60px;
padding: 0.3rem;
border: 1px solid #ccc;
border-radius: 5px;
}
/* Cart Modal */
.cart-popup {
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 999;
}
.cart-popup.show {
display: flex;
}
.cart-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 480px;
max-height: 80vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.cart-header, .cart-footer {
padding: 1rem;
background: #1d3557;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-items {
padding: 1rem;
flex: 1;
}
.cart-item {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #ccc;
}
.checkout-btn {
background-color: #1d3557;
color: white;
border: none;
border-radius: 10px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.checkout-btn:hover {
background-color: #457b9d;
}
.checkout-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
@media(max-width: 600px) {
header h1 {
font-size: 1.25rem;
}
.hero h2 {
font-size: 1.5rem;
}
}
</style>
<!-- Header -->
<header>
<h1>MyFood IoT Ordering</h1>
<button class="cart-btn" wire:click="openCart">
🛒
@if(array_sum($cart) > 0)
<span class="cart-count">{{ array_sum($cart) }}</span>
@endif
</button>
</header>
<!-- Hero for Food -->
<section class="hero">
<h2>🍔 Daftar Makanan</h2>
</section>
<!-- Food Items Grid -->
<section class="menu-grid" aria-label="Makanan">
@foreach($foodItems as $item)
@if($item['type'] === 'food')
<article class="card">
<img src="{{ $item['image_url'] }}" alt="{{ $item['name'] }}">
<div class="card-body">
<div class="food-name">{{ $item['name'] }}</div>
<div class="food-price">Rp {{ number_format($item['price'], 0, ',', '.') }}</div>
<div class="quantity-wrapper">
<label>Qty:</label>
<input type="number" min="0" wire:model.live.debounce.300ms="cart.{{ $item['id'] }}">
</div>
</div>
</article>
@endif
@endforeach
</section>
<!-- Hero for Drink -->
<section class="hero" style="background: linear-gradient(to right, #74b9ff, #a29bfe);">
<h2>🥤 Daftar Minuman</h2>
</section>
<!-- Drink Items Grid -->
<section class="menu-grid" aria-label="Minuman">
@foreach($foodItems as $item)
@if($item['type'] === 'beverage')
<article class="card">
<img src="{{ $item['image_url'] }}" alt="{{ $item['name'] }}">
<div class="card-body">
<div class="food-name">{{ $item['name'] }}</div>
<div class="food-price">Rp {{ number_format($item['price'], 0, ',', '.') }}</div>
<div class="quantity-wrapper">
<label>Qty:</label>
<input type="number" min="0" wire:model.live.debounce.300ms="cart.{{ $item['id'] }}">
</div>
</div>
</article>
@endif
@endforeach
</section>
<!-- Cart Modal -->
<div class="cart-popup @if($showCart) show @endif" wire:keydown.escape="closeCart" wire:click.self="closeCart">
<div class="cart-content">
<div class="cart-header">
<h3>🛒 Keranjang</h3>
<button wire:click="closeCart" style="font-size: 1.5rem;">&times;</button>
</div>
<div class="cart-items">
@if(empty($cartItems))
<p>Keranjang kosong.</p>
@else
@foreach($cartItems as $item)
<div class="cart-item">
<span>{{ $item['name'] }}</span>
<span>x{{ $item['qty'] }}</span>
<span>Rp {{ number_format($item['total_price'], 0, ',', '.') }}</span>
</div>
@endforeach
@endif
</div>
<div class="cart-footer">
<span>Total: Rp {{ number_format($cartTotal, 0, ',', '.') }}</span>
<button class="checkout-btn" wire:click="checkout" @if(count($cartItems ?? []) === 0) disabled @endif>Checkout</button>
</div>
</div>
</div>
<!-- Midtrans + Notify -->
<script src="https://app.sandbox.midtrans.com/snap/snap.js" data-client-key="{{ config('services.midtrans.client_key') }}"></script>
<script>
window.addEventListener('midtransSnapToken', event => {
const token = event.detail.token;
window.snap.pay(token, {
onSuccess: () => window.livewire.dispatch('paymentSuccess'),
onPending: () => window.livewire.dispatch('paymentSuccess'),
onError: (result) => alert('Error: ' + result.status_message),
onClose: () => {}
});
});
window.addEventListener('notify', event => {
alert(event.detail.message);
});
</script>
</div>

View File

@ -9,14 +9,18 @@
use App\Http\Controllers\TypeItemsController;
use App\Http\Controllers\ItemsController;
use App\Http\Controllers\MejaController;
use App\Http\Controllers\BundlesController;
Route::get('/refresh-csrf', function () {
return response()->json(['csrf_token' => csrf_token()]);
});
// Route::get('/', FoodOrder::class)->name('menu.all');
Route::redirect('/', '/menu', 302);
Route::get('/', function () {
return redirect()->route('menu.byType', ['typeSlug' => 'makanan']);
})->name('home');
Route::get('/menu', function () {
return redirect()->route('menu.byType', ['typeSlug' => 'makanan']);
});
Route::get('/menu/{typeSlug?}', FoodOrder::class)->name('menu.byType');
Route::get('/pilih-meja', SelectTable::class)->name('select.table');
@ -30,7 +34,6 @@
Route::resource('typeitems',TypeItemsController::class);
Route::resource('items',ItemsController::class);
Route::resource('tables',MejaController::class);
Route::resource('bundles',BundlesController::class);
Route::get('settings/profile', Profile::class)->name('settings.profile');
Route::get('settings/password', Password::class)->name('settings.password');

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,19 @@
<?php
namespace Facades\Livewire\Features\SupportFileUploads;
use Illuminate\Support\Facades\Facade;
/**
* @mixin \Livewire\Features\SupportFileUploads\GenerateSignedUploadUrl
*/
class GenerateSignedUploadUrl extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'Livewire\Features\SupportFileUploads\GenerateSignedUploadUrl';
}
}

View File

@ -95,7 +95,7 @@ class="mr-2 form-checkbox h-5 w-5 text-blue-600">
<div class="mb-4">
<label for="available-table-select" class="block text-gray-700 text-sm font-bold mb-2">Meja
Tersedia:</label>
<select id="available-table-select" wire:model.live="selectedTableId"
<select id="available-table-select" wire:model.live="selectedTableId" wire:key="select-available"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
<?php echo e($tableIdFromUrl && $selectedOccupiedTableId === null ? 'disabled' : ''); ?>>
<option value="">-- Pilih Meja Tersedia --</option>
@ -116,39 +116,33 @@ class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 lead
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<?php else: ?>
<?php elseif($isExistingCustomer): ?>
<div class="mb-4">
<label for="occupied-table-select"
class="block text-gray-700 text-sm font-bold mb-2">Meja Sudah Terisi (Konfirmasi
Nama):</label>
<select id="occupied-table-select" wire:model.live="selectedOccupiedTableId" wire:key="select-occupied"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
<?php echo e($tableIdFromUrl && $selectedTableId === null ? 'disabled' : ''); ?>>
<option value="">-- Pilih Meja Terisi --</option>
<!--[if BLOCK]><![endif]--><?php $__currentLoopData = $occupiedTables; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $table): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($table['id']); ?>">
<?php echo e($table['device_id']); ?> || Oleh: <?php echo e($table['reserved_by']); ?>
<!--[if BLOCK]><![endif]--><?php if(!empty($occupiedTables)): ?>
<div class="mb-4">
<label for="occupied-table-select"
class="block text-gray-700 text-sm font-bold mb-2">Meja Sudah Terisi (Konfirmasi
Nama):</label>
<select id="occupied-table-select" wire:model.live="selectedOccupiedTableId"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
<?php echo e($tableIdFromUrl && $selectedTableId === null ? 'disabled' : ''); ?>>
<option value="">-- Pilih Meja Terisi --</option>
<!--[if BLOCK]><![endif]--><?php $__currentLoopData = $occupiedTables; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $table): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($table['id']); ?>">
<?php echo e($table['device_id']); ?> || Oleh: <?php echo e($table['reserved_by']); ?>
</option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?><!--[if ENDBLOCK]><![endif]-->
</select>
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['selectedOccupiedTableId'];
</option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?><!--[if ENDBLOCK]><![endif]-->
</select>
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['selectedOccupiedTableId'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-sm mt-1"><?php echo e($message); ?></span>
<?php unset($message);
<span class="text-red-500 text-sm mt-1"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<?php else: ?>
<p class="text-gray-600 text-sm">Tidak ada meja yang saat ini terisi dengan nama pemesan.
</p>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</div>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
@ -277,8 +271,7 @@ class="bg-gray-600 hover:bg-gray-700 text-white py-2 rounded focus:outline-none
<!--[if BLOCK]><![endif]--><?php if($showCart): ?>
<div class="fixed inset-0 bg-black bg-opacity-50 z-20" wire:click="closeCart">
<div class="fixed inset-0 z-20" wire:click="closeCart">
<div class="absolute inset-0 blur-background"></div>
</div>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->

View File

@ -181,25 +181,6 @@
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginalda376aa217444bbd92367ba1444eb3b8 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginalda376aa217444bbd92367ba1444eb3b8 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'e60dd9d2c3a62d619c9acb38f20d5aa5::navlist.item','data' => ['icon' => 'tag','href' => route('bundles.index'),'current' => request()->routeIs('bundles.index'),'wire:navigate' => true]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('flux::navlist.item'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['icon' => 'tag','href' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(route('bundles.index')),'current' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(request()->routeIs('bundles.index')),'wire:navigate' => true]); ?><?php echo e(__('Bundles')); ?> <?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginalda376aa217444bbd92367ba1444eb3b8)): ?>
<?php $attributes = $__attributesOriginalda376aa217444bbd92367ba1444eb3b8; ?>
<?php unset($__attributesOriginalda376aa217444bbd92367ba1444eb3b8); ?>
<?php endif; ?>
<?php if (isset($__componentOriginalda376aa217444bbd92367ba1444eb3b8)): ?>
<?php $component = $__componentOriginalda376aa217444bbd92367ba1444eb3b8; ?>
<?php unset($__componentOriginalda376aa217444bbd92367ba1444eb3b8); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginalda376aa217444bbd92367ba1444eb3b8 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginalda376aa217444bbd92367ba1444eb3b8 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'e60dd9d2c3a62d619c9acb38f20d5aa5::navlist.item','data' => ['icon' => 'lock-closed','href' => route('tables.index'),'current' => request()->routeIs('tables.index'),'wire:navigate' => true]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('flux::navlist.item'); ?>
<?php if ($component->shouldRender()): ?>

View File

@ -118,31 +118,26 @@
}
});
// Pindahkan setInterval untuk refresh CSRF di sini, di dalam livewire:initialized
setInterval(() => {
fetch('/refresh-csrf')
fetch('/refresh-csrf', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
if (csrfMeta) {
csrfMeta.setAttribute('content', data.csrf_token);
}
// Pastikan Livewire sudah ada sebelum mencoba mengaksesnya
if (window.Livewire && typeof window.Livewire.findComponents === 'function') {
window.Livewire.findComponents().forEach(component => {
if (component.canonical) {
component.canonical.csrf = data.csrf_token;
}
});
} else {
console.warn('Livewire.findComponents not available yet.');
}
// Tidak perlu akses internal Livewire jika ingin aman dari perubahan API internal
console.log('CSRF token refreshed:', data.csrf_token);
})
.catch(error => {
console.error('Error refreshing CSRF token:', error);
});
}, 1800000); // 30 menit (1800000 ms)
}, 1800000); // 30 menit
});
</script>
</body>

View File

@ -0,0 +1,315 @@
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<!--[if BLOCK]><![endif]--><?php if(session()->has('message')): ?>
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 3000)"
class="bg-green-500 text-white p-3 rounded-lg shadow-md mb-4">
<?php echo e(session('message')); ?>
</div>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-neutral-800 dark:text-neutral-100">Daftar Item</h2>
<button wire:click="create()"
class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg">
Tambah Item
</button>
</div>
<div class="mb-4">
<input type="text" wire:model.live.debounce.300ms="search" placeholder="Cari item..."
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-neutral-300 dark:border-neutral-700 rounded-md dark:bg-neutral-800 dark:text-neutral-200">
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
<thead class="bg-neutral-50 dark:bg-neutral-800">
<tr>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer"
wire:click="sortBy('name')">
Nama Item
<!--[if BLOCK]><![endif]--><?php if($sortBy == 'name'): ?>
<!--[if BLOCK]><![endif]--><?php if($sortDirection == 'asc'): ?>
&uarr;
<?php else: ?>
&darr;
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Gambar
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
Deskripsi
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer"
wire:click="sortBy('price')">
Harga
<!--[if BLOCK]><![endif]--><?php if($sortBy == 'price'): ?>
<!--[if BLOCK]><![endif]--><?php if($sortDirection == 'asc'): ?>
&uarr;
<?php else: ?>
&darr;
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer"
wire:click="sortBy('type_item_id')">
Tipe Item
<!--[if BLOCK]><![endif]--><?php if($sortBy == 'type_item_id'): ?>
<!--[if BLOCK]><![endif]--><?php if($sortDirection == 'asc'): ?>
&uarr;
<?php else: ?>
&darr;
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer"
wire:click="sortBy('is_available')">
Tersedia
<!--[if BLOCK]><![endif]--><?php if($sortBy == 'is_available'): ?>
<!--[if BLOCK]><![endif]--><?php if($sortDirection == 'asc'): ?>
&uarr;
<?php else: ?>
&darr;
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Aksi</span>
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-neutral-900 divide-y divide-neutral-200 dark:divide-neutral-700">
<!--[if BLOCK]><![endif]--><?php $__empty_1 = true; $__currentLoopData = $items; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr>
<td
class="px-6 py-4 whitespace-nowrap text-sm font-medium text-neutral-900 dark:text-neutral-100">
<?php echo e($item->name); ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-300">
<!--[if BLOCK]><![endif]--><?php if($item->image_url): ?>
<img src="<?php echo e(asset($item->image_url)); ?>" alt="<?php echo e($item->name); ?>"
class="h-10 w-10 object-cover rounded">
<?php else: ?>
<span class="text-neutral-400">Tidak ada gambar</span>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</td>
<td class="px-6 py-4 text-sm text-neutral-500 dark:text-neutral-300 max-w-xs truncate">
<?php echo e($item->description ?? 'Tidak ada deskripsi'); ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-300">
Rp. <?php echo e(number_format($item->price, 0, ',', '.')); ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-300">
<?php echo e($item->typeItem->name ?? 'N/A'); ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<!--[if BLOCK]><![endif]--><?php if($item->is_available): ?>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
Ya
</span>
<?php else: ?>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100">
Tidak
</span>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button wire:click="edit(<?php echo e($item->id); ?>)"
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-500 mr-2">Edit</button>
<button wire:click="confirmDelete(<?php echo e($item->id); ?>)"
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-500">Hapus</button>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="7"
class="px-6 py-4 text-center text-sm text-neutral-500 dark:text-neutral-300">
Tidak ada data item yang ditemukan.
</td>
</tr>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</tbody>
</table>
</div>
<div class="mt-4">
<?php echo e($items->links()); ?>
</div>
</div>
<!--[if BLOCK]><![endif]--><?php if($isModalOpen): ?>
<div class="fixed inset-0 bg-gray-600 bg-opacity-75 flex items-center justify-center z-50">
<div
class="bg-white dark:bg-neutral-800 rounded-lg shadow-xl p-6 w-full max-w-lg mx-auto overflow-y-auto max-h-[90vh]">
<h3 class="text-lg leading-6 font-medium text-neutral-900 dark:text-neutral-100 mb-4">
<?php echo e($itemId ? 'Edit Item' : 'Tambah Item Baru'); ?>
</h3>
<form wire:submit.prevent="store">
<div class="mb-4">
<label for="name"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Nama Item</label>
<input type="text" id="name" wire:model.lazy="name"
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100">
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['name'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="mb-4">
<label for="description"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Deskripsi</label>
<textarea id="description" wire:model.lazy="description" rows="3"
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100"></textarea>
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['description'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="mb-4">
<label for="price"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Harga</label>
<input type="number" step="0.01" id="price" wire:model.lazy="price"
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100">
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['price'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="mb-4">
<label for="new_image"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Gambar Item</label>
<input type="file" id="new_image" wire:model="new_image"
class="mt-1 block w-full text-sm text-neutral-700 dark:text-neutral-200 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 dark:file:bg-neutral-700 dark:file:text-neutral-200 dark:hover:file:bg-neutral-600">
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['new_image'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
<!--[if BLOCK]><![endif]--><?php if($new_image): ?>
<div class="mt-2 text-sm text-neutral-500 dark:text-neutral-300">Pratinjau Gambar Baru:
</div>
<img src="<?php echo e($new_image->temporaryUrl()); ?>" class="mt-2 h-20 w-20 object-cover rounded">
<?php elseif($image_url): ?>
<div class="mt-2 text-sm text-neutral-500 dark:text-neutral-300">Gambar Saat Ini:</div>
<img src="<?php echo e(asset($image_url)); ?>" class="mt-2 h-20 w-20 object-cover rounded">
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="mb-4">
<label for="is_available"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Tersedia?</label>
<input type="checkbox" id="is_available" wire:model="is_available"
class="mt-1 rounded border-neutral-300 dark:border-neutral-700 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['is_available'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="mb-4">
<label for="type_item_id"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Tipe Item</label>
<select id="type_item_id" wire:model.lazy="type_item_id"
class="mt-1 block w-full rounded-md border-neutral-300 dark:border-neutral-700 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-neutral-100">
<option value="">-- Pilih Tipe Item --</option>
<!--[if BLOCK]><![endif]--><?php $__currentLoopData = $allTypeItems; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $typeItem): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($typeItem->id); ?>"><?php echo e($typeItem->name); ?></option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?><!--[if ENDBLOCK]><![endif]-->
</select>
<!--[if BLOCK]><![endif]--><?php $__errorArgs = ['type_item_id'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span class="text-red-500 text-xs"><?php echo e($message); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><!--[if ENDBLOCK]><![endif]-->
</div>
<div class="flex justify-end gap-2">
<button type="button" wire:click="closeModal()"
class="inline-flex justify-center rounded-md border border-neutral-300 dark:border-neutral-700 shadow-sm px-4 py-2 bg-white dark:bg-neutral-700 text-base font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Batal
</button>
<button type="submit"
class="inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm">
Simpan
</button>
</div>
</form>
</div>
</div>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
<!--[if BLOCK]><![endif]--><?php if($isDeleteModalOpen): ?>
<div class="fixed inset-0 bg-gray-600 bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-xl p-6 w-full max-w-sm mx-auto">
<h3 class="text-lg leading-6 font-medium text-neutral-900 dark:text-neutral-100 mb-4">
Konfirmasi Hapus
</h3>
<p class="text-neutral-600 dark:text-neutral-300 mb-6">
Apakah Anda yakin ingin menghapus item ini? Tindakan ini tidak dapat dibatalkan.
</p>
<div class="flex justify-end gap-2">
<button type="button" wire:click="closeDeleteModal()"
class="inline-flex justify-center rounded-md border border-neutral-300 dark:border-neutral-700 shadow-sm px-4 py-2 bg-white dark:bg-neutral-700 text-base font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Batal
</button>
<button type="button" wire:click="delete()"
class="inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
Hapus
</button>
</div>
</div>
</div>
<?php endif; ?><!--[if ENDBLOCK]><![endif]-->
</div>
<?php /**PATH E:\!PROJECT\dfood-website\resources\views/livewire/item-table.blade.php ENDPATH**/ ?>

File diff suppressed because it is too large Load Diff