refactor(outlet): mengubah cara menampilkan data dari menggunakan datatable menjadi foreach card
-kasih pengecekan isset() ke form WithCloseModal -menambahkan form atribute -menghapus dispacth datatable, karena sudah tidak perlu -kasih error untuk input fasilitas
This commit is contained in:
parent
db3d9edb18
commit
77bce6f0eb
@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable\Studio\Master;
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Models\Outlet;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
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;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Filters\MultiSelectFilter;
|
||||
|
||||
class OutletsTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Outlet::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Nama', 'name')->searchable(),
|
||||
|
||||
Column::make('Nomor Telepon', 'phone_number')->searchable(),
|
||||
|
||||
Column::make('Alamat')
|
||||
->label(function ($row) {
|
||||
return <<<HTML
|
||||
<div>
|
||||
<div>{$row->landmark}</div>
|
||||
<div>{$row->address}</div>
|
||||
</div>
|
||||
HTML;
|
||||
})
|
||||
->searchable(function ($query, $searchTerm) {
|
||||
$query->orWhere('landmark', 'like', "%{$searchTerm}%")
|
||||
->orWhere('address', 'like', "%{$searchTerm}%");
|
||||
})
|
||||
->html(),
|
||||
|
||||
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) => "<span class='flex items-center gap-2 py-1 text-xs'>
|
||||
<span class='font-semibold'>".Str::ucfirst($index).': </span>
|
||||
<span class="text-gray-600 dark:text-gray-400">'.(
|
||||
($value['open'] === null && $value['close'] === null)
|
||||
? 'Tutup'
|
||||
: "{$value['open']} - {$value['close']}"
|
||||
).'</span>
|
||||
</span>'
|
||||
)
|
||||
->flexCol(['class' => 'gap-2 flex-wrap']),
|
||||
|
||||
ArrayColumn::make('Fasilitas')
|
||||
->data(fn ($value, $row) => $row->facilities->pluck('name')->toArray())
|
||||
->outputFormat(fn ($index, $value) => Blade::render('<flux:badge color="zinc" dot>'.$value.'</flux:badge>'))
|
||||
->flexRow(['class' => 'gap-2 flex-wrap']),
|
||||
|
||||
Column::make('Tanggal Operasi')
|
||||
->label(function ($row) {
|
||||
$openedDate = formatDate($row->opened_date);
|
||||
$openedAgo = timeAgo($row->opened_date);
|
||||
|
||||
$closedDate = formatDate($row->closed_date);
|
||||
$closedAgo = timeAgo($row->closed_date);
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-300 gap-0.5">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Buka:</span>
|
||||
<span>{$openedDate}</span>
|
||||
<span class="text-gray-500">({$openedAgo})</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Tutup:</span>
|
||||
<span>{$closedDate}</span>
|
||||
<span class="text-gray-500">({$closedAgo})</span>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Status', 'status')
|
||||
->format(fn ($value) => Blade::render('<flux:badge color="'.$value->color().'" dot>'.$value->label().'</flux:badge>'))
|
||||
->html(),
|
||||
|
||||
Column::make('Aksi')
|
||||
->label(function ($row) {
|
||||
$actions = '';
|
||||
|
||||
if (auth()->user()->can('update outlet')) {
|
||||
$actions .= view('components.datatables.edit', [
|
||||
'id' => $row->hash,
|
||||
'editRoute' => route('studio.master.outlet.edit', $row->hash),
|
||||
])->render();
|
||||
}
|
||||
|
||||
if (auth()->user()->can('delete outlet')) {
|
||||
$actions .= view('components.datatables.delete', [
|
||||
'id' => $row->hash,
|
||||
'deleteRoute' => route('studio.master.outlet.delete', $row->hash),
|
||||
])->render();
|
||||
}
|
||||
|
||||
return $actions;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Outlet::select('id', 'name', 'phone_number', 'landmark', 'address', 'opened_date', 'closed_date', 'status')->with(['openingHours', 'facilities']);
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
return [
|
||||
MultiSelectFilter::make('Status')
|
||||
->options(
|
||||
collect(OutletStatus::cases())
|
||||
->mapWithKeys(fn ($case) => [$case->value => $case->label()])
|
||||
->toArray()
|
||||
)
|
||||
->filter(function (Builder $builder, array $values) {
|
||||
$builder->whereIn('status', $values);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,9 @@ public function validationAttributes(): array
|
||||
'address' => 'alamat',
|
||||
'landmark' => 'patokan',
|
||||
'maps_url' => 'tautan google maps',
|
||||
'opening_hours' => 'jam beroperasi',
|
||||
'opening_hours.*' => 'jam beroperasi',
|
||||
'facilities' => 'fasilitas',
|
||||
'facilities.*' => 'fasilitas',
|
||||
'opened_date' => 'tanggal buka',
|
||||
'featured_image' => 'gambar utama',
|
||||
|
||||
@ -36,8 +36,6 @@ public function save()
|
||||
|
||||
$this->form->store();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
$this->toast('Outlet berhasil ditambahkan.');
|
||||
|
||||
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
||||
|
||||
@ -34,11 +34,9 @@ public function save()
|
||||
|
||||
$this->form->update();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
||||
|
||||
$this->toast('Outlet berhasil diperbarui.');
|
||||
|
||||
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@ -2,17 +2,67 @@
|
||||
|
||||
namespace App\Livewire\Studio\Master\Outlet;
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Models\Outlet;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use App\Traits\WithToast;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Outlet')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithConfirmation, WithToast;
|
||||
use WithCloseModal, WithConfirmation, WithMediaHandler, WithToast;
|
||||
|
||||
public array $outlets = [];
|
||||
|
||||
public ?string $imageUrl = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->outlets = Outlet::with(['openingHours', 'facilities'])
|
||||
->latest()
|
||||
->get()
|
||||
->map(fn ($outlet) => [
|
||||
'hash' => $outlet->hash,
|
||||
'name' => $outlet->name,
|
||||
'phone_number' => $outlet->phone_number,
|
||||
'landmark' => $outlet->landmark,
|
||||
'address' => $outlet->address,
|
||||
'status' => [
|
||||
'label' => $outlet->status->label(),
|
||||
'color' => $outlet->status->color(),
|
||||
],
|
||||
'opening_hours' => $outlet->openingHours->mapWithKeys(fn ($hour) => [
|
||||
Str::ucfirst(Str::lower(Day::from($hour->day->value)->label())) => [
|
||||
'open' => formatTime($hour->open_time),
|
||||
'close' => formatTime($hour->close_time),
|
||||
'is_closed' => $hour->open_time === null && $hour->close_time === null,
|
||||
],
|
||||
])->toArray(),
|
||||
'facilities' => $outlet->facilities->pluck('name')->toArray(),
|
||||
'opened_date' => formatDate($outlet->opened_date),
|
||||
'opened_ago' => timeAgo($outlet->opened_date),
|
||||
'closed_date' => $outlet->closed_date ? formatDate($outlet->closed_date) : null,
|
||||
'closed_ago' => $outlet->closed_date ? timeAgo($outlet->closed_date) : null,
|
||||
'edit_route' => route('studio.master.outlet.edit', $outlet->hash),
|
||||
'delete_route' => route('studio.master.outlet.delete', $outlet->hash),
|
||||
'image' => $this->mapMediaCollection($outlet->getMedia('featured_image'))[0]['temporaryUrl'],
|
||||
'images' => collect($this->mapMediaCollection($outlet->getMedia('images')))->map(fn ($image) => $image['temporaryUrl']),
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function openImage(string $imageUrl)
|
||||
{
|
||||
$this->imageUrl = $imageUrl;
|
||||
|
||||
Flux::modal('image-modal')->show();
|
||||
}
|
||||
|
||||
public function delete(Outlet $outlet)
|
||||
{
|
||||
|
||||
@ -10,7 +10,10 @@ public function closeModal(string $modalName, ?bool $isCloseAll = false)
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->resetValidation();
|
||||
$this->reset('form');
|
||||
|
||||
if (isset($this->form)) {
|
||||
$this->reset('form');
|
||||
}
|
||||
|
||||
if ($isCloseAll) {
|
||||
Flux::modals()->close();
|
||||
|
||||
@ -170,7 +170,26 @@ class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
class="cursor-pointer" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@error("form.facilities.$index")
|
||||
<flux:error name="form.facilities.{{ $index }}" />
|
||||
@enderror
|
||||
@endforeach
|
||||
|
||||
@error('form.facilities')
|
||||
<div role="alert" aria-live="polite" aria-atomic="true"
|
||||
class="mt-3 text-sm font-medium text-red-500 dark:text-red-400" data-flux-error="">
|
||||
<svg class="shrink-0 [:where(&)]:size-5 inline" data-flux-icon=""
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
|
||||
<flux:button variant="primary" size="sm" wire:click="addFacility"
|
||||
class="sm:w-auto cursor-pointer">
|
||||
+ Tambah Fasilitas
|
||||
|
||||
@ -14,8 +14,129 @@ class="text-sm">
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.studio.master.outlets-table />
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach ($outlets as $outlet)
|
||||
<flux:card
|
||||
class="space-y-2 transition-transform duration-300 ease-out hover:-translate-y-1 hover:shadow-lg">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:avatar size="lg" src="{{ $outlet['image'] }}"
|
||||
wire:click="openImage('{{ $outlet['image'] }}')" />
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $outlet['name'] }}</flux:heading>
|
||||
<flux:badge color="{{ $outlet['status']['color'] }}" size="sm">
|
||||
{{ $outlet['status']['label'] }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 mb-4">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<flux:icon.phone class="w-4 h-4 text-gray-500 dark:text-white" />
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ $outlet['phone_number'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2 text-sm">
|
||||
<flux:icon.map-pin class="w-4 h-4 text-gray-500 dark:text-white" />
|
||||
<div class="text-gray-700 dark:text-gray-300">
|
||||
<div class="font-medium">{{ $outlet['landmark'] }}</div>
|
||||
<div class="text-gray-600 dark:text-gray-400">{{ $outlet['address'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<flux:icon.clock class="w-4 h-4" />
|
||||
Jam Operasional
|
||||
</h4>
|
||||
<div class="space-y-1 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
@foreach ($outlet['opening_hours'] as $day => $time)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span
|
||||
class="font-medium text-gray-700 dark:text-gray-300">{{ $day }}:</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{{ $time['is_closed'] ? 'Tutup' : "{$time['open']} - {$time['close']}" }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<flux:icon.wifi class="w-4 h-4" />
|
||||
Fasilitas
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($outlet['facilities'] as $facility)
|
||||
<flux:badge color="zinc" size="sm">{{ $facility }}</flux:badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Buka:</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ $outlet['opened_date'] }}</span>
|
||||
<span class="text-gray-500">({{ $outlet['opened_ago'] }})</span>
|
||||
</div>
|
||||
@if ($outlet['closed_date'])
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Tutup:</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ $outlet['closed_date'] }}</span>
|
||||
<span class="text-gray-500">({{ $outlet['closed_ago'] }})</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<flux:icon.photo class="w-4 h-4" />
|
||||
Gambar
|
||||
</h4>
|
||||
<flux:avatar.group class="mt-6">
|
||||
@foreach ($outlet['images'] as $key => $value)
|
||||
<flux:avatar circle size="lg" src="{{ $value }}"
|
||||
wire:click="openImage('{{ $value }}')" />
|
||||
@endforeach
|
||||
</flux:avatar.group>
|
||||
</div>
|
||||
|
||||
<flux:separator></flux:separator>
|
||||
|
||||
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
|
||||
@if (auth()->user()->can('update outlet'))
|
||||
<flux:tooltip content="Ubah">
|
||||
<flux:button href="{{ $outlet['edit_route'] }}" wire:navigate.hover variant="primary"
|
||||
color="yellow" icon="pencil-square" size="sm"></flux:button>
|
||||
</flux:tooltip>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()->can('delete outlet'))
|
||||
<flux:modal.trigger name="delete">
|
||||
<flux:tooltip content="Hapus">
|
||||
<flux:button variant="danger" icon="trash" size="sm"
|
||||
wire:click="$dispatch('fn:confirmDelete', {id: '{{ $outlet['hash'] }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
|
||||
<flux:modal name="image-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
|
||||
<div class="flex justify-center items-center p-4">
|
||||
<img src="{{ $imageUrl }}" alt="Outlet Image" class="rounded-lg max-h-[80vh] object-contain" />
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
</flux:main>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user