complete story 6.3 with qa tests

This commit is contained in:
Naser Mansour
2025-12-27 19:58:29 +02:00
parent 9c9bef0b25
commit 07fc38de8d
12 changed files with 1135 additions and 36 deletions
@@ -421,6 +421,33 @@ new class extends Component
</div>
</div>
{{-- Quick Actions Panel Section --}}
<div class="mt-8">
<flux:heading size="lg" class="mb-6">{{ __('widgets.quick_actions') }}</flux:heading>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
{{-- Quick Actions Panel --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800 lg:col-span-3">
<livewire:admin.widgets.quick-actions />
</div>
{{-- Pending Bookings Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
<livewire:admin.widgets.pending-bookings />
</div>
{{-- Today's Schedule Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
<livewire:admin.widgets.todays-schedule />
</div>
{{-- Recent Updates Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
<livewire:admin.widgets.recent-updates />
</div>
</div>
</div>
@script
<script>
// Ensure Chart.js is available
@@ -0,0 +1,50 @@
<?php
use App\Models\Consultation;
use Livewire\Volt\Component;
new class extends Component
{
public function with(): array
{
return [
'pendingCount' => Consultation::pending()->count(),
'pendingBookings' => Consultation::pending()
->with('user:id,full_name')
->latest()
->take(5)
->get(),
];
}
}; ?>
<div wire:poll.30s>
<div class="mb-4 flex items-center justify-between">
<flux:heading size="sm">{{ __('widgets.pending_bookings') }}</flux:heading>
@if ($pendingCount > 0)
<flux:badge color="red">{{ $pendingCount > 99 ? '99+' : $pendingCount }}</flux:badge>
@endif
</div>
@forelse ($pendingBookings as $booking)
<div wire:key="pending-{{ $booking->id }}" class="border-b border-zinc-100 py-2 last:border-0 dark:border-zinc-700">
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $booking->user?->full_name ?? __('widgets.unknown_client') }}</div>
<div class="flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
<span>{{ $booking->booking_date->translatedFormat('M j') }}</span>
<flux:badge size="sm">{{ $booking->consultation_type->label() }}</flux:badge>
</div>
</div>
@empty
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('widgets.no_pending_bookings') }}</flux:text>
@endforelse
@if ($pendingCount > 5)
<a
href="{{ route('admin.bookings.pending') }}"
class="mt-4 block text-sm text-blue-600 hover:underline dark:text-blue-400"
wire:navigate
>
{{ __('widgets.view_all_pending', ['count' => $pendingCount]) }}
</a>
@endif
</div>
@@ -0,0 +1,117 @@
<?php
use App\Models\BlockedTime;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new class extends Component
{
public bool $showBlockModal = false;
#[Validate('required|date|after_or_equal:today')]
public string $blockDate = '';
#[Validate('required')]
public string $blockStartTime = '';
#[Validate('required|after:blockStartTime')]
public string $blockEndTime = '';
public string $blockReason = '';
public function openBlockModal(): void
{
$this->blockDate = today()->format('Y-m-d');
$this->blockStartTime = '09:00';
$this->blockEndTime = '10:00';
$this->blockReason = '';
$this->resetValidation();
$this->showBlockModal = true;
}
public function closeBlockModal(): void
{
$this->showBlockModal = false;
$this->reset(['blockDate', 'blockStartTime', 'blockEndTime', 'blockReason']);
$this->resetValidation();
}
public function blockTimeSlot(): void
{
$this->validate();
BlockedTime::create([
'block_date' => $this->blockDate,
'start_time' => $this->blockStartTime,
'end_time' => $this->blockEndTime,
'reason' => $this->blockReason ?: null,
]);
$this->closeBlockModal();
session()->flash('block_success', __('widgets.time_slot_blocked'));
}
}; ?>
<div>
<div class="flex flex-wrap gap-3">
<flux:button href="{{ route('admin.clients.individual.create') }}" variant="primary" icon="user-plus" wire:navigate>
{{ __('widgets.create_client') }}
</flux:button>
<flux:button href="{{ route('admin.posts.create') }}" icon="document-plus" wire:navigate>
{{ __('widgets.create_post') }}
</flux:button>
<flux:button wire:click="openBlockModal" icon="clock">
{{ __('widgets.block_time_slot') }}
</flux:button>
</div>
@if (session('block_success'))
<div class="mt-4">
<flux:callout variant="success" icon="check-circle">
{{ session('block_success') }}
</flux:callout>
</div>
@endif
<flux:modal wire:model="showBlockModal" class="w-full max-w-md">
<div class="space-y-6">
<flux:heading size="lg">{{ __('widgets.block_time_slot') }}</flux:heading>
<form wire:submit="blockTimeSlot" class="space-y-4">
<flux:field>
<flux:label>{{ __('widgets.date') }}</flux:label>
<flux:input type="date" wire:model="blockDate" min="{{ today()->format('Y-m-d') }}" />
<flux:error name="blockDate" />
</flux:field>
<div class="grid grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('widgets.start_time') }}</flux:label>
<flux:input type="time" wire:model="blockStartTime" />
<flux:error name="blockStartTime" />
</flux:field>
<flux:field>
<flux:label>{{ __('widgets.end_time') }}</flux:label>
<flux:input type="time" wire:model="blockEndTime" />
<flux:error name="blockEndTime" />
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('widgets.reason') }} ({{ __('common.optional') }})</flux:label>
<flux:textarea wire:model="blockReason" rows="2" />
</flux:field>
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="closeBlockModal">
{{ __('common.cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('widgets.block_slot') }}
</flux:button>
</div>
</form>
</div>
</flux:modal>
</div>
@@ -0,0 +1,45 @@
<?php
use App\Models\TimelineUpdate;
use Livewire\Volt\Component;
new class extends Component
{
public function with(): array
{
return [
'recentUpdates' => TimelineUpdate::query()
->with(['timeline:id,case_name,user_id', 'timeline.user:id,full_name'])
->latest()
->take(5)
->get(),
];
}
}; ?>
<div wire:poll.30s>
<flux:heading size="sm" class="mb-4">{{ __('widgets.recent_timeline_updates') }}</flux:heading>
@forelse ($recentUpdates as $update)
<a
wire:key="update-{{ $update->id }}"
href="{{ route('admin.timelines.show', $update->timeline_id) }}"
class="block border-b border-zinc-100 py-2 last:border-0 hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-700/50"
wire:navigate
>
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{{ Str::limit($update->update_text, 50) }}
</div>
<div class="mt-1 flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
<span>{{ $update->timeline?->case_name ?? __('widgets.unknown_case') }}</span>
<span>&bull;</span>
<span>{{ $update->timeline?->user?->full_name ?? __('widgets.unknown_client') }}</span>
</div>
<div class="mt-1 text-xs text-zinc-400 dark:text-zinc-500">
{{ $update->created_at->diffForHumans() }}
</div>
</a>
@empty
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('widgets.no_recent_updates') }}</flux:text>
@endforelse
</div>
@@ -0,0 +1,67 @@
<?php
use App\Models\Consultation;
use Livewire\Volt\Component;
new class extends Component
{
public function with(): array
{
return [
'todaySchedule' => Consultation::approved()
->whereDate('booking_date', today())
->with('user:id,full_name')
->orderBy('booking_time')
->get(),
];
}
public function markComplete(int $consultationId): void
{
$consultation = Consultation::findOrFail($consultationId);
$consultation->markAsCompleted();
}
public function markNoShow(int $consultationId): void
{
$consultation = Consultation::findOrFail($consultationId);
$consultation->markAsNoShow();
}
}; ?>
<div wire:poll.30s>
<flux:heading size="sm" class="mb-4">{{ __('widgets.todays_schedule') }}</flux:heading>
@forelse ($todaySchedule as $consultation)
<div wire:key="schedule-{{ $consultation->id }}" class="flex items-center justify-between border-b border-zinc-100 py-2 last:border-0 dark:border-zinc-700">
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
</div>
<div class="flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
<span>{{ $consultation->user?->full_name ?? __('widgets.unknown_client') }}</span>
<flux:badge size="sm">{{ $consultation->consultation_type->label() }}</flux:badge>
</div>
</div>
<div class="flex gap-2">
<flux:button
size="xs"
wire:click="markComplete({{ $consultation->id }})"
wire:loading.attr="disabled"
>
{{ __('widgets.complete') }}
</flux:button>
<flux:button
size="xs"
variant="danger"
wire:click="markNoShow({{ $consultation->id }})"
wire:loading.attr="disabled"
>
{{ __('widgets.no_show') }}
</flux:button>
</div>
</div>
@empty
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('widgets.no_consultations_today') }}</flux:text>
@endforelse
</div>