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',
|
'address' => 'alamat',
|
||||||
'landmark' => 'patokan',
|
'landmark' => 'patokan',
|
||||||
'maps_url' => 'tautan google maps',
|
'maps_url' => 'tautan google maps',
|
||||||
|
'opening_hours' => 'jam beroperasi',
|
||||||
'opening_hours.*' => 'jam beroperasi',
|
'opening_hours.*' => 'jam beroperasi',
|
||||||
|
'facilities' => 'fasilitas',
|
||||||
'facilities.*' => 'fasilitas',
|
'facilities.*' => 'fasilitas',
|
||||||
'opened_date' => 'tanggal buka',
|
'opened_date' => 'tanggal buka',
|
||||||
'featured_image' => 'gambar utama',
|
'featured_image' => 'gambar utama',
|
||||||
|
|||||||
@ -36,8 +36,6 @@ public function save()
|
|||||||
|
|
||||||
$this->form->store();
|
$this->form->store();
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
|
||||||
|
|
||||||
$this->toast('Outlet berhasil ditambahkan.');
|
$this->toast('Outlet berhasil ditambahkan.');
|
||||||
|
|
||||||
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
||||||
|
|||||||
@ -34,11 +34,9 @@ public function save()
|
|||||||
|
|
||||||
$this->form->update();
|
$this->form->update();
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
||||||
|
|
||||||
$this->toast('Outlet berhasil diperbarui.');
|
$this->toast('Outlet berhasil diperbarui.');
|
||||||
|
|
||||||
$this->redirectRoute('studio.master.outlet.index', navigate: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|||||||
@ -2,17 +2,67 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Studio\Master\Outlet;
|
namespace App\Livewire\Studio\Master\Outlet;
|
||||||
|
|
||||||
|
use App\Enums\Day;
|
||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
|
use App\Traits\WithCloseModal;
|
||||||
use App\Traits\WithConfirmation;
|
use App\Traits\WithConfirmation;
|
||||||
|
use App\Traits\WithMediaHandler;
|
||||||
use App\Traits\WithToast;
|
use App\Traits\WithToast;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Title('Outlet')]
|
#[Title('Outlet')]
|
||||||
class Index extends Component
|
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)
|
public function delete(Outlet $outlet)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,7 +10,10 @@ public function closeModal(string $modalName, ?bool $isCloseAll = false)
|
|||||||
{
|
{
|
||||||
$this->resetErrorBag();
|
$this->resetErrorBag();
|
||||||
$this->resetValidation();
|
$this->resetValidation();
|
||||||
$this->reset('form');
|
|
||||||
|
if (isset($this->form)) {
|
||||||
|
$this->reset('form');
|
||||||
|
}
|
||||||
|
|
||||||
if ($isCloseAll) {
|
if ($isCloseAll) {
|
||||||
Flux::modals()->close();
|
Flux::modals()->close();
|
||||||
|
|||||||
@ -170,7 +170,26 @@ class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
|||||||
class="cursor-pointer" />
|
class="cursor-pointer" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@error("form.facilities.$index")
|
||||||
|
<flux:error name="form.facilities.{{ $index }}" />
|
||||||
|
@enderror
|
||||||
@endforeach
|
@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"
|
<flux:button variant="primary" size="sm" wire:click="addFacility"
|
||||||
class="sm:w-auto cursor-pointer">
|
class="sm:w-auto cursor-pointer">
|
||||||
+ Tambah Fasilitas
|
+ Tambah Fasilitas
|
||||||
|
|||||||
@ -14,8 +14,129 @@ class="text-sm">
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6">
|
<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>
|
</div>
|
||||||
|
|
||||||
@include('components.confirmation.delete')
|
@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>
|
</flux:main>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user