refactor(outlet): menjadikan kode nya clean code
- menyesuaikan testing - menghapus route untuk delete karena tidak lewat route - menyesuaikan tipe prop dan lainnya
This commit is contained in:
parent
23a2bd580c
commit
977b191ccb
@ -28,21 +28,30 @@ class OutletForm extends Form
|
||||
|
||||
public ?string $maps_url = null;
|
||||
|
||||
/** @var array<string, array{open_time: ?string, close_time: ?string}> */
|
||||
public array $opening_hours = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $facilities = ['Parkir'];
|
||||
|
||||
public string $opened_date = '';
|
||||
|
||||
public string $status = '1';
|
||||
public int $status = OutletStatus::OPERATIONAL->value;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $featured_image = [];
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $images = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
return array_merge($this->getBasicRules(), $this->getOpeningHoursRules());
|
||||
}
|
||||
|
||||
private function getBasicRules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'phone_number' => ['required', 'string', new PhoneNumber],
|
||||
'address' => ['required', 'string'],
|
||||
@ -60,6 +69,11 @@ public function rules(): array
|
||||
'featured_image' => ['required', 'array', 'max:1'],
|
||||
'images' => ['required', 'array', 'max:5'],
|
||||
];
|
||||
}
|
||||
|
||||
private function getOpeningHoursRules(): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
foreach (Day::cases() as $day) {
|
||||
$rules["opening_hours.{$day->value}"] = ['required', 'array'];
|
||||
@ -88,18 +102,33 @@ public function validationAttributes(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function setOutlet(Outlet $outlet)
|
||||
public function setOutlet(Outlet $outlet): void
|
||||
{
|
||||
$outlet = $outlet->load(['openingHours', 'facilities']);
|
||||
$outlet->load(['openingHours', 'facilities']);
|
||||
|
||||
$this->outlet = $outlet;
|
||||
|
||||
$this->fillBasicAttributes($outlet);
|
||||
$this->opening_hours = $this->formatOpeningHoursForForm($outlet);
|
||||
$this->facilities = $outlet->facilities->pluck('name')->toArray();
|
||||
$this->featured_image = $this->mapMediaCollection($outlet->getMedia('featured_image'));
|
||||
$this->images = $this->mapMediaCollection($outlet->getMedia('images'));
|
||||
}
|
||||
|
||||
private function fillBasicAttributes(Outlet $outlet): void
|
||||
{
|
||||
$this->name = $outlet->name;
|
||||
$this->phone_number = $outlet->phone_number;
|
||||
$this->address = $outlet->address;
|
||||
$this->landmark = $outlet->landmark;
|
||||
$this->maps_url = $outlet->maps_url;
|
||||
$this->opening_hours = $outlet->openingHours
|
||||
$this->opened_date = $outlet->opened_date;
|
||||
$this->status = $outlet->status->value;
|
||||
}
|
||||
|
||||
private function formatOpeningHoursForForm(Outlet $outlet): array
|
||||
{
|
||||
return $outlet->openingHours
|
||||
->mapWithKeys(fn ($item) => [
|
||||
$item->day->value => [
|
||||
'open_time' => $item->open_time ? formatTime($item->open_time) : null,
|
||||
@ -107,88 +136,121 @@ public function setOutlet(Outlet $outlet)
|
||||
],
|
||||
])
|
||||
->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'));
|
||||
}
|
||||
|
||||
public function store()
|
||||
public function store(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$data = $this->all();
|
||||
|
||||
if ($data['status'] == OutletStatus::TERMINATED) {
|
||||
$data['closed_date'] = now();
|
||||
} else {
|
||||
$data['closed_date'] = null;
|
||||
}
|
||||
$data = $this->prepareDataForSave();
|
||||
|
||||
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');
|
||||
$this->createOpeningHours($outlet, $data['opening_hours']);
|
||||
$this->createFacilities($outlet, $data['facilities']);
|
||||
$this->handleMediaUpload($outlet);
|
||||
});
|
||||
}
|
||||
|
||||
public function update()
|
||||
private function prepareDataForSave(): array
|
||||
{
|
||||
$data = $this->all();
|
||||
|
||||
$data['closed_date'] = $data['status'] == OutletStatus::TERMINATED->value
|
||||
? now()
|
||||
: null;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function createOpeningHours(Outlet $outlet, array $openingHours): void
|
||||
{
|
||||
$hoursData = collect($openingHours)
|
||||
->map(fn ($time, $day) => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'day' => $day,
|
||||
'open_time' => $time['open_time'],
|
||||
'close_time' => $time['close_time'],
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
OpeningHour::insert($hoursData);
|
||||
}
|
||||
|
||||
private function createFacilities(Outlet $outlet, array $facilities): void
|
||||
{
|
||||
$facilitiesData = collect($facilities)
|
||||
->map(fn ($facility) => ['name' => $facility])
|
||||
->toArray();
|
||||
|
||||
$outlet->facilities()->createMany($facilitiesData);
|
||||
}
|
||||
|
||||
private function handleMediaUpload(Outlet $outlet): void
|
||||
{
|
||||
$this->uploadMedia($this->featured_image, $outlet, 'featured_image');
|
||||
$this->uploadMedia($this->images, $outlet, 'images');
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$data = $this->all();
|
||||
|
||||
if ($data['status'] == OutletStatus::TERMINATED->value) {
|
||||
$data['closed_date'] = now()->toDateString();
|
||||
} else {
|
||||
$data['closed_date'] = null;
|
||||
}
|
||||
$data = $this->prepareDataForSave();
|
||||
|
||||
DB::transaction(function () use ($data) {
|
||||
$this->outlet->update($data);
|
||||
|
||||
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');
|
||||
|
||||
$this->uploadMedia($this->featured_image, $this->outlet, 'featured_image');
|
||||
$this->uploadMedia($this->images, $this->outlet, 'images');
|
||||
$this->updateOpeningHours($data['opening_hours']);
|
||||
$this->updateFacilities($data['facilities']);
|
||||
$this->handleMediaSync($data);
|
||||
$this->handleMediaUpload($this->outlet);
|
||||
});
|
||||
}
|
||||
|
||||
private function updateOpeningHours(array $openingHours): void
|
||||
{
|
||||
// Delete opening hours for days that are not in the new data
|
||||
$this->outlet->openingHours()
|
||||
->whereNotIn('day', array_keys($openingHours))
|
||||
->delete();
|
||||
|
||||
// Add or update opening hours
|
||||
foreach ($openingHours as $day => $hours) {
|
||||
OpeningHour::updateOrCreate(
|
||||
[
|
||||
'outlet_id' => $this->outlet->id,
|
||||
'day' => $day,
|
||||
],
|
||||
[
|
||||
'open_time' => $hours['open_time'] ?? null,
|
||||
'close_time' => $hours['close_time'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateFacilities(array $facilities): void
|
||||
{
|
||||
// Delete facilities that are not in the new list
|
||||
$this->outlet->facilities()
|
||||
->whereNotIn('name', $facilities)
|
||||
->delete();
|
||||
|
||||
// Add or update facilities
|
||||
foreach ($facilities as $facilityName) {
|
||||
$this->outlet->facilities()->updateOrCreate(
|
||||
['name' => $facilityName],
|
||||
['name' => $facilityName]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleMediaSync(array $data): void
|
||||
{
|
||||
$this->syncMedia($data['featured_image'], $this->outlet, 'featured_image');
|
||||
$this->syncMedia($data['images'], $this->outlet, 'images');
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,30 +7,35 @@
|
||||
use App\Traits\Outlet\WithFacilityHandler;
|
||||
use App\Traits\WithAuthorization;
|
||||
use App\Traits\WithToast;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Tambah Outlet')]
|
||||
class Create extends Component
|
||||
{
|
||||
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
|
||||
use WithAuthorization, WithFacilityHandler, WithToast;
|
||||
|
||||
public OutletForm $form;
|
||||
|
||||
/** @var array<int, Day> */
|
||||
public array $days = [];
|
||||
|
||||
public function mount()
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->opening_hours = collect(Day::cases())
|
||||
->mapWithKeys(fn (Day $day) => [
|
||||
$day->value => ['open_time' => null, 'close_time' => null],
|
||||
$day->value => [
|
||||
'open_time' => null,
|
||||
'close_time' => null,
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
|
||||
$this->days = Day::cases();
|
||||
}
|
||||
|
||||
public function save()
|
||||
public function save(): void
|
||||
{
|
||||
$this->canOrAbort('create outlet');
|
||||
|
||||
@ -41,7 +46,7 @@ public function save()
|
||||
$this->redirectRoute('studio.master.outlet.index');
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.master.outlet.form', [
|
||||
'pageTitle' => 'Tambah Outlet',
|
||||
|
||||
@ -8,27 +8,27 @@
|
||||
use App\Traits\Outlet\WithFacilityHandler;
|
||||
use App\Traits\WithAuthorization;
|
||||
use App\Traits\WithToast;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Ubah Outlet')]
|
||||
class Edit extends Component
|
||||
{
|
||||
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
|
||||
use WithAuthorization, WithFacilityHandler, WithToast;
|
||||
|
||||
public OutletForm $form;
|
||||
|
||||
/** @var array<int, Day> */
|
||||
public array $days = [];
|
||||
|
||||
public function mount(Outlet $outlet)
|
||||
public function mount(Outlet $outlet): void
|
||||
{
|
||||
$this->form->setOutlet($outlet);
|
||||
|
||||
$this->days = Day::cases();
|
||||
}
|
||||
|
||||
public function save()
|
||||
public function save(): void
|
||||
{
|
||||
$this->canOrAbort('update outlet');
|
||||
|
||||
@ -39,7 +39,7 @@ public function save()
|
||||
$this->redirectRoute('studio.master.outlet.index');
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.master.outlet.form', [
|
||||
'pageTitle' => 'Ubah Outlet',
|
||||
|
||||
@ -4,98 +4,92 @@
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Models\Outlet;
|
||||
use App\Traits\Notification\WithSubscribeNotification;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use App\Traits\WithToast;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Outlet')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithCloseModal, WithConfirmation, WithMediaHandler, WithSubscribeNotification, WithToast;
|
||||
use WithCloseModal, WithConfirmation, WithMediaHandler, WithToast;
|
||||
|
||||
public array $outlets = [];
|
||||
|
||||
public ?string $imageUrl = null;
|
||||
/** @var Collection<int, Outlet> */
|
||||
public Collection $outlets;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public array $status = [];
|
||||
|
||||
public function mount()
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadOutlets();
|
||||
}
|
||||
|
||||
protected function loadOutlets()
|
||||
protected function loadOutlets(): void
|
||||
{
|
||||
$this->outlets = Outlet::with(['openingHours', 'facilities'])
|
||||
->when(! empty($this->search), fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||
->when(! empty($this->status), fn ($query) => $query->whereIn('status', $this->status))
|
||||
->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' => formatDate($outlet->closed_date),
|
||||
'closed_ago' => timeAgo($outlet->closed_date),
|
||||
'image' => data_get($this->mapMediaCollection($outlet->getMedia('featured_image'))[0] ?? null, 'temporaryUrl', asset('assets/images/logo.png')),
|
||||
'images' => collect($this->mapMediaCollection($outlet->getMedia('images')))->pluck('temporaryUrl')->filter()->values(),
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
protected function formatOpeningHours(Outlet $outlet): array
|
||||
{
|
||||
return $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();
|
||||
}
|
||||
|
||||
protected function getFeaturedImage(Outlet $outlet): string
|
||||
{
|
||||
$mediaCollection = $this->mapMediaCollection($outlet->getMedia('featured_image'));
|
||||
|
||||
return data_get($mediaCollection[0] ?? null, 'temporaryUrl', asset('assets/images/logo.png'));
|
||||
}
|
||||
|
||||
protected function getGalleryImages(Outlet $outlet): array
|
||||
{
|
||||
return collect($this->mapMediaCollection($outlet->getMedia('images')))
|
||||
->pluck('temporaryUrl')
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function updatedSearch(string $value)
|
||||
public function updatedSearch(string $value): void
|
||||
{
|
||||
$this->search = $value;
|
||||
|
||||
$this->loadOutlets();
|
||||
}
|
||||
|
||||
public function updatedStatus()
|
||||
public function updatedStatus(): void
|
||||
{
|
||||
$this->loadOutlets();
|
||||
}
|
||||
|
||||
public function openImage(string $imageUrl)
|
||||
public function delete(Outlet $outlet): void
|
||||
{
|
||||
$this->imageUrl = $imageUrl;
|
||||
$this->authorize('delete outlet');
|
||||
|
||||
Flux::modal('image-modal')->show();
|
||||
}
|
||||
|
||||
public function delete(Outlet $outlet)
|
||||
{
|
||||
$outlet->delete();
|
||||
|
||||
$this->toast('Outlet berhasil dihapus.');
|
||||
|
||||
$this->loadOutlets();
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.master.outlet.index', [
|
||||
'pageTitle' => 'Outlet',
|
||||
|
||||
@ -34,14 +34,13 @@ class="text-sm">
|
||||
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">
|
||||
<img src="{{ $outlet['image'] }}" alt="{{ $outlet['image'] }}"
|
||||
class="w-12 h-12 rounded-full cursor-pointer"
|
||||
wire:click="openImage('{{ $outlet['image'] }}')">
|
||||
<img src="{{ $this->getFeaturedImage($outlet) }}" alt="{{ $outlet->name }}"
|
||||
class="w-12 h-12 rounded-full">
|
||||
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $outlet['name'] }}</flux:heading>
|
||||
<flux:badge color="{{ $outlet['status']['color'] }}" size="sm">
|
||||
{{ $outlet['status']['label'] }}
|
||||
<flux:heading size="lg">{{ $outlet->name }}</flux:heading>
|
||||
<flux:badge color="{{ $outlet->status->color() }}" size="sm">
|
||||
{{ $outlet->status->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,14 +49,14 @@ class="w-12 h-12 rounded-full cursor-pointer"
|
||||
<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>
|
||||
<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 class="font-medium">{{ $outlet->landmark }}</div>
|
||||
<div class="text-gray-600 dark:text-gray-400">{{ $outlet->address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,7 +68,7 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
|
||||
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)
|
||||
@foreach ($this->formatOpeningHours($outlet) as $day => $time)
|
||||
<div class="flex justify-between text-xs">
|
||||
<span
|
||||
class="font-medium text-gray-700 dark:text-gray-300">{{ $day }}:</span>
|
||||
@ -88,8 +87,8 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
|
||||
Fasilitas
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($outlet['facilities'] as $facility)
|
||||
<flux:badge color="zinc" size="sm">{{ $facility }}</flux:badge>
|
||||
@foreach ($outlet->facilities as $facility)
|
||||
<flux:badge color="zinc" size="sm">{{ $facility->name }}</flux:badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@ -98,21 +97,22 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
|
||||
<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>
|
||||
<span
|
||||
class="text-gray-700 dark:text-gray-300">{{ formatDate($outlet->opened_date) }}</span>
|
||||
<span class="text-gray-500">({{ timeAgo($outlet->opened_date) }})</span>
|
||||
</div>
|
||||
@if ($outlet['closed_date'])
|
||||
@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>
|
||||
class="text-gray-700 dark:text-gray-300">{{ formatDate($outlet->closed_date) }}</span>
|
||||
<span class="text-gray-500">({{ timeAgo($outlet->closed_date) }})</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($outlet['images']) > 0)
|
||||
@if ($this->getGalleryImages($outlet))
|
||||
<div class="mb-4">
|
||||
<h4
|
||||
class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
@ -120,10 +120,9 @@ class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-cente
|
||||
Gambar
|
||||
</h4>
|
||||
<div class="flex mt-6">
|
||||
@foreach ($outlet['images'] as $key => $value)
|
||||
<img src="{{ $value }}" alt="{{ $value }}"
|
||||
class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
||||
wire:click="openImage('{{ $value }}')">
|
||||
@foreach ($this->getGalleryImages($outlet) as $imageUrl)
|
||||
<img src="{{ $imageUrl }}" alt="{{ $outlet->name }}"
|
||||
class="w-12 h-12 rounded-full -ml-3 first:ml-0">
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +133,7 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
||||
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
|
||||
@can('update outlet')
|
||||
<flux:tooltip content="Ubah">
|
||||
<flux:button href="{{ route('studio.master.outlet.edit', $outlet['hash']) }}"
|
||||
<flux:button href="{{ route('studio.master.outlet.edit', $outlet->hash) }}"
|
||||
wire:navigate.hover variant="primary" color="yellow" icon="pencil-square"
|
||||
size="sm">
|
||||
</flux:button>
|
||||
@ -142,10 +141,10 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
||||
@endcan
|
||||
|
||||
@can('delete outlet')
|
||||
<flux:modal.trigger name="delete">
|
||||
<flux:modal.trigger name="delete-confirmation">
|
||||
<flux:tooltip content="Hapus">
|
||||
<flux:button variant="danger" icon="trash" size="sm"
|
||||
wire:click="$dispatch('fn:confirmAction', {id: '{{ $outlet['hash'] }}'})">
|
||||
wire:click="$dispatch('fn:confirmAction', {id: '{{ $outlet->hash }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@ -156,19 +155,18 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
@include('components.lottie.not-found')
|
||||
@include('components.animations.lottie.not-found')
|
||||
@endif
|
||||
|
||||
</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>
|
||||
|
||||
@include('components.modals.confirmation', [
|
||||
'modalName' => 'delete-confirmation',
|
||||
'modalTitle' => 'Apakah Anda yakin?',
|
||||
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
|
||||
'buttonVariant' => 'primary',
|
||||
'buttonColor' => 'danger',
|
||||
'buttonText' => 'Ya, Hapus',
|
||||
])
|
||||
</flux:main>
|
||||
|
||||
@assets
|
||||
|
||||
@ -66,7 +66,6 @@
|
||||
Route::get('/', OutletIndex::class)->name('index')->middleware('can:view outlet');
|
||||
Route::get('/create', OutletCreate::class)->name('create')->middleware('can:create outlet');
|
||||
Route::get('/{outlet}/edit', OutletEdit::class)->name('edit')->middleware('can:update outlet');
|
||||
Route::delete('/{outlet}/delete', OutletCreate::class)->name('delete')->middleware('can:delete outlet');
|
||||
});
|
||||
|
||||
Route::prefix('users')->name('user.')->group(function () {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Livewire\Studio\Master\Outlet\Create;
|
||||
use App\Livewire\Studio\Master\Outlet\Index;
|
||||
use App\Models\Employee;
|
||||
@ -8,48 +11,19 @@
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
||||
->map(fn ($role) => Role::create(['name' => $role]));
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
// ->has(Outlet::factory()->count(5))
|
||||
->create();
|
||||
|
||||
$this->user->roles()->attach($roles->pluck('id'));
|
||||
});
|
||||
|
||||
function makeLivewireFile(string $name)
|
||||
{
|
||||
$file = UploadedFile::fake()->image($name, 100, 100);
|
||||
$tmpId = Str::random(32);
|
||||
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
|
||||
$path = "livewire-tmp/{$filename}";
|
||||
|
||||
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
|
||||
|
||||
return [
|
||||
'tmpFilename' => $filename,
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'extension' => $file->getClientOriginalExtension(),
|
||||
'path' => storage_path("app/private/{$path}"),
|
||||
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
|
||||
'size' => $file->getSize(),
|
||||
];
|
||||
}
|
||||
|
||||
function mountCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Create::class);
|
||||
@ -156,8 +130,6 @@ function mountCreateComponent(User $user)
|
||||
'form.phone_number',
|
||||
'form.address',
|
||||
'form.landmark',
|
||||
'form.opening_hours',
|
||||
'form.facilities',
|
||||
'form.opened_date',
|
||||
'form.featured_image',
|
||||
'form.images',
|
||||
@ -220,6 +192,17 @@ function mountCreateComponent(User $user)
|
||||
expect($component->form->facilities)->toBe(['Parkir']);
|
||||
});
|
||||
|
||||
it('initializes opening hours for all days of week', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
foreach (Day::cases() as $day) {
|
||||
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
|
||||
expect($component->form->opening_hours[$day->value])->toHaveKeys(['open_time', 'close_time']);
|
||||
expect($component->form->opening_hours[$day->value]['open_time'])->toBeNull();
|
||||
expect($component->form->opening_hours[$day->value]['close_time'])->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('creates outlet with opening hours for all days', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
@ -253,4 +236,190 @@ function mountCreateComponent(User $user)
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->openingHours)->toHaveCount(count(Day::cases()));
|
||||
|
||||
// Verify opening hours data
|
||||
foreach ($createdOutlet->openingHours as $openingHour) {
|
||||
expect($openingHour->open_time)->toBe('09:00');
|
||||
expect($openingHour->close_time)->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('creates outlet with facilities', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
$facilities = ['Parkir', 'WiFi', 'AC'];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', $outlet['status']->value)
|
||||
->set('form.facilities', $facilities)
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->facilities->pluck('name')->toArray())->toBe($facilities);
|
||||
});
|
||||
|
||||
it('sets closed_date when status is TERMINATED', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', OutletStatus::TERMINATED->value)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->status->value)->toBe(OutletStatus::TERMINATED->value);
|
||||
expect($createdOutlet->closed_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('does not set closed_date for non-terminated status', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', OutletStatus::OPERATIONAL->value)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->closed_date)->toBeNull();
|
||||
});
|
||||
|
||||
it('can add and remove facilities dynamically', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
// Initial state
|
||||
expect($component->form->facilities)->toBe(['Parkir']);
|
||||
|
||||
// Add facility
|
||||
$component->call('addFacility');
|
||||
expect($component->form->facilities)->toBe(['Parkir', '']);
|
||||
|
||||
// Add another facility
|
||||
$component->call('addFacility');
|
||||
expect($component->form->facilities)->toBe(['Parkir', '', '']);
|
||||
|
||||
// Remove middle facility
|
||||
$component->call('removeFacility', 1);
|
||||
expect($component->form->facilities)->toBe(['Parkir', '']);
|
||||
|
||||
// Remove first facility (should keep at least one empty)
|
||||
$component->call('removeFacility', 0);
|
||||
expect($component->form->facilities)->toBe(['']);
|
||||
});
|
||||
|
||||
it('validates maps_url regex pattern', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.maps_url', 'invalid-url')
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.maps_url']);
|
||||
|
||||
// Valid Google Maps URL should pass
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.maps_url', 'https://maps.app.goo.gl/example')
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('handles facility removal edge cases', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
// Set custom facilities
|
||||
$component->set('form.facilities', ['Parkir', 'WiFi', 'AC']);
|
||||
|
||||
// Remove middle facility
|
||||
$component->call('removeFacility', 1);
|
||||
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
|
||||
|
||||
// Try to remove non-existent index (should not crash)
|
||||
$component->call('removeFacility', 99);
|
||||
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
|
||||
});
|
||||
|
||||
it('creates outlet with soft delete capability', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet)->toBeInstanceOf(Outlet::class);
|
||||
|
||||
// Verify soft delete trait
|
||||
$createdOutlet->delete();
|
||||
expect(Outlet::withTrashed()->find($createdOutlet->id))->not->toBeNull();
|
||||
expect(Outlet::find($createdOutlet->id))->toBeNull();
|
||||
});
|
||||
|
||||
@ -1,44 +1,30 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Livewire\Studio\Master\Outlet\Edit;
|
||||
use App\Livewire\Studio\Master\Outlet\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function makeLivewireFile(string $name)
|
||||
{
|
||||
$file = UploadedFile::fake()->image($name, 100, 100);
|
||||
$tmpId = Str::random(32);
|
||||
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
|
||||
$path = "livewire-tmp/{$filename}";
|
||||
|
||||
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
|
||||
|
||||
return [
|
||||
'tmpFilename' => $filename,
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'extension' => $file->getClientOriginalExtension(),
|
||||
'path' => storage_path("app/private/{$path}"),
|
||||
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
|
||||
'size' => $file->getSize(),
|
||||
];
|
||||
}
|
||||
|
||||
function mountEditComponent(User $user, Outlet $outlet)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Edit::class, ['outlet' => $outlet]);
|
||||
@ -132,3 +118,229 @@ function mountEditComponent(User $user, Outlet $outlet)
|
||||
expect($media->getPath())->toBeFile();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads outlet data into form correctly', function () {
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(3))
|
||||
->create();
|
||||
|
||||
$component = mountEditComponent($this->user, $outlet);
|
||||
|
||||
expect($component->form->outlet->id)->toBe($outlet->id);
|
||||
expect($component->form->name)->toBe($outlet->name);
|
||||
expect($component->form->phone_number)->toBe($outlet->phone_number);
|
||||
expect($component->form->address)->toBe($outlet->address);
|
||||
expect($component->form->landmark)->toBe($outlet->landmark);
|
||||
expect($component->form->status)->toBe($outlet->status->value);
|
||||
expect($component->form->facilities)->toBe($outlet->facilities->pluck('name')->toArray());
|
||||
});
|
||||
|
||||
it('loads opening hours into form correctly', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
|
||||
// Create specific opening hours
|
||||
$openingHours = [];
|
||||
foreach (Day::cases() as $day) {
|
||||
$openingHours[] = OpeningHour::factory()->create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'day' => $day,
|
||||
'open_time' => '09:00',
|
||||
'close_time' => '21:00',
|
||||
]);
|
||||
}
|
||||
|
||||
$component = mountEditComponent($this->user, $outlet);
|
||||
|
||||
foreach (Day::cases() as $day) {
|
||||
expect($component->form->opening_hours[$day->value]['open_time'])->toBe('09:00');
|
||||
expect($component->form->opening_hours[$day->value]['close_time'])->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('updates outlet with partial data changes', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(2))
|
||||
->create();
|
||||
|
||||
$newName = 'Updated Outlet Name';
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', $newName)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->name)->toBe($newName);
|
||||
expect($outlet->phone_number)->toBe($outlet->phone_number); // Unchanged
|
||||
});
|
||||
|
||||
it('updates outlet status to TERMINATED and sets closed_date', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.status', OutletStatus::TERMINATED->value)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->status->value)->toBe(OutletStatus::TERMINATED->value);
|
||||
expect($outlet->closed_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('updates facilities correctly', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(2))
|
||||
->create();
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
$newFacilities = ['WiFi', 'AC', 'Parking'];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.facilities', $newFacilities)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->facilities->pluck('name')->toArray())->toBe($newFacilities);
|
||||
});
|
||||
|
||||
it('updates opening hours correctly', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
$newOpeningHours = makeValidOpeningHours();
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.opening_hours', $newOpeningHours)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->openingHours)->toHaveCount(count(Day::cases()));
|
||||
|
||||
foreach ($outlet->openingHours as $openingHour) {
|
||||
expect($openingHour->open_time)->toBe('09:00');
|
||||
expect($openingHour->close_time)->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('validates required fields on update', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', '') // Empty required field
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('maintains outlet relationships integrity', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(3))
|
||||
->create();
|
||||
|
||||
$originalOpeningHoursCount = count(Day::cases()); // Should be 7 after setOutlet
|
||||
$originalFacilitiesCount = 1; // We set facilities to ['Parkir'] in the test
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->openingHours)->toHaveCount($originalOpeningHoursCount);
|
||||
expect($outlet->facilities)->toHaveCount($originalFacilitiesCount);
|
||||
});
|
||||
|
||||
it('handles outlet with no existing media', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->getMedia('featured_image'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('preserves outlet slug when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$originalSlug = $outlet->slug;
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.phone_number', '0812 3456 7890') // Change non-slug field
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->slug)->toBe($originalSlug);
|
||||
});
|
||||
|
||||
@ -7,26 +7,23 @@
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
||||
->map(fn ($role) => Role::create(['name' => $role]));
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
// ->has(Outlet::factory()->count(5))
|
||||
->create();
|
||||
|
||||
$this->user->roles()->attach($roles->pluck('id'));
|
||||
});
|
||||
|
||||
function createOutlet(array $attributes = [])
|
||||
function createOutlet(array $attributes = []): Outlet
|
||||
{
|
||||
return Outlet::factory()
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
@ -37,7 +34,7 @@ function createOutlet(array $attributes = [])
|
||||
->create();
|
||||
}
|
||||
|
||||
function mountIndexComponent(User $user)
|
||||
function mountIndexComponent(User $user): Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
@ -48,23 +45,24 @@ function mountIndexComponent(User $user)
|
||||
->assertViewHas('pageTitle', 'Outlet');
|
||||
});
|
||||
|
||||
it('mounts outlets correctly', function () {
|
||||
it('loads outlets as Eloquent collection', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewHas('outlets', function ($outlets) use ($outlet) {
|
||||
return collect($outlets)->contains(
|
||||
fn ($o) => $o['name'] === $outlet->name &&
|
||||
$o['phone_number'] === $outlet->phone_number
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens image modal when openImage is called', function () {
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
$url = 'https://example.com/test.jpg';
|
||||
$component->call('openImage', $url)->assertSet('imageUrl', $url);
|
||||
expect($component->get('outlets'))
|
||||
->toBeInstanceOf(Collection::class)
|
||||
->and($component->get('outlets')->first()->id)->toBe($outlet->id);
|
||||
});
|
||||
|
||||
it('loads outlets with relationships', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
|
||||
expect($loadedOutlet->openingHours)->toHaveCount(7)
|
||||
->and($loadedOutlet->facilities)->toHaveCount(5);
|
||||
});
|
||||
|
||||
it('displays all outlets', function () {
|
||||
@ -72,15 +70,25 @@ function mountIndexComponent(User $user)
|
||||
createOutlet();
|
||||
}
|
||||
|
||||
$outletCount = Outlet::count();
|
||||
$firstOutlet = Outlet::first();
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->assertSee($firstOutlet->name)
|
||||
->assertViewHas('outlets', fn ($outlets) => count($outlets) === $outletCount);
|
||||
expect($component->get('outlets'))->toHaveCount(5);
|
||||
expect($component->get('outlets')->first())->toBeInstanceOf(Outlet::class);
|
||||
});
|
||||
|
||||
it('deletes an outlet successfully', function () {
|
||||
it('prevents delete when user is unauthorized', function () {
|
||||
$outlet = createOutlet();
|
||||
Gate::define('delete outlet', fn () => false);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $outlet)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('deletes an outlet successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'delete outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = createOutlet();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
@ -97,18 +105,19 @@ function mountIndexComponent(User $user)
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(3);
|
||||
expect($component->get('outlets'))->toHaveCount(3);
|
||||
|
||||
$component->set('search', 'Jakarta')
|
||||
->assertSet('search', 'Jakarta');
|
||||
// Test filtering by Jakarta
|
||||
$component->set('search', 'Jakarta');
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('search', 'Jakarta');
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(1);
|
||||
expect($component->get('outlets.0.name'))->toBe('Outlet Jakarta');
|
||||
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||
expect($filteredComponent->get('outlets')->first()->name)->toBe('Outlet Jakarta');
|
||||
|
||||
$component->set('search', 'Outlet')
|
||||
->assertSet('search', 'Outlet');
|
||||
// Test filtering by Outlet
|
||||
$filteredComponent2 = mountIndexComponent($this->user)->set('search', 'Outlet');
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(2);
|
||||
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filters outlets by status', function () {
|
||||
@ -118,20 +127,20 @@ function mountIndexComponent(User $user)
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(3);
|
||||
expect($component->get('outlets'))->toHaveCount(3);
|
||||
|
||||
$component->set('status', [OutletStatus::OPERATIONAL->value])
|
||||
->assertSet('status', [OutletStatus::OPERATIONAL->value]);
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('status', [OutletStatus::OPERATIONAL->value]);
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(1);
|
||||
expect($component->get('outlets.0.name'))->toBe($outlet1->name);
|
||||
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||
expect($filteredComponent->get('outlets')->first()->name)->toBe($outlet1->name);
|
||||
|
||||
$component->set('status', [
|
||||
// Test filtering by multiple statuses
|
||||
$filteredComponent2 = mountIndexComponent($this->user)->set('status', [
|
||||
OutletStatus::OPERATIONAL->value,
|
||||
OutletStatus::UNDER_CONSTRUCTION->value,
|
||||
]);
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(2);
|
||||
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filters outlets by both search and status', function () {
|
||||
@ -143,35 +152,19 @@ function mountIndexComponent(User $user)
|
||||
->set('search', 'Outlet')
|
||||
->set('status', [OutletStatus::OPERATIONAL->value]);
|
||||
|
||||
expect(count($component->get('outlets')))->toBe(2);
|
||||
expect($component->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('loads outlet data with all required fields', function () {
|
||||
it('loads outlets with all required relationships', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$outletData = $component->get('outlets.0');
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
|
||||
expect($outletData)->toHaveKeys([
|
||||
'hash',
|
||||
'name',
|
||||
'phone_number',
|
||||
'landmark',
|
||||
'address',
|
||||
'status',
|
||||
'opening_hours',
|
||||
'facilities',
|
||||
'opened_date',
|
||||
'opened_ago',
|
||||
'closed_date',
|
||||
'closed_ago',
|
||||
'image',
|
||||
'images',
|
||||
]);
|
||||
|
||||
expect($outletData['status'])->toHaveKeys(['label', 'color']);
|
||||
expect($outletData['name'])->toBe($outlet->name);
|
||||
expect($outletData['phone_number'])->toBe($outlet->phone_number);
|
||||
expect($loadedOutlet->openingHours)->toHaveCount(7)
|
||||
->and($loadedOutlet->facilities)->toHaveCount(5)
|
||||
->and($loadedOutlet->name)->toBe($outlet->name)
|
||||
->and($loadedOutlet->phone_number)->toBe($outlet->phone_number);
|
||||
});
|
||||
|
||||
it('loads outlets ordered by latest', function () {
|
||||
@ -181,6 +174,66 @@ function mountIndexComponent(User $user)
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets.0.name'))->toBe('Second');
|
||||
expect($component->get('outlets.1.name'))->toBe('First');
|
||||
expect($component->get('outlets')->first()->name)->toBe('Second');
|
||||
expect($component->get('outlets')->last()->name)->toBe('First');
|
||||
});
|
||||
|
||||
it('provides helper methods for view formatting', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Test that outlets are loaded with proper data structure
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
expect($loadedOutlet)->toBeInstanceOf(Outlet::class);
|
||||
expect($loadedOutlet->openingHours)->toBeInstanceOf(Collection::class);
|
||||
expect($loadedOutlet->facilities)->toBeInstanceOf(Collection::class);
|
||||
});
|
||||
|
||||
it('handles empty search results gracefully', function () {
|
||||
createOutlet(['name' => 'Existing Outlet']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'NonExistentOutlet');
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('maintains search state across component lifecycle', function () {
|
||||
$outlet1 = createOutlet(['name' => 'Test Outlet']);
|
||||
$outlet2 = createOutlet(['name' => 'Another Outlet']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'Test');
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(1);
|
||||
expect($component->get('search'))->toBe('Test');
|
||||
|
||||
// Re-mount should maintain state
|
||||
$newComponent = mountIndexComponent($this->user)->set('search', 'Test');
|
||||
|
||||
expect($newComponent->get('outlets'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('handles status filter with empty array', function () {
|
||||
createOutlet(['status' => OutletStatus::OPERATIONAL]);
|
||||
createOutlet(['status' => OutletStatus::TERMINATED]);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('status', []);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('loads outlets efficiently with eager loading', function () {
|
||||
$outlets = collect();
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$outlets->push(createOutlet());
|
||||
}
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Verify relationships are loaded
|
||||
$loadedOutlets = $component->get('outlets');
|
||||
foreach ($loadedOutlets as $loadedOutlet) {
|
||||
expect($loadedOutlet->relationLoaded('openingHours'))->toBeTrue();
|
||||
expect($loadedOutlet->relationLoaded('facilities'))->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
37
tests/Feature/Livewire/Studio/Master/Outlet/TestHelpers.php
Normal file
37
tests/Feature/Livewire/Studio/Master/Outlet/TestHelpers.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Day;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
function makeLivewireFile(string $name)
|
||||
{
|
||||
$file = UploadedFile::fake()->image($name, 100, 100);
|
||||
$tmpId = Str::random(32);
|
||||
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
|
||||
$path = "livewire-tmp/{$filename}";
|
||||
|
||||
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
|
||||
|
||||
return [
|
||||
'tmpFilename' => $filename,
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'extension' => $file->getClientOriginalExtension(),
|
||||
'path' => storage_path("app/private/{$path}"),
|
||||
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
|
||||
'size' => $file->getSize(),
|
||||
];
|
||||
}
|
||||
|
||||
function makeValidOpeningHours()
|
||||
{
|
||||
return collect(Day::cases())
|
||||
->mapWithKeys(fn ($day) => [
|
||||
$day->value => [
|
||||
'open_time' => '09:00',
|
||||
'close_time' => '21:00',
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user