549 lines
35 KiB
PHP
549 lines
35 KiB
PHP
<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',$stats['total_cats'] ?? 0,'fa-cat','cats.index'],
|
||
['Dalam perawatan',$stats['sick_cats'] ?? 0,'fa-heart-pulse','cats.index'],
|
||
['Pembiakan aktif',$stats['total_mating'] ?? 0,'fa-venus-mars','matting.index'],
|
||
['Anakan tercatat',$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>
|
||
</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 src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||
<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' });
|
||
};
|
||
|
||
if (typeof Chart !== 'undefined') {
|
||
Chart.defaults.font.family = "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
||
Chart.defaults.color = '#64748b';
|
||
|
||
const categoryColors = {
|
||
Kitten: '#2563eb',
|
||
Junior: '#db2777',
|
||
Dewasa: '#059669'
|
||
};
|
||
const catPalette = ['#d97706', '#7c3aed', '#0891b2', '#2563eb', '#059669', '#be123c'];
|
||
|
||
const avgData = @json($categoryAverageTrend ?? ['labels'=>[], 'series'=>[]]);
|
||
const avgCanvas = document.getElementById('weightCategoryAverageChart');
|
||
if (avgCanvas && avgData.labels && avgData.labels.length) {
|
||
new Chart(avgCanvas, {
|
||
type: 'line',
|
||
data: {
|
||
labels: avgData.labels.map(formatMonth),
|
||
datasets: (avgData.series || []).map((series) => ({
|
||
label: series.name,
|
||
data: series.data,
|
||
borderColor: categoryColors[series.name] || '#64748b',
|
||
backgroundColor: categoryColors[series.name] || '#64748b',
|
||
borderWidth: 2.5,
|
||
pointRadius: 3,
|
||
pointHoverRadius: 5,
|
||
pointBorderWidth: 0,
|
||
tension: 0.28,
|
||
spanGaps: true
|
||
}))
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: { usePointStyle: true, boxWidth: 8, boxHeight: 8, padding: 18, font: { size: 11, weight: '600' } }
|
||
},
|
||
tooltip: {
|
||
backgroundColor: '#0f172a',
|
||
padding: 10,
|
||
cornerRadius: 10,
|
||
callbacks: { label: (ctx) => `${ctx.dataset.label}: ${formatKg(ctx.parsed.y)}` }
|
||
}
|
||
},
|
||
scales: {
|
||
x: { grid: { display: false }, border: { display: false }, ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 8 } },
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: '#e2e8f0' },
|
||
border: { display: false },
|
||
title: { display: true, text: 'Berat (kg)', color: '#64748b', font: { size: 11, weight: '600' } }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const trendLabels = @json($weightTrendLabels ?? []);
|
||
const trendSeries = @json($weightTrendSeries ?? []);
|
||
const trendCanvas = document.getElementById('weightSelectedCategoryChart');
|
||
if (trendCanvas && trendLabels.length && trendSeries.length) {
|
||
new Chart(trendCanvas, {
|
||
type: 'line',
|
||
data: {
|
||
labels: trendLabels.map(formatMonth),
|
||
datasets: trendSeries.map((series, index) => ({
|
||
label: series.name,
|
||
data: series.data,
|
||
borderColor: catPalette[index % catPalette.length],
|
||
backgroundColor: catPalette[index % catPalette.length],
|
||
borderWidth: 2.2,
|
||
pointRadius: 3,
|
||
pointHoverRadius: 5,
|
||
pointBorderWidth: 0,
|
||
tension: 0.28,
|
||
spanGaps: true
|
||
}))
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: { mode: 'nearest', intersect: false },
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: { usePointStyle: true, boxWidth: 8, boxHeight: 8, padding: 15, font: { size: 10, weight: '600' } }
|
||
},
|
||
tooltip: {
|
||
backgroundColor: '#0f172a',
|
||
padding: 10,
|
||
cornerRadius: 10,
|
||
callbacks: { label: (ctx) => `${ctx.dataset.label}: ${formatKg(ctx.parsed.y)}` }
|
||
}
|
||
},
|
||
scales: {
|
||
x: { grid: { display: false }, border: { display: false }, ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 8 } },
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: '#e2e8f0' },
|
||
border: { display: false },
|
||
title: { display: true, text: 'Berat (kg)', color: '#64748b', font: { size: 11, weight: '600' } }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Pencarian + pagination tabel berat terbaru (client-side, tanpa request tambahan).
|
||
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 renderWeightTable = () => {
|
||
if (!tableRows.length) {
|
||
if (tableInfo) tableInfo.textContent = '0 data';
|
||
if (pageLabel) pageLabel.textContent = '';
|
||
if (prevButton) prevButton.disabled = true;
|
||
if (nextButton) nextButton.disabled = true;
|
||
return;
|
||
}
|
||
|
||
const search = (searchInput?.value || '').trim().toLowerCase();
|
||
const pageSize = Number(rowsPerPageSelect?.value || 10);
|
||
const filtered = tableRows.filter((row) => (row.dataset.name || '').includes(search));
|
||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||
currentPage = Math.min(currentPage, totalPages);
|
||
const start = (currentPage - 1) * pageSize;
|
||
const visible = new Set(filtered.slice(start, start + pageSize));
|
||
|
||
tableRows.forEach((row) => row.classList.toggle('hidden', !visible.has(row)));
|
||
|
||
if (tableInfo) {
|
||
const from = filtered.length ? start + 1 : 0;
|
||
const to = Math.min(start + pageSize, filtered.length);
|
||
tableInfo.textContent = `Menampilkan ${from}-${to} dari ${filtered.length} data`;
|
||
}
|
||
if (pageLabel) pageLabel.textContent = `Hal. ${currentPage} / ${totalPages}`;
|
||
if (prevButton) prevButton.disabled = currentPage <= 1;
|
||
if (nextButton) nextButton.disabled = currentPage >= totalPages;
|
||
};
|
||
|
||
searchInput?.addEventListener('input', () => { currentPage = 1; renderWeightTable(); });
|
||
rowsPerPageSelect?.addEventListener('change', () => { currentPage = 1; renderWeightTable(); });
|
||
prevButton?.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderWeightTable(); } });
|
||
nextButton?.addEventListener('click', () => { currentPage++; renderWeightTable(); });
|
||
renderWeightTable();
|
||
});
|
||
</script>
|
||
@endpush
|
||
</x-app-layout>
|