initial commit

This commit is contained in:
2026-09-23 12:45:02 +07:00
parent d16f454083
commit 507fc6ccdf
235 changed files with 19587 additions and 2835 deletions
+541 -134
View File
@@ -1,141 +1,548 @@
<x-app-layout>
<div class="max-w-[1600px] mx-auto p-4 sm:px-6 lg:px-8 py-8 space-y-8">
<div class="flex gap-3">
{{-- Tombol Laporan: Cek modul 'reports' dengan akses 'read' --}}
@can('check-access', ['reports', 'read'])
<a href="{{ route('admin.reports.index') }}"
class="px-6 py-3 bg-slate-100 text-slate-600 text-[10px] font-black uppercase rounded-2xl hover:bg-slate-200 transition-all tracking-widest">
Laporan Lengkap
</a>
@endcan
{{-- Tombol Tambah Kucing: Cek modul 'cats' dengan akses 'create' --}}
@can('check-access', ['cats', 'create'])
<a href="{{ route('cats.create') }}"
class="px-6 py-3 bg-indigo-600 text-white text-[10px] font-black uppercase rounded-2xl shadow-xl shadow-indigo-100 hover:bg-indigo-700 transition-all tracking-widest flex items-center">
<i class="fas fa-plus mr-2"></i> Tambah Kucing
</a>
@endcan
</div>
{{-- Birth Watch Alert --}}
@if($upcomingBirths->count() > 0)
<div class="bg-white rounded-[3rem] border-2 border-rose-100 shadow-xl shadow-rose-50 overflow-hidden">
<div class="bg-rose-600 px-8 py-4 flex justify-between items-center">
<div class="flex items-center gap-3">
<i class="fas fa-baby-carriage text-white animate-bounce"></i>
<h3 class="text-[10px] font-black text-white uppercase tracking-[0.3em]">Birth Watch: Persalinan Mendatang</h3>
</div>
</div>
<div class="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
@foreach($upcomingBirths as $birth)
<div class="bg-rose-50/50 border border-rose-100 p-6 rounded-[2rem] relative group">
<div class="absolute -top-3 -right-3 bg-rose-600 text-white w-10 h-10 rounded-xl flex items-center justify-center font-black text-sm shadow-lg transform rotate-12">
{{ $birth->days_to_go }}d
</div>
<div class="text-xs font-black text-rose-700 uppercase mb-1">{{ $birth->dam->nama_kucing }}</div>
<div class="text-[9px] font-bold text-rose-400 uppercase tracking-tighter mb-3">Sire: {{ $birth->sire->nama_kucing }}</div>
<div class="text-[10px] font-bold text-slate-600">Estimasi: {{ $birth->edd->format('d M Y') }}</div>
</div>
@endforeach
</div>
<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>
@endif
{{-- Stats Cards --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<x-stats-card title="Total Kucing" :value="$stats['total_cats']" icon="fa-cat" color="indigo" />
<x-stats-card title="Indukan Hamil" :value="$stats['total_mating']" icon="fa-venus" color="rose" />
<x-stats-card title="Sedang Sakit" :value="$stats['sick_cats']" icon="fa-stethoscope" color="amber" />
<x-stats-card title="Stok Kritis" :value="$totalLowStock" icon="fa-exclamation-triangle" color="rose" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
{{-- Left Content: Records & Stock --}}
<div class="lg:col-span-2 space-y-8">
<div class="bg-white rounded-[3rem] border border-slate-100 shadow-sm overflow-hidden">
<div class="p-8 border-b border-slate-50 flex justify-between items-center">
<h3 class="text-xs font-black text-slate-800 uppercase tracking-widest">Rekam Medis Terbaru</h3>
<a href="{{ route('treatments.index') }}" class="text-[10px] font-black text-indigo-600 uppercase hover:underline">Lihat Semua</a>
</div>
<table class="w-full text-left">
<tbody>
@foreach($recentTreatments as $t)
<tr class="hover:bg-slate-50/50">
<td class="px-8 py-5 flex items-center gap-4">
<div class="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600 font-black text-xs">
{{ substr($t->cat->nama_kucing, 0, 1) }}
</div>
<div>
<div class="text-xs font-black text-slate-800 uppercase leading-none">{{ $t->cat->nama_kucing }}</div>
<div class="text-[9px] text-slate-400 font-bold mt-1">{{ $t->created_at->diffForHumans() }}</div>
</div>
</td>
<td class="px-8 py-5 text-xs italic text-slate-500">"{{ $t->keluhan }}"</td>
<td class="px-8 py-5 text-right font-black text-indigo-600 text-[10px] uppercase">{{ $t->nama_obat }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Charts --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white rounded-[3rem] border border-slate-100 p-8">
<h4 class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-6">Sebaran Ras</h4>
<canvas id="breedChart" class="max-h-[200px]"></canvas>
</div>
<div class="bg-white rounded-[3rem] border border-slate-100 p-8">
<h4 class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-6">Tren Berat Badan (kg)</h4>
<canvas id="weightChart" class="max-h-[200px]"></canvas>
</div>
</div>
</div>
{{-- Right Content: Reminders --}}
<div class="space-y-8">
<div class="bg-slate-900 rounded-[3rem] p-8 text-white relative overflow-hidden shadow-2xl">
<h3 class="text-[10px] font-black text-indigo-400 uppercase tracking-[0.3em] mb-6">Medical Reminders</h3>
<div class="space-y-6">
@foreach([['Vaksin', 'vaccine', 'syringe'], ['Kutu', 'flea', 'bug'], ['Cacing', 'deworming', 'bug-slash'], ['Grooming', 'grooming', 'soap']] as $item)
<div class="flex justify-between items-center">
<span class="text-[10px] font-bold text-slate-300 uppercase"><i class="fas fa-{{ $item[2] }} mr-2"></i> {{ $item[0] }}</span>
<span class="text-xs font-black {{ $medicalAlerts[$item[1]] > 0 ? 'text-rose-400 animate-pulse' : 'text-slate-500' }}">{{ $medicalAlerts[$item[1]] }}</span>
</div>
@endforeach
</div>
</div>
<div class="bg-indigo-600 rounded-[3rem] p-8 text-white">
<h4 class="font-black text-sm uppercase tracking-widest mb-2 italic">Breeder Tip</h4>
<p class="text-[10px] text-indigo-100 leading-relaxed font-medium">Jangan lupa berikan vitamin bulu khusus untuk kucing <strong>Persian</strong> Anda setiap pagi.</p>
</div>
</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>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Breed Chart
new Chart(document.getElementById('breedChart'), {
type: 'doughnut',
data: {
labels: {!! json_encode($chartData->pluck('ras')) !!},
datasets: [{ data: {!! json_encode($chartData->pluck('total')) !!}, backgroundColor: ['#6366f1', '#f43f5e', '#fbbf24', '#10b981'], borderWidth: 0, spacing: 10 }]
},
options: { cutout: '80%', plugins: { legend: { position: 'bottom', labels: { font: { size: 10, weight: 'bold' }, usePointStyle: true } } } }
});
<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>
// Weight Chart
new Chart(document.getElementById('weightChart'), {
type: 'line',
data: {
labels: {!! json_encode($weightTrends->pluck('month')) !!},
datasets: [{ label: 'Berat (kg)', data: {!! json_encode($weightTrends->pluck('avg_weight')) !!}, borderColor: '#6366f1', tension: 0.4, fill: true, backgroundColor: 'rgba(99, 102, 241, 0.1)' }]
},
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: false, grid: { display: false } }, x: { grid: { display: false } } } }
});
</script>
{{-- 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>
</x-app-layout>
@@ -2,7 +2,7 @@
<div class="space-y-8 pb-12">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-3xl font-black text-slate-800 tracking-tight leading-none mb-1">Medical Insights</h2>
<h2 class="text-3xl font-black text-slate-800 tracking-tight leading-none mb-1">Ringkasan Kesehatans</h2>
<p class="text-sm text-slate-500 font-medium italic uppercase tracking-tighter">Analisis Kesehatan & Konsumsi Obat Ily Cattery</p>
</div>
<div class="flex gap-2">
@@ -51,7 +51,7 @@
<i class="fas fa-pills text-emerald-400"></i>
</div>
<div class="space-y-6">
@foreach($topMedicines as $medicine)
@foreach($topObats as $medicine)
<div class="relative">
<div class="flex justify-between text-[10px] font-black uppercase mb-2">
<span>{{ $medicine->nama_obat }}</span>
@@ -59,7 +59,7 @@
</div>
<div class="w-full bg-slate-800 rounded-full h-1.5">
@php
$maxUsed = $topMedicines->first()->total_used ?? 1;
$maxUsed = $topObats->first()->total_used ?? 1;
$percentage = ($medicine->total_used / $maxUsed) * 100;
@endphp
<div class="bg-emerald-400 h-1.5 rounded-full" style="width: {{ $percentage }}%"></div>