396 lines
13 KiB
PHP
396 lines
13 KiB
PHP
<?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">
|
|
{{ __('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">
|
|
@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 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">
|
|
{{ $blocked->start_time }} - {{ $blocked->end_time }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($blocked->reason)
|
|
<p class="mt-2 text-sm text-zinc-500">
|
|
{{ $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">
|
|
{{ __('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>
|