libra/resources/views/livewire/admin/settings/blocked-times.blade.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>