complete story 3.2 with qa tests
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\BlockedTime;
|
||||
use App\Models\Consultation;
|
||||
use Carbon\Carbon;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $filter = 'upcoming';
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public ?int $editingId = null;
|
||||
|
||||
public string $block_date = '';
|
||||
|
||||
public bool $is_all_day = true;
|
||||
|
||||
public string $start_time = '09:00';
|
||||
|
||||
public string $end_time = '17:00';
|
||||
|
||||
public string $reason = '';
|
||||
|
||||
public array $pendingBookings = [];
|
||||
|
||||
public bool $showDeleteModal = false;
|
||||
|
||||
public ?int $deletingId = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->block_date = today()->format('Y-m-d');
|
||||
}
|
||||
|
||||
public function openCreateModal(): void
|
||||
{
|
||||
$this->reset(['editingId', 'block_date', 'is_all_day', 'start_time', 'end_time', 'reason', 'pendingBookings']);
|
||||
$this->block_date = today()->format('Y-m-d');
|
||||
$this->is_all_day = true;
|
||||
$this->start_time = '09:00';
|
||||
$this->end_time = '17:00';
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function openEditModal(int $id): void
|
||||
{
|
||||
$blockedTime = BlockedTime::findOrFail($id);
|
||||
|
||||
$this->editingId = $id;
|
||||
$this->block_date = $blockedTime->block_date->format('Y-m-d');
|
||||
$this->is_all_day = $blockedTime->isAllDay();
|
||||
$this->start_time = $blockedTime->start_time ?? '09:00';
|
||||
$this->end_time = $blockedTime->end_time ?? '17:00';
|
||||
$this->reason = $blockedTime->reason ?? '';
|
||||
$this->pendingBookings = [];
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function closeModal(): void
|
||||
{
|
||||
$this->showModal = false;
|
||||
$this->reset(['editingId', 'pendingBookings']);
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
public function updatedBlockDate(): void
|
||||
{
|
||||
$this->checkPendingBookings();
|
||||
}
|
||||
|
||||
public function updatedIsAllDay(): void
|
||||
{
|
||||
$this->checkPendingBookings();
|
||||
}
|
||||
|
||||
public function updatedStartTime(): void
|
||||
{
|
||||
$this->checkPendingBookings();
|
||||
}
|
||||
|
||||
public function updatedEndTime(): void
|
||||
{
|
||||
$this->checkPendingBookings();
|
||||
}
|
||||
|
||||
public function checkPendingBookings(): void
|
||||
{
|
||||
if (empty($this->block_date)) {
|
||||
$this->pendingBookings = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query = Consultation::query()
|
||||
->whereDate('booking_date', $this->block_date)
|
||||
->where('status', ConsultationStatus::Pending)
|
||||
->with('user:id,full_name,company_name');
|
||||
|
||||
if (! $this->is_all_day && $this->start_time && $this->end_time) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('booking_time', '>=', $this->start_time)
|
||||
->where('booking_time', '<', $this->end_time);
|
||||
});
|
||||
}
|
||||
|
||||
$this->pendingBookings = $query->get()->map(fn ($c) => [
|
||||
'id' => $c->id,
|
||||
'time' => $c->booking_time,
|
||||
'client' => $c->user->full_name ?? $c->user->company_name ?? __('clients.unknown'),
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$rules = [
|
||||
'block_date' => ['required', 'date'],
|
||||
'is_all_day' => ['boolean'],
|
||||
'reason' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
if (! $this->editingId) {
|
||||
$rules['block_date'][] = 'after_or_equal:today';
|
||||
}
|
||||
|
||||
if (! $this->is_all_day) {
|
||||
$rules['start_time'] = ['required'];
|
||||
$rules['end_time'] = ['required', 'after:start_time'];
|
||||
}
|
||||
|
||||
$this->validate($rules, [
|
||||
'block_date.after_or_equal' => __('validation.block_date_future'),
|
||||
'end_time.after' => __('validation.end_time_after_start'),
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'block_date' => $this->block_date,
|
||||
'start_time' => $this->is_all_day ? null : $this->start_time,
|
||||
'end_time' => $this->is_all_day ? null : $this->end_time,
|
||||
'reason' => $this->reason ?: null,
|
||||
];
|
||||
|
||||
if ($this->editingId) {
|
||||
$blockedTime = BlockedTime::findOrFail($this->editingId);
|
||||
$oldValues = $blockedTime->toArray();
|
||||
$blockedTime->update($data);
|
||||
$action = 'update';
|
||||
} else {
|
||||
$blockedTime = BlockedTime::create($data);
|
||||
$oldValues = null;
|
||||
$action = 'create';
|
||||
}
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => $action,
|
||||
'target_type' => 'blocked_time',
|
||||
'target_id' => $blockedTime->id,
|
||||
'old_values' => $oldValues,
|
||||
'new_values' => $data,
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->closeModal();
|
||||
session()->flash('success', __('messages.blocked_time_saved'));
|
||||
}
|
||||
|
||||
public function confirmDelete(int $id): void
|
||||
{
|
||||
$this->deletingId = $id;
|
||||
$this->showDeleteModal = true;
|
||||
}
|
||||
|
||||
public function closeDeleteModal(): void
|
||||
{
|
||||
$this->showDeleteModal = false;
|
||||
$this->deletingId = null;
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
$blockedTime = BlockedTime::findOrFail($this->deletingId);
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'delete',
|
||||
'target_type' => 'blocked_time',
|
||||
'target_id' => $blockedTime->id,
|
||||
'old_values' => $blockedTime->toArray(),
|
||||
'new_values' => null,
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$blockedTime->delete();
|
||||
|
||||
$this->closeDeleteModal();
|
||||
session()->flash('success', __('messages.blocked_time_deleted'));
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$query = BlockedTime::query()->orderBy('block_date');
|
||||
|
||||
if ($this->filter === 'upcoming') {
|
||||
$query->upcoming();
|
||||
} elseif ($this->filter === 'past') {
|
||||
$query->past()->orderByDesc('block_date');
|
||||
}
|
||||
|
||||
return [
|
||||
'blockedTimes' => $query->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('admin.blocked_times') }}</flux:heading>
|
||||
<flux:text class="mt-1 text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('admin.blocked_times_description') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<flux:button variant="primary" wire:click="openCreateModal" icon="plus">
|
||||
{{ __('admin.add_blocked_time') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="mb-6">
|
||||
<flux:callout variant="success" icon="check-circle">
|
||||
{{ session('success') }}
|
||||
</flux:callout>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="mb-6">
|
||||
<flux:select wire:model.live="filter" class="w-48">
|
||||
<option value="upcoming">{{ __('admin.upcoming') }}</option>
|
||||
<option value="past">{{ __('admin.past') }}</option>
|
||||
<option value="all">{{ __('admin.all') }}</option>
|
||||
</flux:select>
|
||||
</div>
|
||||
|
||||
{{-- List --}}
|
||||
<div class="rounded-lg border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-800">
|
||||
@forelse($blockedTimes as $blocked)
|
||||
<div
|
||||
wire:key="blocked-{{ $blocked->id }}"
|
||||
class="flex flex-col gap-4 border-b border-zinc-200 p-4 last:border-b-0 dark:border-zinc-700 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:badge
|
||||
color="{{ $blocked->block_date->isPast() ? 'zinc' : 'amber' }}"
|
||||
size="sm"
|
||||
>
|
||||
{{ $blocked->block_date->format('d/m/Y') }}
|
||||
</flux:badge>
|
||||
|
||||
@if ($blocked->isAllDay())
|
||||
<flux:badge color="red" size="sm">
|
||||
{{ __('admin.all_day') }}
|
||||
</flux:badge>
|
||||
@else
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ $blocked->start_time }} - {{ $blocked->end_time }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($blocked->reason)
|
||||
<p class="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $blocked->reason }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:button size="sm" wire:click="openEditModal({{ $blocked->id }})">
|
||||
{{ __('common.edit') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
wire:click="confirmDelete({{ $blocked->id }})"
|
||||
>
|
||||
{{ __('common.delete') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-8 text-center">
|
||||
<flux:icon name="calendar-days" class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:text class="mt-2 text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('admin.no_blocked_times') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- Create/Edit Modal --}}
|
||||
<flux:modal wire:model="showModal" class="w-full max-w-lg">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">
|
||||
{{ $editingId ? __('admin.edit_blocked_time') : __('admin.add_blocked_time') }}
|
||||
</flux:heading>
|
||||
|
||||
<form wire:submit="save" class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.block_date') }}</flux:label>
|
||||
<flux:input
|
||||
type="date"
|
||||
wire:model.live="block_date"
|
||||
min="{{ $editingId ? '' : today()->format('Y-m-d') }}"
|
||||
/>
|
||||
<flux:error name="block_date" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:switch wire:model.live="is_all_day" />
|
||||
<flux:label>{{ __('admin.all_day') }}</flux:label>
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
@if (! $is_all_day)
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.start_time') }}</flux:label>
|
||||
<flux:input type="time" wire:model.live="start_time" />
|
||||
<flux:error name="start_time" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.end_time') }}</flux:label>
|
||||
<flux:input type="time" wire:model.live="end_time" />
|
||||
<flux:error name="end_time" />
|
||||
</flux:field>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.reason') }} ({{ __('common.optional') }})</flux:label>
|
||||
<flux:textarea wire:model="reason" rows="2" />
|
||||
<flux:error name="reason" />
|
||||
</flux:field>
|
||||
|
||||
@if (count($pendingBookings) > 0)
|
||||
<flux:callout variant="warning" icon="exclamation-triangle">
|
||||
<flux:text>
|
||||
{{ __('messages.pending_bookings_warning', ['count' => count($pendingBookings)]) }}
|
||||
</flux:text>
|
||||
<ul class="mt-2 list-inside list-disc text-sm">
|
||||
@foreach ($pendingBookings as $booking)
|
||||
<li>{{ $booking['time'] }} - {{ $booking['client'] }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="closeModal">
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ __('common.save') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Delete Confirmation Modal --}}
|
||||
<flux:modal wire:model="showDeleteModal" class="w-full max-w-md">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">{{ __('admin.confirm_delete') }}</flux:heading>
|
||||
<flux:text>{{ __('admin.confirm_delete_blocked_time') }}</flux:text>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<flux:button wire:click="closeDeleteModal">
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button variant="danger" wire:click="delete">
|
||||
{{ __('common.delete') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
Reference in New Issue
Block a user