613 lines
30 KiB
PHP
613 lines
30 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\ConsultationType;
|
|
use App\Enums\PostStatus;
|
|
use App\Enums\TimelineStatus;
|
|
use App\Enums\UserStatus;
|
|
use App\Enums\UserType;
|
|
use App\Models\Consultation;
|
|
use App\Models\Post;
|
|
use App\Models\Timeline;
|
|
use App\Models\TimelineUpdate;
|
|
use App\Models\User;
|
|
use App\Services\AnalyticsService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Livewire\Volt\Component;
|
|
|
|
new class extends Component
|
|
{
|
|
public string $chartPeriod = '6m';
|
|
|
|
public ?string $customStartMonth = null;
|
|
|
|
public ?string $customEndMonth = null;
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return __('admin_metrics.title');
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'userMetrics' => $this->getUserMetrics(),
|
|
'bookingMetrics' => $this->getBookingMetrics(),
|
|
'timelineMetrics' => $this->getTimelineMetrics(),
|
|
'postMetrics' => $this->getPostMetrics(),
|
|
'chartData' => $this->getChartData(),
|
|
];
|
|
}
|
|
|
|
public function updatedChartPeriod(): void
|
|
{
|
|
// Reset custom range when switching to preset
|
|
if ($this->chartPeriod !== 'custom') {
|
|
$this->customStartMonth = null;
|
|
$this->customEndMonth = null;
|
|
}
|
|
}
|
|
|
|
public function setCustomRange(): void
|
|
{
|
|
if ($this->customStartMonth && $this->customEndMonth) {
|
|
$this->chartPeriod = 'custom';
|
|
}
|
|
}
|
|
|
|
private function getChartData(): array
|
|
{
|
|
$service = app(AnalyticsService::class);
|
|
|
|
$months = match ($this->chartPeriod) {
|
|
'6m' => 6,
|
|
'12m' => 12,
|
|
'custom' => $this->getCustomMonthCount(),
|
|
default => 6,
|
|
};
|
|
|
|
$startDate = $this->chartPeriod === 'custom' && $this->customStartMonth
|
|
? Carbon::parse($this->customStartMonth)->startOfMonth()
|
|
: now()->subMonths($months - 1)->startOfMonth();
|
|
|
|
return [
|
|
'labels' => $service->getMonthLabels($startDate, $months),
|
|
'newClients' => $service->getMonthlyNewClients($startDate, $months),
|
|
'consultations' => $service->getMonthlyConsultations($startDate, $months),
|
|
'consultationBreakdown' => $service->getConsultationTypeBreakdown($startDate, $months),
|
|
'noShowRates' => $service->getMonthlyNoShowRates($startDate, $months),
|
|
];
|
|
}
|
|
|
|
private function getCustomMonthCount(): int
|
|
{
|
|
if (! $this->customStartMonth || ! $this->customEndMonth) {
|
|
return 6;
|
|
}
|
|
|
|
return Carbon::parse($this->customStartMonth)
|
|
->diffInMonths(Carbon::parse($this->customEndMonth)) + 1;
|
|
}
|
|
|
|
private function getUserMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.users', 300, fn () => [
|
|
'total_active' => User::query()
|
|
->where('status', UserStatus::Active)
|
|
->whereIn('user_type', [UserType::Individual, UserType::Company])
|
|
->count(),
|
|
'individual' => User::query()
|
|
->where('user_type', UserType::Individual)
|
|
->where('status', UserStatus::Active)
|
|
->count(),
|
|
'company' => User::query()
|
|
->where('user_type', UserType::Company)
|
|
->where('status', UserStatus::Active)
|
|
->count(),
|
|
'deactivated' => User::query()
|
|
->where('status', UserStatus::Deactivated)
|
|
->whereIn('user_type', [UserType::Individual, UserType::Company])
|
|
->count(),
|
|
'new_this_month' => User::query()
|
|
->whereIn('user_type', [UserType::Individual, UserType::Company])
|
|
->whereMonth('created_at', now()->month)
|
|
->whereYear('created_at', now()->year)
|
|
->count(),
|
|
]);
|
|
}
|
|
|
|
private function getBookingMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.bookings', 300, function () {
|
|
$total = Consultation::query()
|
|
->whereIn('status', [ConsultationStatus::Completed, ConsultationStatus::NoShow])
|
|
->count();
|
|
$noShows = Consultation::query()
|
|
->where('status', ConsultationStatus::NoShow)
|
|
->count();
|
|
|
|
return [
|
|
'pending' => Consultation::query()
|
|
->where('status', ConsultationStatus::Pending)
|
|
->count(),
|
|
'today' => Consultation::query()
|
|
->whereDate('booking_date', today())
|
|
->where('status', ConsultationStatus::Approved)
|
|
->count(),
|
|
'this_week' => Consultation::query()
|
|
->whereBetween('booking_date', [now()->startOfWeek(), now()->endOfWeek()])
|
|
->whereIn('status', [ConsultationStatus::Approved, ConsultationStatus::Pending])
|
|
->count(),
|
|
'this_month' => Consultation::query()
|
|
->whereMonth('booking_date', now()->month)
|
|
->whereYear('booking_date', now()->year)
|
|
->count(),
|
|
'free' => Consultation::query()
|
|
->where('consultation_type', ConsultationType::Free)
|
|
->count(),
|
|
'paid' => Consultation::query()
|
|
->where('consultation_type', ConsultationType::Paid)
|
|
->count(),
|
|
'no_show_rate' => $total > 0 ? round(($noShows / $total) * 100, 1) : 0,
|
|
];
|
|
});
|
|
}
|
|
|
|
private function getTimelineMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.timelines', 300, fn () => [
|
|
'active' => Timeline::query()
|
|
->where('status', TimelineStatus::Active)
|
|
->count(),
|
|
'archived' => Timeline::query()
|
|
->where('status', TimelineStatus::Archived)
|
|
->count(),
|
|
'updates_this_week' => TimelineUpdate::query()
|
|
->where('created_at', '>=', now()->subWeek())
|
|
->count(),
|
|
]);
|
|
}
|
|
|
|
private function getPostMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.posts', 300, fn () => [
|
|
'total_published' => Post::query()
|
|
->where('status', PostStatus::Published)
|
|
->count(),
|
|
'this_month' => Post::query()
|
|
->where('status', PostStatus::Published)
|
|
->whereMonth('published_at', now()->month)
|
|
->whereYear('published_at', now()->year)
|
|
->count(),
|
|
]);
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<div class="mb-6">
|
|
<flux:heading size="xl">{{ __('admin_metrics.title') }}</flux:heading>
|
|
<flux:text class="mt-1 text-zinc-500 dark:text-zinc-400">{{ __('admin_metrics.subtitle') }}</flux:text>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
|
|
{{-- User Metrics Card --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<div class="mb-4 flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-[#0A1F44] text-white dark:bg-[#D4AF37] dark:text-zinc-900">
|
|
<flux:icon name="users" class="h-5 w-5" />
|
|
</div>
|
|
<flux:heading size="lg">{{ __('admin_metrics.clients') }}</flux:heading>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.total_active') }}</flux:text>
|
|
<span class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{{ $userMetrics['total_active'] }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.individual') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $userMetrics['individual'] }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.company') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $userMetrics['company'] }}</span>
|
|
</div>
|
|
<div class="border-t border-zinc-200 pt-3 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.deactivated') }}</flux:text>
|
|
<span class="font-medium text-zinc-500 dark:text-zinc-400">{{ $userMetrics['deactivated'] }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.new_this_month') }}</flux:text>
|
|
<flux:badge color="lime" size="sm">{{ $userMetrics['new_this_month'] }}</flux:badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Booking Metrics Card --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<div class="mb-4 flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-[#0A1F44] text-white dark:bg-[#D4AF37] dark:text-zinc-900">
|
|
<flux:icon name="calendar" class="h-5 w-5" />
|
|
</div>
|
|
<flux:heading size="lg">{{ __('admin_metrics.consultations') }}</flux:heading>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.pending_requests') }}</flux:text>
|
|
<flux:badge color="amber" size="sm">{{ $bookingMetrics['pending'] }}</flux:badge>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.today') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['today'] }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.this_week') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['this_week'] }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.this_month') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['this_month'] }}</span>
|
|
</div>
|
|
<div class="border-t border-zinc-200 pt-3 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.free') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['free'] }}</span>
|
|
</div>
|
|
<div class="mt-2 flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.paid') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['paid'] }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.no_show_rate') }}</flux:text>
|
|
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $bookingMetrics['no_show_rate'] }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Timeline Metrics Card --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<div class="mb-4 flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-[#0A1F44] text-white dark:bg-[#D4AF37] dark:text-zinc-900">
|
|
<flux:icon name="clipboard-document-list" class="h-5 w-5" />
|
|
</div>
|
|
<flux:heading size="lg">{{ __('admin_metrics.timelines') }}</flux:heading>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.active_cases') }}</flux:text>
|
|
<span class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{{ $timelineMetrics['active'] }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.archived') }}</flux:text>
|
|
<span class="font-medium text-zinc-500 dark:text-zinc-400">{{ $timelineMetrics['archived'] }}</span>
|
|
</div>
|
|
<div class="border-t border-zinc-200 pt-3 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.updates_this_week') }}</flux:text>
|
|
<flux:badge color="lime" size="sm">{{ $timelineMetrics['updates_this_week'] }}</flux:badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Posts Metrics Card --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<div class="mb-4 flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-[#0A1F44] text-white dark:bg-[#D4AF37] dark:text-zinc-900">
|
|
<flux:icon name="document-text" class="h-5 w-5" />
|
|
</div>
|
|
<flux:heading size="lg">{{ __('admin_metrics.posts') }}</flux:heading>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.total_published') }}</flux:text>
|
|
<span class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">{{ $postMetrics['total_published'] }}</span>
|
|
</div>
|
|
<div class="border-t border-zinc-200 pt-3 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<flux:text class="text-zinc-600 dark:text-zinc-400">{{ __('admin_metrics.published_this_month') }}</flux:text>
|
|
<flux:badge color="lime" size="sm">{{ $postMetrics['this_month'] }}</flux:badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Analytics Charts Section --}}
|
|
<div class="mt-8">
|
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<flux:heading size="lg">{{ __('admin_metrics.analytics_charts') }}</flux:heading>
|
|
|
|
{{-- Date Range Selector --}}
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<flux:button
|
|
wire:click="$set('chartPeriod', '6m')"
|
|
:variant="$chartPeriod === '6m' ? 'primary' : 'ghost'"
|
|
size="sm"
|
|
>
|
|
{{ __('admin_metrics.last_6_months') }}
|
|
</flux:button>
|
|
<flux:button
|
|
wire:click="$set('chartPeriod', '12m')"
|
|
:variant="$chartPeriod === '12m' ? 'primary' : 'ghost'"
|
|
size="sm"
|
|
>
|
|
{{ __('admin_metrics.last_12_months') }}
|
|
</flux:button>
|
|
|
|
{{-- Custom Range --}}
|
|
<div class="flex items-center gap-2">
|
|
<flux:input
|
|
type="month"
|
|
wire:model="customStartMonth"
|
|
class="w-36"
|
|
size="sm"
|
|
/>
|
|
<span class="text-zinc-500">-</span>
|
|
<flux:input
|
|
type="month"
|
|
wire:model="customEndMonth"
|
|
class="w-36"
|
|
size="sm"
|
|
/>
|
|
<flux:button
|
|
wire:click="setCustomRange"
|
|
:variant="$chartPeriod === 'custom' ? 'primary' : 'ghost'"
|
|
size="sm"
|
|
>
|
|
{{ __('admin_metrics.apply') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
{{-- Monthly Trends Chart --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<flux:heading size="sm" class="mb-4">{{ __('admin_metrics.monthly_trends') }}</flux:heading>
|
|
@if (array_sum($chartData['newClients']) === 0 && array_sum($chartData['consultations']) === 0)
|
|
<div class="flex h-64 items-center justify-center">
|
|
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('admin_metrics.no_data_available') }}</flux:text>
|
|
</div>
|
|
@else
|
|
<div
|
|
wire:ignore
|
|
x-data="{
|
|
chart: null,
|
|
data: @js($chartData),
|
|
init() {
|
|
if (typeof Chart === 'undefined') return;
|
|
this.chart = new Chart(this.$refs.canvas, {
|
|
type: 'line',
|
|
data: {
|
|
labels: this.data.labels,
|
|
datasets: [
|
|
{
|
|
label: @js(__('admin_metrics.new_clients')),
|
|
data: this.data.newClients,
|
|
borderColor: '#D4AF37',
|
|
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
|
tension: 0.3,
|
|
fill: false,
|
|
},
|
|
{
|
|
label: @js(__('admin_metrics.consultations')),
|
|
data: this.data.consultations,
|
|
borderColor: '#0A1F44',
|
|
backgroundColor: 'rgba(10, 31, 68, 0.1)',
|
|
tension: 0.3,
|
|
fill: false,
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
tooltip: { enabled: true },
|
|
legend: {
|
|
position: 'bottom',
|
|
rtl: document.dir === 'rtl',
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: { precision: 0 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
destroy() {
|
|
if (this.chart) this.chart.destroy();
|
|
}
|
|
}"
|
|
class="h-64"
|
|
>
|
|
<canvas x-ref="canvas"></canvas>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Consultation Breakdown Chart --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
|
<flux:heading size="sm" class="mb-4">{{ __('admin_metrics.consultation_breakdown') }}</flux:heading>
|
|
@if ($chartData['consultationBreakdown']['free'] === 0 && $chartData['consultationBreakdown']['paid'] === 0)
|
|
<div class="flex h-64 items-center justify-center">
|
|
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('admin_metrics.no_data_available') }}</flux:text>
|
|
</div>
|
|
@else
|
|
<div
|
|
wire:ignore
|
|
x-data="{
|
|
chart: null,
|
|
data: @js($chartData['consultationBreakdown']),
|
|
init() {
|
|
if (typeof Chart === 'undefined') return;
|
|
const total = this.data.free + this.data.paid;
|
|
this.chart = new Chart(this.$refs.canvas, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [
|
|
@js(__('admin_metrics.free')) + ` (${this.data.free})`,
|
|
@js(__('admin_metrics.paid')) + ` (${this.data.paid})`
|
|
],
|
|
datasets: [{
|
|
data: [this.data.free, this.data.paid],
|
|
backgroundColor: ['#D4AF37', '#0A1F44'],
|
|
borderWidth: 0,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const value = context.parsed;
|
|
const percent = total > 0 ? Math.round((value / total) * 100) : 0;
|
|
return `${context.label}: ${percent}%`;
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
rtl: document.dir === 'rtl',
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
destroy() {
|
|
if (this.chart) this.chart.destroy();
|
|
}
|
|
}"
|
|
class="h-64"
|
|
>
|
|
<canvas x-ref="canvas"></canvas>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- No-show Rate Chart --}}
|
|
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800 lg:col-span-2">
|
|
<flux:heading size="sm" class="mb-4">{{ __('admin_metrics.noshow_rate_trend') }}</flux:heading>
|
|
@if (array_sum($chartData['noShowRates']) === 0)
|
|
<div class="flex h-64 items-center justify-center">
|
|
<flux:text class="text-zinc-500 dark:text-zinc-400">{{ __('admin_metrics.no_data_available') }}</flux:text>
|
|
</div>
|
|
@else
|
|
<div
|
|
wire:ignore
|
|
x-data="{
|
|
chart: null,
|
|
data: @js($chartData),
|
|
init() {
|
|
if (typeof Chart === 'undefined') return;
|
|
this.chart = new Chart(this.$refs.canvas, {
|
|
type: 'line',
|
|
data: {
|
|
labels: this.data.labels,
|
|
datasets: [{
|
|
label: @js(__('admin_metrics.noshow_rate_percent')),
|
|
data: this.data.noShowRates,
|
|
borderColor: '#E74C3C',
|
|
backgroundColor: 'rgba(231, 76, 60, 0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.parsed.y + '%';
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
rtl: document.dir === 'rtl',
|
|
},
|
|
annotation: {
|
|
annotations: {
|
|
threshold: {
|
|
type: 'line',
|
|
yMin: 20,
|
|
yMax: 20,
|
|
borderColor: 'rgba(231, 76, 60, 0.5)',
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
label: {
|
|
display: true,
|
|
content: @js(__('admin_metrics.concerning_threshold')),
|
|
position: 'end'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
min: 0,
|
|
max: 100,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value + '%';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
destroy() {
|
|
if (this.chart) this.chart.destroy();
|
|
}
|
|
}"
|
|
class="h-64"
|
|
>
|
|
<canvas x-ref="canvas"></canvas>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</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>
|
|
|
|
</div>
|