2026-02-08 15:16:35 +07:00
|
|
|
|
<x-app-layout>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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([
|
2026-09-23 13:56:51 +07:00
|
|
|
|
['Total kucing aktif',$stats['total_cats'] ?? 0,'fa-cat','cats.index'],
|
2026-09-23 12:45:02 +07:00
|
|
|
|
['Dalam perawatan',$stats['sick_cats'] ?? 0,'fa-heart-pulse','cats.index'],
|
2026-09-23 13:56:51 +07:00
|
|
|
|
['Masa kehamilan',$stats['total_mating'] ?? 0,'fa-venus-mars','matting.index'],
|
|
|
|
|
|
['Total anakan',$stats['total_kittens'] ?? 0,'fa-paw','matting.index']
|
2026-09-23 12:45:02 +07:00
|
|
|
|
] 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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-23 12:45:02 +07:00
|
|
|
|
@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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
@endforeach
|
|
|
|
|
|
</div>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
</div>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
@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
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
</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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-23 13:56:51 +07:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-09-23 12:45:02 +07:00
|
|
|
|
</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>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@push('scripts')
|
2026-09-23 12:45:02 +07:00
|
|
|
|
<script>
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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' });
|
|
|
|
|
|
};
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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);
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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;
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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);
|
2026-09-23 12:45:02 +07:00
|
|
|
|
}
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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); });
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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(); });
|
|
|
|
|
|
});
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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; });
|
|
|
|
|
|
}
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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);});
|
2026-09-23 12:45:02 +07:00
|
|
|
|
|
2026-09-23 13:56:51 +07:00
|
|
|
|
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();
|
|
|
|
|
|
});
|
2026-09-23 12:45:02 +07:00
|
|
|
|
</script>
|
2026-02-08 15:16:35 +07:00
|
|
|
|
@endpush
|
2026-09-23 12:45:02 +07:00
|
|
|
|
</x-app-layout>
|