diff --git a/app/Livewire/Datatable/OutletsTable.php b/app/Livewire/Datatable/OutletsTable.php index 2692250..a3ccae1 100644 --- a/app/Livewire/Datatable/OutletsTable.php +++ b/app/Livewire/Datatable/OutletsTable.php @@ -2,6 +2,7 @@ namespace App\Livewire\Datatable; +use App\Enums\Day; use App\Enums\OutletStatus; use App\Models\Outlet; use App\Traits\Datatable\WithAppendColumn; @@ -9,6 +10,7 @@ use App\Traits\Datatable\WithPrependColumn; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Str; use Rappasoft\LaravelLivewireTables\DataTableComponent; use Rappasoft\LaravelLivewireTables\Views\Column; use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn; @@ -42,22 +44,30 @@ public function columns(): array }) ->html(), - ArrayColumn::make('Jam Buka', 'opening_hours') - ->data(fn ($value, $row) => normalizeOpeningHours($row->opening_hours)) + ArrayColumn::make('Jam Operasioanl') + ->data( + fn ($value, $row) => $row->openingHours + ->mapWithKeys(fn ($hour) => [ + Str::lower(Day::from($hour->day->value)->label()) => [ + 'open' => formatTime($hour->open_time), + 'close' => formatTime($hour->close_time), + ], + ]) + ) ->outputFormat( fn ($index, $value) => " - ".ucfirst($index).': - ' - .(($value['open'] === null && $value['close'] === null) - ? 'Tutup' - : "{$value['open']} - {$value['close']}") - .' - ' + ".Str::ucfirst($index).': + '.( + ($value['open'] === null && $value['close'] === null) + ? 'Tutup' + : "{$value['open']} - {$value['close']}" + ).' + ' ) ->flexCol(['class' => 'gap-2 flex-wrap']), - ArrayColumn::make('Fasilitas', 'facilities') - ->data(fn ($value, $row) => $row->facilities) + ArrayColumn::make('Fasilitas') + ->data(fn ($value, $row) => $row->facilities->pluck('name')->toArray()) ->outputFormat(fn ($index, $value) => Blade::render(''.$value.'')) ->flexRow(['class' => 'gap-2 flex-wrap']), @@ -112,7 +122,7 @@ public function columns(): array public function builder(): Builder { - return Outlet::select('id', 'name', 'phone_number', 'landmark', 'address', 'opening_hours', 'facilities', 'opened_date', 'closed_date', 'status'); + return Outlet::select('id', 'name', 'phone_number', 'landmark', 'address', 'opened_date', 'closed_date', 'status')->with(['openingHours', 'facilities']); } public function filters(): array diff --git a/app/Livewire/Forms/OutletForm.php b/app/Livewire/Forms/OutletForm.php index 420d649..75032e3 100644 --- a/app/Livewire/Forms/OutletForm.php +++ b/app/Livewire/Forms/OutletForm.php @@ -2,7 +2,9 @@ namespace App\Livewire\Forms; +use App\Enums\Day; use App\Enums\OutletStatus; +use App\Models\OpeningHour; use App\Models\Outlet; use App\Rules\PhoneNumber; use App\Traits\WithMediaHandler; @@ -26,21 +28,13 @@ class OutletForm extends Form public ?string $maps_url = null; - public array $opening_hours = [ - 'senin' => ['open' => null, 'close' => null], - 'selasa' => ['open' => null, 'close' => null], - 'rabu' => ['open' => null, 'close' => null], - 'kamis' => ['open' => null, 'close' => null], - 'jumat' => ['open' => null, 'close' => null], - 'sabtu' => ['open' => null, 'close' => null], - 'minggu' => ['open' => null, 'close' => null], - ]; + public array $opening_hours = []; - public array $facilities = ['']; + public array $facilities = ['Parkir']; public string $opened_date = ''; - public ?string $status = null; + public string $status = '1'; public array $featured_image = []; @@ -48,13 +42,11 @@ class OutletForm extends Form public function rules(): array { - $days = ['senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu', 'minggu']; - $rules = [ 'name' => ['required', 'string', 'max:100'], 'phone_number' => ['required', 'string', new PhoneNumber], 'address' => ['required', 'string'], - 'landmark' => ['required', 'string', 'max:40'], + 'landmark' => ['required', 'string', 'max:20'], 'maps_url' => [ 'nullable', 'string', @@ -69,10 +61,10 @@ public function rules(): array 'images' => ['required', 'array', 'max:5'], ]; - foreach ($days as $day) { - $rules["opening_hours.$day"] = ['required', 'array']; - $rules["opening_hours.$day.open"] = ['nullable', 'date_format:H:i']; - $rules["opening_hours.$day.close"] = ['nullable', 'date_format:H:i']; + foreach (Day::cases() as $day) { + $rules["opening_hours.{$day->value}"] = ['required', 'array']; + $rules["opening_hours.{$day->value}.open_time"] = ['nullable', 'date_format:H:i']; + $rules["opening_hours.{$day->value}.close_time"] = ['nullable', 'date_format:H:i']; } return $rules; @@ -96,6 +88,8 @@ public function validationAttributes(): array public function setOutlet(Outlet $outlet) { + $outlet = $outlet->load(['openingHours', 'facilities']); + $this->outlet = $outlet; $this->name = $outlet->name; @@ -103,11 +97,17 @@ public function setOutlet(Outlet $outlet) $this->address = $outlet->address; $this->landmark = $outlet->landmark; $this->maps_url = $outlet->maps_url; - $this->opening_hours = $outlet->opening_hours; - $this->facilities = $outlet->facilities; + $this->opening_hours = $outlet->openingHours + ->mapWithKeys(fn ($item) => [ + $item->day->value => [ + 'open_time' => formatTime($item->open_time), + 'close_time' => formatTime($item->close_time), + ], + ]) + ->toArray(); + $this->facilities = $outlet->facilities->pluck('name')->toArray(); $this->opened_date = $outlet->opened_date; $this->status = $outlet->status->value; - $this->featured_image = $this->mapMediaCollection($outlet->getMedia('featured_image')); $this->images = $this->mapMediaCollection($outlet->getMedia('images')); } @@ -127,6 +127,19 @@ public function store() DB::transaction(function () use ($data) { $outlet = Outlet::create($data); + $openingHours = collect($data['opening_hours']) + ->map(fn ($time, $day) => [ + 'outlet_id' => $outlet->id, + 'day' => $day, + 'open_time' => $time['open_time'], + 'close_time' => $time['close_time'], + ]) + ->values() + ->toArray(); + + OpeningHour::insert($openingHours); + $outlet->facilities()->createMany(collect($data['facilities'])->map(fn ($facility) => ['name' => $facility])->toArray()); + $this->uploadMedia($this->featured_image, $outlet, 'featured_image'); $this->uploadMedia($this->images, $outlet, 'images'); }); @@ -138,9 +151,6 @@ public function update() $data = $this->all(); - $this->syncMedia($data['featured_image'], $this->outlet, 'featured_image'); - $this->syncMedia($data['images'], $this->outlet, 'images'); - if ($data['status'] == OutletStatus::TERMINATED->value) { $data['closed_date'] = now()->toDateString(); } else { @@ -150,8 +160,30 @@ public function update() DB::transaction(function () use ($data) { $this->outlet->update($data); - $this->uploadMedia($this->featured_image, $this->outlet, 'featured_image'); - $this->uploadMedia($this->images, $this->outlet, 'images'); + foreach ($data['opening_hours'] as $day => $hours) { + OpeningHour::updateOrCreate( + [ + 'outlet_id' => $this->outlet->id, + 'day' => $day, + ], + [ + 'open_time' => $hours['open_time'] ?? null, + 'close_time' => $hours['close_time'] ?? null, + ] + ); + } + + $facilities = collect($data['facilities']); + + foreach ($facilities as $facilityName) { + $this->outlet->facilities()->updateOrCreate( + ['name' => $facilityName], + ['name' => $facilityName] + ); + } + + $this->syncMedia($data['featured_image'], $this->outlet, 'featured_image'); + $this->syncMedia($data['images'], $this->outlet, 'images'); }); } } diff --git a/app/Livewire/Studio/Master/Outlet/Create.php b/app/Livewire/Studio/Master/Outlet/Create.php index df6dd87..b7b0859 100644 --- a/app/Livewire/Studio/Master/Outlet/Create.php +++ b/app/Livewire/Studio/Master/Outlet/Create.php @@ -2,6 +2,7 @@ namespace App\Livewire\Studio\Master\Outlet; +use App\Enums\Day; use App\Livewire\Forms\OutletForm; use App\Traits\Outlet\WithFacilityHandler; use App\Traits\WithUpdatedData; @@ -16,10 +17,24 @@ class Create extends Component public OutletForm $form; + public array $days = []; + + public function mount() + { + $this->form->opening_hours = collect(Day::cases()) + ->mapWithKeys(fn (Day $day) => [ + $day->value => ['open_time' => null, 'close_time' => null], + ]) + ->toArray(); + $this->days = Day::cases(); + } + public function save() { $this->form->store(); + $this->dispatch('refreshDatatable'); + Flux::toast( heading: 'Berhasil', text: 'Outlet berhasil ditambahkan.', diff --git a/app/Livewire/Studio/Master/Outlet/Edit.php b/app/Livewire/Studio/Master/Outlet/Edit.php index f976838..2fbda53 100644 --- a/app/Livewire/Studio/Master/Outlet/Edit.php +++ b/app/Livewire/Studio/Master/Outlet/Edit.php @@ -2,6 +2,7 @@ namespace App\Livewire\Studio\Master\Outlet; +use App\Enums\Day; use App\Livewire\Forms\OutletForm; use App\Models\Outlet; use App\Traits\Outlet\WithFacilityHandler; @@ -17,9 +18,13 @@ class Edit extends Component public OutletForm $form; + public array $days = []; + public function mount(Outlet $outlet) { $this->form->setOutlet($outlet); + + $this->days = Day::cases(); } public function save() diff --git a/app/Models/Facility.php b/app/Models/Facility.php new file mode 100644 index 0000000..ea46811 --- /dev/null +++ b/app/Models/Facility.php @@ -0,0 +1,20 @@ +belongsTo(Outlet::class); + } +} diff --git a/app/Models/OpeningHour.php b/app/Models/OpeningHour.php new file mode 100644 index 0000000..d61ad15 --- /dev/null +++ b/app/Models/OpeningHour.php @@ -0,0 +1,28 @@ + Day::class, + ]; + } + + public function outlet(): BelongsTo + { + return $this->belongsTo(Outlet::class); + } +} diff --git a/app/Models/Outlet.php b/app/Models/Outlet.php index 3abf158..a2a4a83 100644 --- a/app/Models/Outlet.php +++ b/app/Models/Outlet.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; @@ -21,8 +22,6 @@ class Outlet extends Model implements HasMedia protected function casts(): array { return [ - 'opening_hours' => 'array', - 'facilities' => 'array', 'status' => OutletStatus::class, ]; } @@ -38,4 +37,14 @@ public function users(): BelongsToMany { return $this->belongsToMany(User::class, 'user_outlet')->withPivot('position_id'); } + + public function openingHours(): HasMany + { + return $this->hasMany(OpeningHour::class); + } + + public function facilities(): HasMany + { + return $this->hasMany(Facility::class); + } } diff --git a/database/migrations/2025_09_13_045752_create_outlets_table.php b/database/migrations/2025_09_13_045752_create_outlets_table.php index d573dea..00bd217 100644 --- a/database/migrations/2025_09_13_045752_create_outlets_table.php +++ b/database/migrations/2025_09_13_045752_create_outlets_table.php @@ -20,8 +20,6 @@ public function up(): void $table->text('address'); $table->string('landmark', 20); $table->text('maps_url')->nullable(); - $table->json('opening_hours'); - $table->json('facilities'); $table->enum('status', [OutletStatus::values()])->default(OutletStatus::OPERATIONAL)->comment(OutletStatus::comment()); $table->date('opened_date'); $table->date('closed_date')->nullable(); diff --git a/database/migrations/2025_09_24_130401_create_opening_hours_table.php b/database/migrations/2025_09_24_130401_create_opening_hours_table.php new file mode 100644 index 0000000..2a9c390 --- /dev/null +++ b/database/migrations/2025_09_24_130401_create_opening_hours_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('outlet_id')->constrained()->cascadeOnDelete(); + $table->enum('day', Day::values())->comment(Day::comment()); + $table->time('open_time')->nullable(); + $table->time('close_time')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('opening_hours'); + } +}; diff --git a/database/migrations/2025_09_24_130406_create_facilities_table.php b/database/migrations/2025_09_24_130406_create_facilities_table.php new file mode 100644 index 0000000..04439df --- /dev/null +++ b/database/migrations/2025_09_24_130406_create_facilities_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('outlet_id')->constrained()->cascadeOnDelete(); + $table->string('name', 30); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('facilities'); + } +}; diff --git a/resources/views/livewire/studio/master/outlet/form.blade.php b/resources/views/livewire/studio/master/outlet/form.blade.php index be5df31..7266580 100644 --- a/resources/views/livewire/studio/master/outlet/form.blade.php +++ b/resources/views/livewire/studio/master/outlet/form.blade.php @@ -19,27 +19,26 @@
+ wire:model.live.debounce.500ms="form.name" autofocus autocomplete="off" /> + autocomplete="off" />
+ wire:model.live.debounce.500ms="form.landmark" autocomplete="off" /> + wire:model.live.debounce.500ms="form.maps_url" autocomplete="off" />

Jam Operasional

- @foreach (['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'] as $day) -
- {{ $day }} + @foreach ($days as $day) +
+ {{ $day->label() }} + wire:model.live.debounce.500ms="form.opening_hours.{{ $day->value }}.open_time" /> + + wire:model.live.debounce.500ms="form.opening_hours.{{ $day->value }}.close_time" />
@endforeach
@@ -124,7 +124,7 @@ class="mt-3 text-sm font-medium text-red-500 dark:text-red-400"> @foreach ($form->facilities as $index => $facility)
+ placeholder="Masukkan fasilitas" class="flex-1" autocomplete="off" /> @if ($index != 0)