Files
ilycats/resources/views/pages/dashboard/index.blade.php
T

473 lines
34 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
<div class="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">Beranda</h1>
<p class="mt-1 text-sm text-slate-500">Ringkasan operasional cattery dan pemantauan kondisi kucing.</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="{{ route('cats.create') }}" class="bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700">
<i class="fas fa-plus mr-2"></i>Tambah kucing
</a>
<a href="{{ route('matting.create') }}" class="border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">
Catat perkawinan
</a>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@foreach([
['Total kucing aktif',$stats['total_cats'] ?? 0,'fa-cat','cats.index'],
['Dalam perawatan',$stats['sick_cats'] ?? 0,'fa-heart-pulse','cats.index'],
['Masa kehamilan',$stats['total_mating'] ?? 0,'fa-venus-mars','matting.index'],
['Total anakan',$stats['total_kittens'] ?? 0,'fa-paw','matting.index']
] as $i=>$card)
<a href="{{ $i===1 ? route($card[3],['filter_health'=>'treatment']) : route($card[3]) }}" class="border border-slate-200 bg-white p-5 hover:border-slate-300">
<div class="flex items-start justify-between">
<div>
<p class="text-sm text-slate-500">{{ $card[0] }}</p>
<p class="mt-2 text-3xl font-bold text-slate-900">{{ $card[1] }}</p>
</div>
<i class="fas {{ $card[2] }} mt-1 text-slate-300"></i>
</div>
</a>
@endforeach
</div>
{{-- PEMANTAUAN BERAT BADAN - REVISION 3 --}}
<section id="pemantauan-berat" class="mt-6 scroll-mt-6 rounded-2xl border border-slate-200 bg-white shadow-sm">
<div class="flex flex-col gap-5 border-b border-slate-100 px-5 py-5 lg:flex-row lg:items-end lg:justify-between">
<div class="flex items-start gap-3">
<div class="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-blue-50 text-blue-600">
<i class="fas fa-weight-scale text-lg"></i>
<span class="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border-2 border-white bg-white text-[8px] text-blue-500 shadow-sm">
<i class="fas fa-paw"></i>
</span>
</div>
<div>
<h2 class="text-xl font-bold text-slate-900">Pemantauan Berat Badan</h2>
<p class="mt-1 text-sm text-slate-500">Pantau perkembangan berat badan kucing secara ringkas dan mudah dibaca.</p>
</div>
</div>
<form method="GET" action="{{ route('dashboard') }}#pemantauan-berat" class="grid gap-3 sm:grid-cols-2 lg:min-w-[470px]">
<input type="hidden" name="weight_focus" value="1">
<input type="hidden" name="trend_category" value="{{ $weightTrendCategory ?? 'Kitten' }}">
<div>
<label for="weight_months" class="mb-1.5 block text-xs font-semibold text-slate-500">Periode</label>
<div class="relative">
<i class="fas fa-calendar-days pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-slate-400"></i>
<select id="weight_months" name="weight_months" class="w-full rounded-xl border-slate-200 bg-white py-2.5 pl-9 pr-8 text-sm text-slate-700 shadow-sm focus:border-blue-400 focus:ring-blue-400" onchange="this.form.submit()">
@foreach([3=>'3 bulan',6=>'6 bulan',12=>'12 bulan',24=>'24 bulan'] as $months=>$label)
<option value="{{ $months }}" @selected(($weightTrendMonths ?? 6) == $months)>{{ $label }}</option>
@endforeach
</select>
</div>
</div>
<div>
<label for="weight_category" class="mb-1.5 block text-xs font-semibold text-slate-500">Kategori</label>
<div class="relative">
<i class="fas fa-cat pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-slate-400"></i>
<select id="weight_category" name="weight_category" class="w-full rounded-xl border-slate-200 bg-white py-2.5 pl-9 pr-8 text-sm text-slate-700 shadow-sm focus:border-blue-400 focus:ring-blue-400" onchange="this.form.submit()">
@foreach(['Semua'=>'Semua Kategori','Kitten'=>'Kitten','Junior'=>'Junior','Dewasa'=>'Dewasa'] as $value=>$label)
<option value="{{ $value }}" @selected(($weightTableCategory ?? 'Semua') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
</div>
</form>
</div>
@php
$weightCards = [
[
'label' => 'Total Ditimbang Bulan Ini',
'value' => (int) data_get($weightKpis ?? [], 'weighed.value', 0),
'suffix' => 'kucing',
'change' => (float) data_get($weightKpis ?? [], 'weighed.change', 0),
'hasComparison' => true,
'icon' => 'fa-paw',
'box' => 'bg-blue-50/70 border-blue-100',
'iconBox' => 'bg-blue-100 text-blue-600',
],
[
'label' => 'Rata-rata Kitten',
'value' => (float) data_get($weightKpis ?? [], 'categories.Kitten.average', 0),
'suffix' => 'kg',
'change' => (float) data_get($weightKpis ?? [], 'categories.Kitten.change', 0),
'hasComparison' => (bool) data_get($weightKpis ?? [], 'categories.Kitten.has_comparison', false),
'icon' => 'fa-cat',
'box' => 'bg-indigo-50/70 border-indigo-100',
'iconBox' => 'bg-indigo-100 text-indigo-600',
],
[
'label' => 'Rata-rata Junior',
'value' => (float) data_get($weightKpis ?? [], 'categories.Junior.average', 0),
'suffix' => 'kg',
'change' => (float) data_get($weightKpis ?? [], 'categories.Junior.change', 0),
'hasComparison' => (bool) data_get($weightKpis ?? [], 'categories.Junior.has_comparison', false),
'icon' => 'fa-cat',
'box' => 'bg-rose-50/70 border-rose-100',
'iconBox' => 'bg-rose-100 text-rose-600',
],
[
'label' => 'Rata-rata Dewasa',
'value' => (float) data_get($weightKpis ?? [], 'categories.Dewasa.average', 0),
'suffix' => 'kg',
'change' => (float) data_get($weightKpis ?? [], 'categories.Dewasa.change', 0),
'hasComparison' => (bool) data_get($weightKpis ?? [], 'categories.Dewasa.has_comparison', false),
'icon' => 'fa-cat',
'box' => 'bg-emerald-50/70 border-emerald-100',
'iconBox' => 'bg-emerald-100 text-emerald-600',
],
];
@endphp
<div class="grid gap-3 p-5 sm:grid-cols-2 xl:grid-cols-4">
@foreach($weightCards as $card)
@php
$isCount = $card['suffix'] === 'kucing';
$change = $card['change'];
$positive = $change >= 0;
@endphp
<div class="rounded-2xl border p-4 {{ $card['box'] }}">
<div class="flex items-start gap-3">
<div class="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl {{ $card['iconBox'] }}">
<i class="fas {{ $card['icon'] }}"></i>
<span class="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full border-2 border-white bg-white text-[6px] opacity-90"><i class="fas fa-paw"></i></span>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-slate-700">{{ $card['label'] }}</p>
<div class="mt-1 flex items-end gap-1.5">
<p class="text-3xl font-bold tracking-tight text-slate-900">
{{ $isCount ? number_format($card['value'], 0) : number_format($card['value'], 1, ',', '.') }}
</p>
<span class="mb-1 text-sm font-medium text-slate-500">{{ $card['suffix'] }}</span>
</div>
@if($card['hasComparison'])
<p class="mt-1 text-xs {{ $positive ? 'text-emerald-600' : 'text-rose-600' }}">
<i class="fas {{ $positive ? 'fa-arrow-up' : 'fa-arrow-down' }} mr-1"></i>{{ $positive ? '+' : '' }}{{ number_format($change, 1, ',', '.') }}%
<span class="text-slate-400">dari bulan lalu</span>
</p>
@else
<p class="mt-1 text-xs text-slate-400">Belum ada pembanding bulan lalu</p>
@endif
</div>
</div>
</div>
@endforeach
</div>
<div class="grid gap-4 px-5 pb-5 xl:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white p-4">
<div class="mb-4">
<h3 class="text-base font-bold text-slate-900">Rata-rata per kategori</h3>
<p class="mt-0.5 text-xs text-slate-500">Rata-rata berat terakhir setiap kucing pada masing-masing bulan.</p>
</div>
<div class="h-72 sm:h-80">
<canvas id="weightCategoryAverageChart"></canvas>
</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4">
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="text-base font-bold text-slate-900">Tren kategori terpilih</h3>
<p class="mt-0.5 text-xs text-slate-500">Maksimal 6 kucing dengan data terlengkap agar grafik tetap mudah dibaca.</p>
</div>
<div class="inline-flex rounded-xl bg-slate-100 p-1">
@foreach(['Kitten','Junior','Dewasa'] as $category)
@php
$tabQuery = array_merge(request()->except('page', 'trend_category'), [
'weight_focus' => 1,
'trend_category' => $category,
]);
@endphp
<a href="{{ route('dashboard', $tabQuery) }}#pemantauan-berat"
class="rounded-lg px-3 py-1.5 text-xs font-semibold transition {{ ($weightTrendCategory ?? 'Kitten') === $category ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:text-slate-900' }}">
{{ $category }}
</a>
@endforeach
</div>
</div>
<div class="h-72 sm:h-80">
@if(count($weightTrendSeries ?? []) > 0)
<canvas id="weightSelectedCategoryChart"></canvas>
@else
<div class="flex h-full flex-col items-center justify-center rounded-xl bg-slate-50 text-center">
<div class="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-white text-slate-300 shadow-sm"><i class="fas fa-cat text-xl"></i></div>
<p class="text-sm font-semibold text-slate-600">Belum ada tren {{ $weightTrendCategory ?? 'kategori ini' }}</p>
<p class="mt-1 text-xs text-slate-400">Tambahkan minimal satu pencatatan berat pada periode yang dipilih.</p>
</div>
@endif
</div>
</div>
</div>
<div class="border-t border-slate-100 px-5 py-5">
<div class="mb-4 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<h3 class="text-base font-bold text-slate-900">Berat Terakhir</h3>
<p class="mt-0.5 text-xs text-slate-500">Daftar berat terbaru, perubahan dari pencatatan sebelumnya, dan tanggal timbang.</p>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<label class="relative block">
<span class="sr-only">Cari nama kucing</span>
<i class="fas fa-magnifying-glass pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-slate-400"></i>
<input id="weightTableSearch" type="search" placeholder="Cari nama kucing..." class="w-full rounded-xl border-slate-200 py-2.5 pl-9 pr-3 text-sm text-slate-700 sm:w-64 focus:border-blue-400 focus:ring-blue-400">
</label>
<div class="flex items-center gap-2 text-xs text-slate-500">
<span>Tampilkan</span>
<select id="weightRowsPerPage" class="rounded-xl border-slate-200 py-2 text-sm text-slate-700 focus:border-blue-400 focus:ring-blue-400">
<option value="10" selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
<span>data</span>
</div>
@can('check-access', ['cats','update'])
<a href="{{ route('cats.bulk-weight') }}" class="inline-flex items-center justify-center rounded-xl bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-blue-700">
<i class="fas fa-weight-scale mr-2"></i>Perbarui berat
</a>
@endcan
</div>
</div>
<div class="overflow-hidden rounded-2xl border border-slate-200">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 text-sm">
<thead class="bg-slate-50/80 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr>
<th class="px-4 py-3">Nama Kucing</th>
<th class="px-4 py-3">Kategori</th>
<th class="px-4 py-3 text-right">Berat Terakhir</th>
<th class="px-4 py-3 text-right">Perubahan</th>
<th class="px-4 py-3">Tanggal</th>
</tr>
</thead>
<tbody id="weightTableBody" class="divide-y divide-slate-100 bg-white">
@forelse(($weightLatestTable ?? collect()) as $item)
@php
$photoUrl = $item['photo'] ?
\Illuminate\Support\Facades\Storage::url($item['photo']) : asset('images/default-cat.png');
$change = $item['change'];
@endphp
<tr data-weight-row data-name="{{ strtolower($item['name']) }}" class="hover:bg-slate-50/70">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<img src="{{ $photoUrl }}" alt="Foto {{ $item['name'] }}" class="h-9 w-9 rounded-full border border-slate-200 object-cover" onerror="this.src='{{ asset('images/default-cat.png') }}'">
<div class="min-w-0">
<p class="truncate font-semibold text-slate-800">{{ $item['name'] }}</p>
<p class="text-xs text-slate-400">{{ $item['gender'] ?: '-' }}</p>
</div>
</div>
</td>
<td class="px-4 py-3">
@php
$badge = match($item['category']) {
'Kitten' => 'bg-blue-50 text-blue-700 ring-blue-100',
'Junior' => 'bg-rose-50 text-rose-700 ring-rose-100',
default => 'bg-emerald-50 text-emerald-700 ring-emerald-100',
};
@endphp
<span class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset {{ $badge }}">{{ $item['category'] }}</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-right font-bold text-slate-800">{{ number_format($item['latest'], 2, ',', '.') }} kg</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
@if($change === null)
<span class="text-xs text-slate-400">Belum ada pembanding</span>
@elseif($change > 0)
<span class="font-semibold text-emerald-600"><i class="fas fa-arrow-up mr-1 text-xs"></i>+{{ number_format($change, 2, ',', '.') }} kg</span>
@elseif($change < 0)
<span class="font-semibold text-rose-600"><i class="fas fa-arrow-down mr-1 text-xs"></i>{{ number_format($change, 2, ',', '.') }} kg</span>
@else
<span class="font-semibold text-slate-500"><i class="fas fa-minus mr-1 text-xs"></i>0,00 kg</span>
@endif
</td>
<td class="whitespace-nowrap px-4 py-3 text-slate-500">{{ \Carbon\Carbon::parse($item['date'])->translatedFormat('d M Y') }}</td>
</tr>
@empty
<tr data-empty-row><td colspan="5" class="px-4 py-10 text-center text-sm text-slate-500">Belum ada data berat pada periode yang dipilih.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="flex flex-col gap-3 border-t border-slate-100 bg-slate-50/40 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<p id="weightTableInfo" class="text-xs text-slate-500"></p>
<div class="flex items-center gap-2">
<button id="weightPrevPage" type="button" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40">Sebelumnya</button>
<span id="weightPageLabel" class="min-w-20 text-center text-xs font-semibold text-slate-600"></span>
<button id="weightNextPage" type="button" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40">Berikutnya</button>
</div>
</div>
</div>
</div>
<div class="border-t border-slate-100 px-5 pb-5 pt-5">
<div class="mb-4 flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
<div><h3 class="text-base font-bold text-slate-900">Perubahan berat yang perlu diperhatikan</h3><p class="mt-0.5 text-xs text-slate-500">Kenaikan dan penurunan berdasarkan dua pencatatan terakhir dalam periode {{ $weightPeriodLabel ?? '-' }}.</p></div>
</div>
<div class="grid gap-4 xl:grid-cols-3">
@foreach(['Kitten','Junior','Dewasa'] as $category)
@php $movement = data_get($weightMovements ?? [], $category, ['gainers'=>collect(),'losers'=>collect()]); @endphp
<div class="rounded-2xl border border-slate-200 bg-slate-50/40 p-4">
<h4 class="mb-3 font-bold text-slate-800">{{ $category }}</h4>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
<div class="rounded-xl border border-emerald-100 bg-emerald-50/70 p-3"><p class="mb-2 text-xs font-bold text-emerald-700"><i class="fas fa-arrow-trend-up mr-1"></i>Kenaikan</p><div class="space-y-2">@forelse($movement['gainers'] as $row)<div class="flex items-center justify-between gap-3 text-xs"><a href="{{ route('cats.show',$row['cat_id']) }}" class="truncate font-semibold text-slate-700 hover:text-indigo-600">{{ $row['name'] }}</a><span class="shrink-0 font-bold text-emerald-700">+{{ number_format($row['change'],2,',','.') }} kg</span></div>@empty<p class="text-xs text-slate-400">Tidak ada kenaikan tercatat.</p>@endforelse</div></div>
<div class="rounded-xl border border-rose-100 bg-rose-50/70 p-3"><p class="mb-2 text-xs font-bold text-rose-700"><i class="fas fa-arrow-trend-down mr-1"></i>Penurunan</p><div class="space-y-2">@forelse($movement['losers'] as $row)<div class="flex items-center justify-between gap-3 text-xs"><a href="{{ route('cats.show',$row['cat_id']) }}" class="truncate font-semibold text-slate-700 hover:text-indigo-600">{{ $row['name'] }}</a><span class="shrink-0 font-bold text-rose-700">{{ number_format($row['change'],2,',','.') }} kg</span></div>@empty<p class="text-xs text-slate-400">Tidak ada penurunan tercatat.</p>@endforelse</div></div>
</div>
</div>
@endforeach
</div>
</div>
</section>
<div class="mt-6 grid gap-6 xl:grid-cols-3">
<section class="border border-slate-200 bg-white xl:col-span-2">
<div class="flex items-center justify-between border-b border-slate-200 px-5 py-4">
<div>
<h2 class="font-semibold text-slate-900">Perkiraan kelahiran</h2>
<p class="text-xs text-slate-500">Perkawinan aktif dengan perkiraan lahir sekitar 63 hari setelah kawin.</p>
</div>
<a href="{{ route('matting.index') }}" class="text-sm font-medium text-indigo-600">Lihat semua</a>
</div>
<div class="divide-y divide-slate-100">
@forelse($upcomingBirths as $mating)
<a href="{{ route('matting.edit',$mating->id) }}" class="grid gap-3 px-5 py-4 hover:bg-slate-50 sm:grid-cols-[1fr_auto] sm:items-center">
<div>
<p class="font-semibold text-slate-800">{{ $mating->dam->nama_kucing ?? 'Betina' }} <span class="font-normal text-slate-400">×</span> {{ $mating->sire->nama_kucing ?? 'Jantan' }}</p>
<p class="mt-1 text-sm text-slate-500">Perkiraan lahir {{ $mating->edd?->translatedFormat('d M Y') }}</p>
</div>
<span class="text-sm font-semibold {{ $mating->days_to_go <= 2 ? 'text-rose-600' : 'text-slate-600' }}">{{ $mating->days_to_go < 0 ? 'Lewat '.abs($mating->days_to_go).' hari' : $mating->days_to_go.' hari lagi' }}</span>
</a>
@empty
<div class="px-5 py-10 text-center text-sm text-slate-500">Belum ada perkiraan kelahiran dalam waktu dekat.</div>
@endforelse
</div>
</section>
<section class="border border-slate-200 bg-white">
<div class="border-b border-slate-200 px-5 py-4">
<h2 class="font-semibold text-slate-900">Jadwal yang terlambat</h2>
<p class="text-xs text-slate-500">Perlu ditindaklanjuti dari data kucing.</p>
</div>
<div class="grid grid-cols-2 gap-px bg-slate-200">
@php $labels=['vaccine'=>'Vaksin','flea'=>'Obat kutu','deworming'=>'Obat cacing','grooming'=>'Grooming']; @endphp
@foreach($overdueAlerts as $key=>$data)
<a href="{{ $data['url'] }}" class="bg-white p-4 hover:bg-slate-50">
<p class="text-xs text-slate-500">{{ $labels[$key] ?? $key }}</p>
<p class="mt-1 text-2xl font-bold {{ $data['count']>0?'text-rose-600':'text-slate-800' }}">{{ $data['count'] }}</p>
</a>
@endforeach
</div>
</section>
</div>
<div class="mt-6 grid gap-6 xl:grid-cols-2">
<section class="border border-slate-200 bg-white">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Stok menipis</h2></div>
<div class="divide-y divide-slate-100">
@forelse(($lowStock ?? collect())->take(8) as $item)
<a href="{{ $item->url ?? '#' }}" class="flex justify-between px-5 py-3 text-sm hover:bg-slate-50">
<span class="truncate pr-4">{{ $item->name }}</span>
<span class="font-semibold text-rose-600">{{ (float)$item->qty }} {{ $item->unit }}</span>
</a>
@empty
<div class="p-5 text-sm text-slate-500">Tidak ada stok kritis.</div>
@endforelse
</div>
</section>
<section class="border border-slate-200 bg-white">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Mendekati kedaluwarsa</h2></div>
<div class="divide-y divide-slate-100">
@forelse($expiringItems as $item)
<a href="{{ $item->url ?? '#' }}" class="flex items-center justify-between gap-4 px-5 py-3 text-sm hover:bg-slate-50">
<div class="min-w-0">
<p class="truncate font-medium">{{ $item->name }}</p>
<p class="text-xs {{ $item->is_expired?'text-rose-600':'text-amber-600' }}">{{ $item->is_expired?'Sudah kedaluwarsa':$item->days_left.' hari lagi' }} · {{ $item->formatted_date }}</p>
</div>
<span class="shrink-0 text-xs text-slate-500">{{ (float)$item->qty }} {{ $item->unit }}</span>
</a>
@empty
<div class="p-5 text-sm text-slate-500">Tidak ada peringatan kedaluwarsa.</div>
@endforelse
</div>
</section>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const formatKg = (value) => `${Number(value).toFixed(2).replace('.', ',')} kg`;
const formatMonth = (month) => {
const d = new Date(`${month}-01T00:00:00`);
return Number.isNaN(d.getTime()) ? month : d.toLocaleDateString('id-ID', { month: 'short', year: '2-digit' });
};
function renderLineChart(canvas, labels, datasets) {
if (!canvas || !labels.length || !datasets.length) return;
const ctx = canvas.getContext('2d');
const parent = canvas.parentElement;
const cssWidth = Math.max(320, parent.clientWidth);
const cssHeight = Math.max(250, parent.clientHeight);
const dpr = window.devicePixelRatio || 1;
canvas.width = cssWidth * dpr; canvas.height = cssHeight * dpr;
canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect(0,0,cssWidth,cssHeight);
const pad = {left:48,right:16,top:12,bottom:58};
const w = cssWidth-pad.left-pad.right, h = cssHeight-pad.top-pad.bottom;
const values = datasets.flatMap(d => d.data.filter(v => v !== null && v !== undefined).map(Number));
if (!values.length) return;
const maxY = Math.max(1, Math.ceil(Math.max(...values)*1.15*2)/2);
const minY = 0;
const xAt = i => pad.left + (labels.length<=1 ? w/2 : (i/(labels.length-1))*w);
const yAt = v => pad.top + h - ((Number(v)-minY)/(maxY-minY))*h;
ctx.font='11px system-ui, -apple-system, Segoe UI, sans-serif'; ctx.textBaseline='middle';
for(let i=0;i<=4;i++){
const v=maxY*(i/4), y=yAt(v);
ctx.strokeStyle='#e2e8f0'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(pad.left,y); ctx.lineTo(pad.left+w,y); ctx.stroke();
ctx.fillStyle='#64748b'; ctx.textAlign='right'; ctx.fillText(v.toFixed(v<1?1:1),pad.left-8,y);
}
const step=Math.max(1,Math.ceil(labels.length/6));
labels.forEach((lab,i)=>{ if(i%step!==0 && i!==labels.length-1) return; ctx.fillStyle='#64748b'; ctx.textAlign='center'; ctx.fillText(formatMonth(lab),xAt(i),pad.top+h+20); });
datasets.forEach(ds=>{
ctx.strokeStyle=ds.color; ctx.fillStyle=ds.color; ctx.lineWidth=2.4; ctx.lineJoin='round'; ctx.lineCap='round';
ctx.beginPath(); let started=false;
ds.data.forEach((v,i)=>{ if(v===null||v===undefined){started=false;return;} const x=xAt(i),y=yAt(v); if(!started){ctx.moveTo(x,y);started=true;} else ctx.lineTo(x,y); });
ctx.stroke();
ds.data.forEach((v,i)=>{ if(v===null||v===undefined)return; const x=xAt(i),y=yAt(v); ctx.beginPath();ctx.arc(x,y,3,0,Math.PI*2);ctx.fill(); });
});
let lx=pad.left, ly=cssHeight-16;
datasets.forEach(ds=>{ const text=ds.label; const tw=ctx.measureText(text).width+28; if(lx+tw>cssWidth-pad.right){ly-=18;lx=pad.left;} ctx.fillStyle=ds.color;ctx.beginPath();ctx.arc(lx+4,ly,4,0,Math.PI*2);ctx.fill();ctx.fillStyle='#475569';ctx.textAlign='left';ctx.fillText(text,lx+14,ly);lx+=tw; });
}
const categoryColors={Kitten:'#2563eb',Junior:'#db2777',Dewasa:'#059669'};
const catPalette=['#d97706','#7c3aed','#0891b2','#2563eb','#059669','#be123c'];
const avgData=@json($categoryAverageTrend ?? ['labels'=>[], 'series'=>[]]);
const trendLabels=@json($weightTrendLabels ?? []);
const trendSeries=@json($weightTrendSeries ?? []);
const avgCanvas=document.getElementById('weightCategoryAverageChart');
const trendCanvas=document.getElementById('weightSelectedCategoryChart');
const draw=()=>{
renderLineChart(avgCanvas, avgData.labels||[], (avgData.series||[]).map(s=>({label:s.name,data:s.data,color:categoryColors[s.name]||'#64748b'})));
renderLineChart(trendCanvas, trendLabels||[], (trendSeries||[]).map((s,i)=>({label:s.name,data:s.data,color:catPalette[i%catPalette.length]})));
};
draw(); let resizeTimer; window.addEventListener('resize',()=>{clearTimeout(resizeTimer);resizeTimer=setTimeout(draw,120);});
const searchInput=document.getElementById('weightTableSearch');
const rowsPerPageSelect=document.getElementById('weightRowsPerPage');
const tableRows=Array.from(document.querySelectorAll('[data-weight-row]'));
const prevButton=document.getElementById('weightPrevPage'); const nextButton=document.getElementById('weightNextPage');
const pageLabel=document.getElementById('weightPageLabel'); const tableInfo=document.getElementById('weightTableInfo'); let currentPage=1;
const renderTable=()=>{ const search=(searchInput?.value||'').trim().toLowerCase(); const size=Number(rowsPerPageSelect?.value||10); const filtered=tableRows.filter(r=>(r.dataset.name||'').includes(search)); const pages=Math.max(1,Math.ceil(filtered.length/size)); currentPage=Math.min(currentPage,pages); const start=(currentPage-1)*size; const visible=new Set(filtered.slice(start,start+size)); tableRows.forEach(r=>r.classList.toggle('hidden',!visible.has(r))); if(tableInfo)tableInfo.textContent=`Menampilkan ${filtered.length?start+1:0}-${Math.min(start+size,filtered.length)} dari ${filtered.length} data`; if(pageLabel)pageLabel.textContent=`Hal. ${currentPage} / ${pages}`; if(prevButton)prevButton.disabled=currentPage<=1; if(nextButton)nextButton.disabled=currentPage>=pages; };
searchInput?.addEventListener('input',()=>{currentPage=1;renderTable();}); rowsPerPageSelect?.addEventListener('change',()=>{currentPage=1;renderTable();}); prevButton?.addEventListener('click',()=>{if(currentPage>1){currentPage--;renderTable();}}); nextButton?.addEventListener('click',()=>{currentPage++;renderTable();}); renderTable();
});
</script>
@endpush
</x-app-layout>