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;
|
public ?string $maps_url = null;
|
||||||
|
|
||||||
|
/** @var array<string, array{open_time: ?string, close_time: ?string}> */
|
||||||
public array $opening_hours = [];
|
public array $opening_hours = [];
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
public array $facilities = ['Parkir'];
|
public array $facilities = ['Parkir'];
|
||||||
|
|
||||||
public string $opened_date = '';
|
public string $opened_date = '';
|
||||||
|
|
||||||
public string $status = '1';
|
public int $status = OutletStatus::OPERATIONAL->value;
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $featured_image = [];
|
public array $featured_image = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $images = [];
|
public array $images = [];
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$rules = [
|
return array_merge($this->getBasicRules(), $this->getOpeningHoursRules());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBasicRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
'name' => ['required', 'string', 'max:100'],
|
'name' => ['required', 'string', 'max:100'],
|
||||||
'phone_number' => ['required', 'string', new PhoneNumber],
|
'phone_number' => ['required', 'string', new PhoneNumber],
|
||||||
'address' => ['required', 'string'],
|
'address' => ['required', 'string'],
|
||||||
@ -60,6 +69,11 @@ public function rules(): array
|
|||||||
'featured_image' => ['required', 'array', 'max:1'],
|
'featured_image' => ['required', 'array', 'max:1'],
|
||||||
'images' => ['required', 'array', 'max:5'],
|
'images' => ['required', 'array', 'max:5'],
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOpeningHoursRules(): array
|
||||||
|
{
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
foreach (Day::cases() as $day) {
|
foreach (Day::cases() as $day) {
|
||||||
$rules["opening_hours.{$day->value}"] = ['required', 'array'];
|
$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->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->name = $outlet->name;
|
||||||
$this->phone_number = $outlet->phone_number;
|
$this->phone_number = $outlet->phone_number;
|
||||||
$this->address = $outlet->address;
|
$this->address = $outlet->address;
|
||||||
$this->landmark = $outlet->landmark;
|
$this->landmark = $outlet->landmark;
|
||||||
$this->maps_url = $outlet->maps_url;
|
$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) => [
|
->mapWithKeys(fn ($item) => [
|
||||||
$item->day->value => [
|
$item->day->value => [
|
||||||
'open_time' => $item->open_time ? formatTime($item->open_time) : null,
|
'open_time' => $item->open_time ? formatTime($item->open_time) : null,
|
||||||
@ -107,29 +136,37 @@ public function setOutlet(Outlet $outlet)
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
->toArray();
|
->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();
|
$this->validate();
|
||||||
|
|
||||||
$data = $this->all();
|
$data = $this->prepareDataForSave();
|
||||||
|
|
||||||
if ($data['status'] == OutletStatus::TERMINATED) {
|
|
||||||
$data['closed_date'] = now();
|
|
||||||
} else {
|
|
||||||
$data['closed_date'] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::transaction(function () use ($data) {
|
DB::transaction(function () use ($data) {
|
||||||
$outlet = Outlet::create($data);
|
$outlet = Outlet::create($data);
|
||||||
|
|
||||||
$openingHours = collect($data['opening_hours'])
|
$this->createOpeningHours($outlet, $data['opening_hours']);
|
||||||
|
$this->createFacilities($outlet, $data['facilities']);
|
||||||
|
$this->handleMediaUpload($outlet);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => [
|
->map(fn ($time, $day) => [
|
||||||
'outlet_id' => $outlet->id,
|
'outlet_id' => $outlet->id,
|
||||||
'day' => $day,
|
'day' => $day,
|
||||||
@ -139,30 +176,49 @@ public function store()
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
OpeningHour::insert($openingHours);
|
OpeningHour::insert($hoursData);
|
||||||
$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');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update()
|
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();
|
$this->validate();
|
||||||
|
|
||||||
$data = $this->all();
|
$data = $this->prepareDataForSave();
|
||||||
|
|
||||||
if ($data['status'] == OutletStatus::TERMINATED->value) {
|
|
||||||
$data['closed_date'] = now()->toDateString();
|
|
||||||
} else {
|
|
||||||
$data['closed_date'] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::transaction(function () use ($data) {
|
DB::transaction(function () use ($data) {
|
||||||
$this->outlet->update($data);
|
$this->outlet->update($data);
|
||||||
|
|
||||||
foreach ($data['opening_hours'] as $day => $hours) {
|
$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(
|
OpeningHour::updateOrCreate(
|
||||||
[
|
[
|
||||||
'outlet_id' => $this->outlet->id,
|
'outlet_id' => $this->outlet->id,
|
||||||
@ -174,21 +230,27 @@ public function update()
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$facilities = collect($data['facilities']);
|
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) {
|
foreach ($facilities as $facilityName) {
|
||||||
$this->outlet->facilities()->updateOrCreate(
|
$this->outlet->facilities()->updateOrCreate(
|
||||||
['name' => $facilityName],
|
['name' => $facilityName],
|
||||||
['name' => $facilityName]
|
['name' => $facilityName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleMediaSync(array $data): void
|
||||||
|
{
|
||||||
$this->syncMedia($data['featured_image'], $this->outlet, 'featured_image');
|
$this->syncMedia($data['featured_image'], $this->outlet, 'featured_image');
|
||||||
$this->syncMedia($data['images'], $this->outlet, 'images');
|
$this->syncMedia($data['images'], $this->outlet, 'images');
|
||||||
|
|
||||||
$this->uploadMedia($this->featured_image, $this->outlet, 'featured_image');
|
|
||||||
$this->uploadMedia($this->images, $this->outlet, 'images');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,30 +7,35 @@
|
|||||||
use App\Traits\Outlet\WithFacilityHandler;
|
use App\Traits\Outlet\WithFacilityHandler;
|
||||||
use App\Traits\WithAuthorization;
|
use App\Traits\WithAuthorization;
|
||||||
use App\Traits\WithToast;
|
use App\Traits\WithToast;
|
||||||
use App\Traits\WithUpdatedData;
|
use Illuminate\Contracts\View\View;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Title('Tambah Outlet')]
|
#[Title('Tambah Outlet')]
|
||||||
class Create extends Component
|
class Create extends Component
|
||||||
{
|
{
|
||||||
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
|
use WithAuthorization, WithFacilityHandler, WithToast;
|
||||||
|
|
||||||
public OutletForm $form;
|
public OutletForm $form;
|
||||||
|
|
||||||
|
/** @var array<int, Day> */
|
||||||
public array $days = [];
|
public array $days = [];
|
||||||
|
|
||||||
public function mount()
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->form->opening_hours = collect(Day::cases())
|
$this->form->opening_hours = collect(Day::cases())
|
||||||
->mapWithKeys(fn (Day $day) => [
|
->mapWithKeys(fn (Day $day) => [
|
||||||
$day->value => ['open_time' => null, 'close_time' => null],
|
$day->value => [
|
||||||
|
'open_time' => null,
|
||||||
|
'close_time' => null,
|
||||||
|
],
|
||||||
])
|
])
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
$this->days = Day::cases();
|
$this->days = Day::cases();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save()
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->canOrAbort('create outlet');
|
$this->canOrAbort('create outlet');
|
||||||
|
|
||||||
@ -41,7 +46,7 @@ public function save()
|
|||||||
$this->redirectRoute('studio.master.outlet.index');
|
$this->redirectRoute('studio.master.outlet.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.studio.master.outlet.form', [
|
return view('livewire.studio.master.outlet.form', [
|
||||||
'pageTitle' => 'Tambah Outlet',
|
'pageTitle' => 'Tambah Outlet',
|
||||||
|
|||||||
@ -8,27 +8,27 @@
|
|||||||
use App\Traits\Outlet\WithFacilityHandler;
|
use App\Traits\Outlet\WithFacilityHandler;
|
||||||
use App\Traits\WithAuthorization;
|
use App\Traits\WithAuthorization;
|
||||||
use App\Traits\WithToast;
|
use App\Traits\WithToast;
|
||||||
use App\Traits\WithUpdatedData;
|
use Illuminate\Contracts\View\View;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Title('Ubah Outlet')]
|
#[Title('Ubah Outlet')]
|
||||||
class Edit extends Component
|
class Edit extends Component
|
||||||
{
|
{
|
||||||
use WithAuthorization, WithFacilityHandler, WithToast, WithUpdatedData;
|
use WithAuthorization, WithFacilityHandler, WithToast;
|
||||||
|
|
||||||
public OutletForm $form;
|
public OutletForm $form;
|
||||||
|
|
||||||
|
/** @var array<int, Day> */
|
||||||
public array $days = [];
|
public array $days = [];
|
||||||
|
|
||||||
public function mount(Outlet $outlet)
|
public function mount(Outlet $outlet): void
|
||||||
{
|
{
|
||||||
$this->form->setOutlet($outlet);
|
$this->form->setOutlet($outlet);
|
||||||
|
|
||||||
$this->days = Day::cases();
|
$this->days = Day::cases();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save()
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->canOrAbort('update outlet');
|
$this->canOrAbort('update outlet');
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ public function save()
|
|||||||
$this->redirectRoute('studio.master.outlet.index');
|
$this->redirectRoute('studio.master.outlet.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.studio.master.outlet.form', [
|
return view('livewire.studio.master.outlet.form', [
|
||||||
'pageTitle' => 'Ubah Outlet',
|
'pageTitle' => 'Ubah Outlet',
|
||||||
|
|||||||
@ -4,98 +4,92 @@
|
|||||||
|
|
||||||
use App\Enums\Day;
|
use App\Enums\Day;
|
||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
use App\Traits\Notification\WithSubscribeNotification;
|
|
||||||
use App\Traits\WithCloseModal;
|
use App\Traits\WithCloseModal;
|
||||||
use App\Traits\WithConfirmation;
|
use App\Traits\WithConfirmation;
|
||||||
use App\Traits\WithMediaHandler;
|
use App\Traits\WithMediaHandler;
|
||||||
use App\Traits\WithToast;
|
use App\Traits\WithToast;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
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 WithCloseModal, WithConfirmation, WithMediaHandler, WithSubscribeNotification, WithToast;
|
use WithCloseModal, WithConfirmation, WithMediaHandler, WithToast;
|
||||||
|
|
||||||
public array $outlets = [];
|
/** @var Collection<int, Outlet> */
|
||||||
|
public Collection $outlets;
|
||||||
public ?string $imageUrl = null;
|
|
||||||
|
|
||||||
public string $search = '';
|
public string $search = '';
|
||||||
|
|
||||||
public array $status = [];
|
public array $status = [];
|
||||||
|
|
||||||
public function mount()
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->loadOutlets();
|
$this->loadOutlets();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadOutlets()
|
protected function loadOutlets(): void
|
||||||
{
|
{
|
||||||
$this->outlets = Outlet::with(['openingHours', 'facilities'])
|
$this->outlets = Outlet::with(['openingHours', 'facilities'])
|
||||||
->when(! empty($this->search), fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
->when(! empty($this->search), fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||||
->when(! empty($this->status), fn ($query) => $query->whereIn('status', $this->status))
|
->when(! empty($this->status), fn ($query) => $query->whereIn('status', $this->status))
|
||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get();
|
||||||
->map(fn ($outlet) => [
|
}
|
||||||
'hash' => $outlet->hash,
|
|
||||||
'name' => $outlet->name,
|
protected function formatOpeningHours(Outlet $outlet): array
|
||||||
'phone_number' => $outlet->phone_number,
|
{
|
||||||
'landmark' => $outlet->landmark,
|
return $outlet->openingHours->mapWithKeys(fn ($hour) => [
|
||||||
'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())) => [
|
Str::ucfirst(Str::lower(Day::from($hour->day->value)->label())) => [
|
||||||
'open' => formatTime($hour->open_time),
|
'open' => formatTime($hour->open_time),
|
||||||
'close' => formatTime($hour->close_time),
|
'close' => formatTime($hour->close_time),
|
||||||
'is_closed' => $hour->open_time === null && $hour->close_time === null,
|
'is_closed' => $hour->open_time === null && $hour->close_time === null,
|
||||||
],
|
],
|
||||||
])->toArray(),
|
])->toArray();
|
||||||
'facilities' => $outlet->facilities->pluck('name')->toArray(),
|
}
|
||||||
'opened_date' => formatDate($outlet->opened_date),
|
|
||||||
'opened_ago' => timeAgo($outlet->opened_date),
|
protected function getFeaturedImage(Outlet $outlet): string
|
||||||
'closed_date' => formatDate($outlet->closed_date),
|
{
|
||||||
'closed_ago' => timeAgo($outlet->closed_date),
|
$mediaCollection = $this->mapMediaCollection($outlet->getMedia('featured_image'));
|
||||||
'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(),
|
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();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedSearch(string $value)
|
public function updatedSearch(string $value): void
|
||||||
{
|
{
|
||||||
$this->search = $value;
|
$this->search = $value;
|
||||||
|
|
||||||
$this->loadOutlets();
|
$this->loadOutlets();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedStatus()
|
public function updatedStatus(): void
|
||||||
{
|
{
|
||||||
$this->loadOutlets();
|
$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();
|
$outlet->delete();
|
||||||
|
|
||||||
$this->toast('Outlet berhasil dihapus.');
|
$this->toast('Outlet berhasil dihapus.');
|
||||||
|
$this->loadOutlets();
|
||||||
Flux::modals()->close();
|
Flux::modals()->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.studio.master.outlet.index', [
|
return view('livewire.studio.master.outlet.index', [
|
||||||
'pageTitle' => 'Outlet',
|
'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">
|
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-start justify-between mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<img src="{{ $outlet['image'] }}" alt="{{ $outlet['image'] }}"
|
<img src="{{ $this->getFeaturedImage($outlet) }}" alt="{{ $outlet->name }}"
|
||||||
class="w-12 h-12 rounded-full cursor-pointer"
|
class="w-12 h-12 rounded-full">
|
||||||
wire:click="openImage('{{ $outlet['image'] }}')">
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<flux:heading size="lg">{{ $outlet['name'] }}</flux:heading>
|
<flux:heading size="lg">{{ $outlet->name }}</flux:heading>
|
||||||
<flux:badge color="{{ $outlet['status']['color'] }}" size="sm">
|
<flux:badge color="{{ $outlet->status->color() }}" size="sm">
|
||||||
{{ $outlet['status']['label'] }}
|
{{ $outlet->status->label() }}
|
||||||
</flux:badge>
|
</flux:badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,14 +49,14 @@ class="w-12 h-12 rounded-full cursor-pointer"
|
|||||||
<div class="space-y-3 mb-4">
|
<div class="space-y-3 mb-4">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<flux:icon.phone class="w-4 h-4 text-gray-500 dark:text-white" />
|
<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>
|
||||||
|
|
||||||
<div class="flex items-start gap-2 text-sm">
|
<div class="flex items-start gap-2 text-sm">
|
||||||
<flux:icon.map-pin class="w-4 h-4 text-gray-500 dark:text-white" />
|
<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="text-gray-700 dark:text-gray-300">
|
||||||
<div class="font-medium">{{ $outlet['landmark'] }}</div>
|
<div class="font-medium">{{ $outlet->landmark }}</div>
|
||||||
<div class="text-gray-600 dark:text-gray-400">{{ $outlet['address'] }}</div>
|
<div class="text-gray-600 dark:text-gray-400">{{ $outlet->address }}</div>
|
||||||
</div>
|
</div>
|
||||||
</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
|
Jam Operasional
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
<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">
|
<div class="flex justify-between text-xs">
|
||||||
<span
|
<span
|
||||||
class="font-medium text-gray-700 dark:text-gray-300">{{ $day }}:</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
|
Fasilitas
|
||||||
</h4>
|
</h4>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@foreach ($outlet['facilities'] as $facility)
|
@foreach ($outlet->facilities as $facility)
|
||||||
<flux:badge color="zinc" size="sm">{{ $facility }}</flux:badge>
|
<flux:badge color="zinc" size="sm">{{ $facility->name }}</flux:badge>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</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 class="flex flex-col gap-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Buka:</span>
|
<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
|
||||||
<span class="text-gray-500">({{ $outlet['opened_ago'] }})</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>
|
</div>
|
||||||
@if ($outlet['closed_date'])
|
@if ($outlet->closed_date)
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">Tgl Tutup:</span>
|
<span class="font-medium text-gray-900 dark:text-white">Tgl Tutup:</span>
|
||||||
<span
|
<span
|
||||||
class="text-gray-700 dark:text-gray-300">{{ $outlet['closed_date'] }}</span>
|
class="text-gray-700 dark:text-gray-300">{{ formatDate($outlet->closed_date) }}</span>
|
||||||
<span class="text-gray-500">({{ $outlet['closed_ago'] }})</span>
|
<span class="text-gray-500">({{ timeAgo($outlet->closed_date) }})</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (count($outlet['images']) > 0)
|
@if ($this->getGalleryImages($outlet))
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4
|
<h4
|
||||||
class="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
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
|
Gambar
|
||||||
</h4>
|
</h4>
|
||||||
<div class="flex mt-6">
|
<div class="flex mt-6">
|
||||||
@foreach ($outlet['images'] as $key => $value)
|
@foreach ($this->getGalleryImages($outlet) as $imageUrl)
|
||||||
<img src="{{ $value }}" alt="{{ $value }}"
|
<img src="{{ $imageUrl }}" alt="{{ $outlet->name }}"
|
||||||
class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
class="w-12 h-12 rounded-full -ml-3 first:ml-0">
|
||||||
wire:click="openImage('{{ $value }}')">
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
|
||||||
@can('update outlet')
|
@can('update outlet')
|
||||||
<flux:tooltip content="Ubah">
|
<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"
|
wire:navigate.hover variant="primary" color="yellow" icon="pencil-square"
|
||||||
size="sm">
|
size="sm">
|
||||||
</flux:button>
|
</flux:button>
|
||||||
@ -142,10 +141,10 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
|||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
@can('delete outlet')
|
@can('delete outlet')
|
||||||
<flux:modal.trigger name="delete">
|
<flux:modal.trigger name="delete-confirmation">
|
||||||
<flux:tooltip content="Hapus">
|
<flux:tooltip content="Hapus">
|
||||||
<flux:button variant="danger" icon="trash" size="sm"
|
<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:button>
|
||||||
</flux:tooltip>
|
</flux:tooltip>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
@ -156,19 +155,18 @@ class="w-12 h-12 rounded-full -ml-3 first:ml-0 cursor-pointer"
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
@include('components.lottie.not-found')
|
@include('components.animations.lottie.not-found')
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@include('components.confirmation.delete')
|
@include('components.modals.confirmation', [
|
||||||
|
'modalName' => 'delete-confirmation',
|
||||||
<flux:modal name="image-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
|
'modalTitle' => 'Apakah Anda yakin?',
|
||||||
<div class="flex justify-center items-center p-4">
|
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
|
||||||
<img src="{{ $imageUrl }}" alt="Outlet Image" class="rounded-lg max-h-[80vh] object-contain" />
|
'buttonVariant' => 'primary',
|
||||||
</div>
|
'buttonColor' => 'danger',
|
||||||
</flux:modal>
|
'buttonText' => 'Ya, Hapus',
|
||||||
|
])
|
||||||
</flux:main>
|
</flux:main>
|
||||||
|
|
||||||
@assets
|
@assets
|
||||||
|
|||||||
@ -66,7 +66,6 @@
|
|||||||
Route::get('/', OutletIndex::class)->name('index')->middleware('can:view outlet');
|
Route::get('/', OutletIndex::class)->name('index')->middleware('can:view outlet');
|
||||||
Route::get('/create', OutletCreate::class)->name('create')->middleware('can:create 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::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 () {
|
Route::prefix('users')->name('user.')->group(function () {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/TestHelpers.php';
|
||||||
|
|
||||||
use App\Enums\Day;
|
use App\Enums\Day;
|
||||||
|
use App\Enums\OutletStatus;
|
||||||
use App\Livewire\Studio\Master\Outlet\Create;
|
use App\Livewire\Studio\Master\Outlet\Create;
|
||||||
use App\Livewire\Studio\Master\Outlet\Index;
|
use App\Livewire\Studio\Master\Outlet\Index;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
@ -8,48 +11,19 @@
|
|||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\UploadedFile;
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
use Spatie\Permission\Models\Role;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
|
||||||
->map(fn ($role) => Role::create(['name' => $role]));
|
|
||||||
|
|
||||||
$this->user = User::factory()
|
$this->user = User::factory()
|
||||||
->active()
|
->active()
|
||||||
->has(Employee::factory())
|
->has(Employee::factory())
|
||||||
// ->has(Outlet::factory()->count(5))
|
|
||||||
->create();
|
->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)
|
function mountCreateComponent(User $user)
|
||||||
{
|
{
|
||||||
return Livewire::actingAs($user)->test(Create::class);
|
return Livewire::actingAs($user)->test(Create::class);
|
||||||
@ -156,8 +130,6 @@ function mountCreateComponent(User $user)
|
|||||||
'form.phone_number',
|
'form.phone_number',
|
||||||
'form.address',
|
'form.address',
|
||||||
'form.landmark',
|
'form.landmark',
|
||||||
'form.opening_hours',
|
|
||||||
'form.facilities',
|
|
||||||
'form.opened_date',
|
'form.opened_date',
|
||||||
'form.featured_image',
|
'form.featured_image',
|
||||||
'form.images',
|
'form.images',
|
||||||
@ -220,6 +192,17 @@ function mountCreateComponent(User $user)
|
|||||||
expect($component->form->facilities)->toBe(['Parkir']);
|
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 () {
|
it('creates outlet with opening hours for all days', function () {
|
||||||
$permission = Permission::create(['name' => 'create outlet']);
|
$permission = Permission::create(['name' => 'create outlet']);
|
||||||
$this->user->givePermissionTo($permission);
|
$this->user->givePermissionTo($permission);
|
||||||
@ -253,4 +236,190 @@ function mountCreateComponent(User $user)
|
|||||||
|
|
||||||
$createdOutlet = Outlet::first();
|
$createdOutlet = Outlet::first();
|
||||||
expect($createdOutlet->openingHours)->toHaveCount(count(Day::cases()));
|
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
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/TestHelpers.php';
|
||||||
|
|
||||||
use App\Enums\Day;
|
use App\Enums\Day;
|
||||||
|
use App\Enums\OutletStatus;
|
||||||
use App\Livewire\Studio\Master\Outlet\Edit;
|
use App\Livewire\Studio\Master\Outlet\Edit;
|
||||||
use App\Livewire\Studio\Master\Outlet\Index;
|
use App\Livewire\Studio\Master\Outlet\Index;
|
||||||
|
use App\Models\Employee;
|
||||||
use App\Models\Facility;
|
use App\Models\Facility;
|
||||||
|
use App\Models\OpeningHour;
|
||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\UploadedFile;
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function () {
|
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)
|
function mountEditComponent(User $user, Outlet $outlet)
|
||||||
{
|
{
|
||||||
return Livewire::actingAs($user)->test(Edit::class, ['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();
|
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\OpeningHour;
|
||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Livewire\Features\SupportTesting\Testable;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
|
||||||
->map(fn ($role) => Role::create(['name' => $role]));
|
|
||||||
|
|
||||||
$this->user = User::factory()
|
$this->user = User::factory()
|
||||||
->active()
|
->active()
|
||||||
->has(Employee::factory())
|
->has(Employee::factory())
|
||||||
// ->has(Outlet::factory()->count(5))
|
|
||||||
->create();
|
->create();
|
||||||
|
|
||||||
$this->user->roles()->attach($roles->pluck('id'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function createOutlet(array $attributes = [])
|
function createOutlet(array $attributes = []): Outlet
|
||||||
{
|
{
|
||||||
return Outlet::factory()
|
return Outlet::factory()
|
||||||
->has(OpeningHour::factory()->count(7))
|
->has(OpeningHour::factory()->count(7))
|
||||||
@ -37,7 +34,7 @@ function createOutlet(array $attributes = [])
|
|||||||
->create();
|
->create();
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountIndexComponent(User $user)
|
function mountIndexComponent(User $user): Testable
|
||||||
{
|
{
|
||||||
return Livewire::actingAs($user)->test(Index::class);
|
return Livewire::actingAs($user)->test(Index::class);
|
||||||
}
|
}
|
||||||
@ -48,23 +45,24 @@ function mountIndexComponent(User $user)
|
|||||||
->assertViewHas('pageTitle', 'Outlet');
|
->assertViewHas('pageTitle', 'Outlet');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mounts outlets correctly', function () {
|
it('loads outlets as Eloquent collection', function () {
|
||||||
$outlet = createOutlet();
|
$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);
|
$component = mountIndexComponent($this->user);
|
||||||
|
|
||||||
$url = 'https://example.com/test.jpg';
|
expect($component->get('outlets'))
|
||||||
$component->call('openImage', $url)->assertSet('imageUrl', $url);
|
->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 () {
|
it('displays all outlets', function () {
|
||||||
@ -72,15 +70,25 @@ function mountIndexComponent(User $user)
|
|||||||
createOutlet();
|
createOutlet();
|
||||||
}
|
}
|
||||||
|
|
||||||
$outletCount = Outlet::count();
|
$component = mountIndexComponent($this->user);
|
||||||
$firstOutlet = Outlet::first();
|
|
||||||
|
|
||||||
mountIndexComponent($this->user)
|
expect($component->get('outlets'))->toHaveCount(5);
|
||||||
->assertSee($firstOutlet->name)
|
expect($component->get('outlets')->first())->toBeInstanceOf(Outlet::class);
|
||||||
->assertViewHas('outlets', fn ($outlets) => count($outlets) === $outletCount);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
$outlet = createOutlet();
|
||||||
|
|
||||||
mountIndexComponent($this->user)
|
mountIndexComponent($this->user)
|
||||||
@ -97,18 +105,19 @@ function mountIndexComponent(User $user)
|
|||||||
|
|
||||||
$component = mountIndexComponent($this->user);
|
$component = mountIndexComponent($this->user);
|
||||||
|
|
||||||
expect(count($component->get('outlets')))->toBe(3);
|
expect($component->get('outlets'))->toHaveCount(3);
|
||||||
|
|
||||||
$component->set('search', 'Jakarta')
|
// Test filtering by Jakarta
|
||||||
->assertSet('search', 'Jakarta');
|
$component->set('search', 'Jakarta');
|
||||||
|
$filteredComponent = mountIndexComponent($this->user)->set('search', 'Jakarta');
|
||||||
|
|
||||||
expect(count($component->get('outlets')))->toBe(1);
|
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||||
expect($component->get('outlets.0.name'))->toBe('Outlet Jakarta');
|
expect($filteredComponent->get('outlets')->first()->name)->toBe('Outlet Jakarta');
|
||||||
|
|
||||||
$component->set('search', 'Outlet')
|
// Test filtering by Outlet
|
||||||
->assertSet('search', '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 () {
|
it('filters outlets by status', function () {
|
||||||
@ -118,20 +127,20 @@ function mountIndexComponent(User $user)
|
|||||||
|
|
||||||
$component = mountIndexComponent($this->user);
|
$component = mountIndexComponent($this->user);
|
||||||
|
|
||||||
expect(count($component->get('outlets')))->toBe(3);
|
expect($component->get('outlets'))->toHaveCount(3);
|
||||||
|
|
||||||
$component->set('status', [OutletStatus::OPERATIONAL->value])
|
$filteredComponent = mountIndexComponent($this->user)->set('status', [OutletStatus::OPERATIONAL->value]);
|
||||||
->assertSet('status', [OutletStatus::OPERATIONAL->value]);
|
|
||||||
|
|
||||||
expect(count($component->get('outlets')))->toBe(1);
|
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||||
expect($component->get('outlets.0.name'))->toBe($outlet1->name);
|
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::OPERATIONAL->value,
|
||||||
OutletStatus::UNDER_CONSTRUCTION->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 () {
|
it('filters outlets by both search and status', function () {
|
||||||
@ -143,35 +152,19 @@ function mountIndexComponent(User $user)
|
|||||||
->set('search', 'Outlet')
|
->set('search', 'Outlet')
|
||||||
->set('status', [OutletStatus::OPERATIONAL->value]);
|
->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();
|
$outlet = createOutlet();
|
||||||
|
|
||||||
$component = mountIndexComponent($this->user);
|
$component = mountIndexComponent($this->user);
|
||||||
$outletData = $component->get('outlets.0');
|
$loadedOutlet = $component->get('outlets')->first();
|
||||||
|
|
||||||
expect($outletData)->toHaveKeys([
|
expect($loadedOutlet->openingHours)->toHaveCount(7)
|
||||||
'hash',
|
->and($loadedOutlet->facilities)->toHaveCount(5)
|
||||||
'name',
|
->and($loadedOutlet->name)->toBe($outlet->name)
|
||||||
'phone_number',
|
->and($loadedOutlet->phone_number)->toBe($outlet->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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads outlets ordered by latest', function () {
|
it('loads outlets ordered by latest', function () {
|
||||||
@ -181,6 +174,66 @@ function mountIndexComponent(User $user)
|
|||||||
|
|
||||||
$component = mountIndexComponent($this->user);
|
$component = mountIndexComponent($this->user);
|
||||||
|
|
||||||
expect($component->get('outlets.0.name'))->toBe('Second');
|
expect($component->get('outlets')->first()->name)->toBe('Second');
|
||||||
expect($component->get('outlets.1.name'))->toBe('First');
|
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