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
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApiToken extends Model
{
protected $fillable = ['user_id', 'name', 'token_hash', 'last_used_at', 'expires_at'];
protected function casts(): array { return ['last_used_at' => 'datetime', 'expires_at' => 'datetime']; }
public function user() { return $this->belongsTo(User::class); }
}
+106 -44
View File
@@ -16,7 +16,6 @@ class Cat extends Model
/**
* SINKRONISASI TOTAL: 22 Kolom Database
* Memasukkan berat_terakhir, lokasi, dan kolom identitas lainnya.
*/
protected $fillable = [
'nama_kucing', 'ras', 'registry', 'jenis_kelamin', 'tanggal_lahir',
@@ -24,9 +23,38 @@ class Cat extends Model
'jadwal_vaksin', 'jadwal_obat_cacing', 'jadwal_obat_kutu',
'jadwal_grooming', 'induk_jantan_id', 'induk_betina_id',
'lokasi_kandang', 'pic_id', 'berat_badan',
'pedigree', 'microchip', 'status', 'kode_ems' // Kolom tambahan dari UI Anda
'pedigree','sertifikat_ped_file', 'microchip', 'status', 'kode_ems','treatment_status'
];
/**
* Logic Otomatisasi (Booted)
*/
protected static function booted()
{
// A. Sebelum simpan: Sinkronisasi berat_terakhir
static::saving(function ($cat) {
if ($cat->isDirty('berat_badan')) {
$cat->berat_terakhir = $cat->berat_badan;
}
});
// B. Setelah update berhasil: Catat ke tabel history
static::updated(function ($cat) {
if ($cat->wasChanged('berat_badan')) {
// Kita gunakan try-catch agar jika tabel history bermasalah, update kucing tidak ikut mati
try {
$cat->weightHistories()->create([
'berat' => $cat->berat_badan,
'tanggal_timbang' => now(), // Pastikan kolom ini ada di migrasi Anda
]);
} catch (\Exception $e) {
// Log error diam-diam agar user tidak melihat Error 500
\Illuminate\Support\Facades\Log::error("Gagal mencatat history berat: " . $e->getMessage());
}
}
});
}
/**
* Casting Laravel 11/12 Style
*/
@@ -38,7 +66,7 @@ class Cat extends Model
'jadwal_obat_cacing' => 'date',
'jadwal_obat_kutu' => 'date',
'jadwal_grooming' => 'date',
'registry' => 'array',
'registry' => 'array', // Otomatis mengurus JSON di DB
'berat_badan' => 'decimal:2',
'berat_terakhir' => 'decimal:2',
];
@@ -48,10 +76,10 @@ class Cat extends Model
/* RELATIONS */
/* -------------------------------------------------------------------------- */
public function treatments(): HasMany
{
return $this->hasMany(Treatment::class);
}
public function treatments()
{
return $this->hasMany(Treatment::class, 'cat_id');
}
public function weightHistories(): HasMany
{
@@ -89,26 +117,38 @@ class Cat extends Model
]);
}
public function registrations(): HasMany
{
return $this->hasMany(CatRegistration::class)->orderByDesc('is_primary')->orderBy('id');
}
public function primaryRegistration()
{
return $this->hasOne(CatRegistration::class)->where('is_primary', true);
}
/* -------------------------------------------------------------------------- */
/* ACCESSORS */
/* ACCESSORS (Penyempurnaan Null-Safety & Logic) */
/* -------------------------------------------------------------------------- */
protected function genderIcon(): Attribute
{
return Attribute::make(
// Database menggunakan enum 'Jantan'/'Betina' (Case Sensitive)
get: fn () => strtolower($this->jenis_kelamin) === 'jantan'
? '<i class="fas fa-mars text-indigo-500"></i>'
: '<i class="fas fa-venus text-rose-500"></i>'
get: fn () => strtolower($this->jenis_kelamin ?? '') === 'jantan'
? '<i class="fas fa-mars text-indigo-500" title="Jantan"></i>'
: '<i class="fas fa-venus text-rose-500" title="Betina"></i>'
);
}
protected function fotoUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->foto
? Storage::url($this->foto)
: asset('images/default-cat.png')
get: function () {
if ($this->foto && Storage::disk('public')->exists($this->foto)) {
return Storage::url($this->foto);
}
return asset('images/default-cat.png');
}
);
}
@@ -126,57 +166,79 @@ class Cat extends Model
);
}
// Usia presisi untuk laporan medis (Tahun, Bulan, Hari)
protected function usiaDetail(): Attribute
{
return Attribute::make(
get: function () {
if (!$this->tanggal_lahir) return 'Data N/A';
$diff = $this->tanggal_lahir->diff(now());
return "{$diff->y} Thn, {$diff->m} Bln, {$diff->d} Hari";
return "{$diff->y}th {$diff->m}bln {$diff->d}hr";
}
);
}
protected function registryLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->registry ?? 'NON-PED'
);
}
// Badges untuk registry
/**
* Badge Registry yang disempurnakan (lebih bersih)
*/
protected function registryBadges(): Attribute
{
return Attribute::make(
get: function () {
// Jika tidak ada registry
if (empty($this->registry)) {
return '<span class="text-[9px] text-slate-400 font-bold italic tracking-widest">NON-PED</span>';
{
return Attribute::make(
get: function () {
$registries = $this->registry;
// Jika kosong atau eksplisit memilih NON-PED
if (empty($registries) || (is_array($registries) && in_array('NON-PED', $registries))) {
return '<span class="px-2 py-0.5 bg-slate-100 text-slate-400 border border-slate-200 rounded-lg text-[8px] font-black uppercase tracking-tighter">NON-PED</span>';
}
// Mapping warna untuk setiap federasi
$config = [
'ICA' => ['bg' => 'bg-blue-50', 'text' => 'text-blue-700', 'border' => 'border-blue-100'],
'CFA' => ['bg' => 'bg-rose-50', 'text' => 'text-rose-700', 'border' => 'border-rose-100'],
'WCF' => ['bg' => 'bg-emerald-50', 'text' => 'text-emerald-700', 'border' => 'border-emerald-100'],
'FFF' => ['bg' => 'bg-amber-50', 'text' => 'text-amber-700', 'border' => 'border-amber-100'],
'ICA' => 'bg-blue-50 text-blue-700 border-blue-100',
'CFA' => 'bg-rose-50 text-rose-700 border-rose-100',
'WCF' => 'bg-emerald-50 text-emerald-700 border-emerald-100',
'FFF' => 'bg-amber-50 text-amber-700 border-amber-100',
];
$html = '<div class="flex flex-wrap gap-1">';
$registries = is_array($this->registry) ? $this->registry : [$this->registry];
$list = is_array($registries) ? $registries : [$registries];
foreach ($registries as $reg) {
$style = $config[$reg] ?? ['bg' => 'bg-slate-50', 'text' => 'text-slate-600', 'border' => 'border-slate-200'];
$html .= "<span class='px-2 py-0.5 {$style['bg']} {$style['text']} {$style['border']} border rounded-lg text-[8px] font-black uppercase tracking-tighter shadow-sm'>";
$html .= $reg;
$html .= "</span>";
foreach ($list as $reg) {
if($reg === 'NON-PED') continue; // Skip jika tercampur (opsional)
$uReg = strtoupper($reg);
$style = $config[$uReg] ?? 'bg-slate-50 text-slate-600 border-slate-200';
$html .= "<span class='px-2 py-0.5 border rounded-lg text-[8px] font-black uppercase tracking-tighter shadow-sm {$style}'>{$uReg}</span>";
}
$html .= '</div>';
return $html;
}
);
}
public function medicalRecords()
{
// Mengambil riwayat, terbaru diletakkan paling atas
return $this->hasMany(MedicalRecord::class)->orderBy('date_given', 'desc');
}
/**
* Sinkronkan kolom status perawatan dari treatment yang masih aktif.
* Nilai 1 = dalam perawatan, 2 = sehat.
*/
public function updateTreatmentStatus(): void
{
$status = $this->treatments()->where('status_perawatan', 1)->exists() ? 1 : 2;
$this->forceFill(['treatment_status' => $status])->saveQuietly();
}
/**
* Scope untuk filter kucing yang sedang sakit (Under Treatment)
*/
public function scopeUnderTreatment($query)
{
// Hapus logika whereHas lama, ganti dengan ini:
return $query->where('treatment_status', 1);
}
public function scopeHealthy($query)
{
return $query->where('treatment_status', 2);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CatAssociation extends Model
{
protected $fillable = ['code', 'name', 'country', 'website', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function registrations()
{
return $this->hasMany(CatRegistration::class, 'association_id');
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class CatRegistration extends Model
{
protected $fillable = [
'cat_id', 'association_id', 'registration_number', 'pedigree_number',
'certificate_number', 'issue_date', 'title', 'ownership_status',
'is_primary', 'notes',
];
protected function casts(): array
{
return ['issue_date' => 'date', 'is_primary' => 'boolean'];
}
protected static function booted(): void
{
static::deleting(function (CatRegistration $registration) {
$registration->loadMissing('documents');
foreach ($registration->documents as $document) {
if ($document->file_path) {
Storage::disk('public')->delete($document->file_path);
}
}
});
}
public function cat() { return $this->belongsTo(Cat::class); }
public function association() { return $this->belongsTo(CatAssociation::class, 'association_id'); }
public function documents() { return $this->hasMany(CatRegistrationDocument::class)->latest(); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class CatRegistrationDocument extends Model
{
protected $fillable = ['cat_registration_id', 'label', 'document_type', 'file_path', 'issued_at'];
protected function casts(): array { return ['issued_at' => 'date']; }
public function registration() { return $this->belongsTo(CatRegistration::class, 'cat_registration_id'); }
public function getUrlAttribute(): string { return Storage::disk('public')->url($this->file_path); }
}
+47 -14
View File
@@ -3,19 +3,41 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class Food extends Model
{
protected $table = 'foods';
// Gunakan guarded atau fillable, pastikan 'slug' dan 'category_id' diizinkan
protected $guarded = ['id'];
// Menggunakan fillable lebih aman untuk tracking kolom apa saja yang masuk ke DB
protected $fillable = [
'nama_makanan',
'foto_produk',
'slug',
'qty',
'unit',
'category_id',
'kategori',
'expired_at'
];
/**
* Casting tipe data agar terbaca sebagai objek oleh Carbon
*/
protected function casts(): array
{
return [
'expired_at' => 'date',
'qty' => 'float',
'created_at' => 'datetime',
];
}
/**
* Relasi ke model Category
*/
public function category()
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
@@ -27,14 +49,13 @@ class Food extends Model
{
static::saving(function ($food) {
// 1. OTOMATIS GENERATE SLUG
// Hanya generate jika nama berubah atau slug masih kosong
if ($food->isDirty('nama_makanan') || empty($food->slug)) {
$food->slug = Str::slug($food->nama_makanan) . '-' . Str::random(5);
$food->slug = Str::slug($food->nama_makanan) . '-' . Str::lower(Str::random(5));
}
// 2. AUTO-CATEGORIZATION (SMART LOGIC)
// Hanya jalankan jika category_id belum diisi manual
if (empty($food->category_id)) {
// Jalankan hanya jika category_id belum diisi manual atau nama makanan berubah
if (empty($food->category_id) || $food->isDirty('nama_makanan')) {
$productName = strtolower($food->nama_makanan);
$categories = Category::whereNotNull('keywords')->get();
$found = false;
@@ -49,27 +70,39 @@ class Food extends Model
$food->category_id = $cat->id;
$food->kategori = $cat->name;
$found = true;
break; // Keluar dari loop keyword
break 2; // Keluar dari kedua loop sekaligus
}
}
if ($found) break; // Keluar dari loop kategori
}
// 3. FALLBACK LOGIC
// Jika tidak ada keyword yang cocok, cari kategori default
if (!$found) {
if (!$found && empty($food->category_id)) {
$default = Category::where('name', 'Dry Food')
->orWhere('name', 'Umum')
->first();
->orWhere('name', 'Umum')
->first();
if ($default) {
$food->category_id = $default->id;
$food->kategori = $default->name;
} else {
$food->kategori = 'Umum';
$food->kategori = $food->kategori ?: 'Umum';
}
}
}
// 4. SINKRONISASI TEKS KATEGORI
// Jika category_id diisi manual di form, pastikan kolom 'kategori' (string) juga terupdate
if ($food->isDirty('category_id') && !empty($food->category_id)) {
$food->kategori = $food->category?->name ?? $food->kategori;
}
});
}
/**
* Accessor: Cek Status Expired
*/
public function getIsExpiredAttribute(): bool
{
return $this->expired_at ? $this->expired_at->isPast() : false;
}
}
+2 -2
View File
@@ -91,9 +91,9 @@ class Kitten extends Model
return Attribute::make(
get: function () {
if ($this->is_pedigree) {
return '<span class="px-2 py-0.5 bg-indigo-50 text-indigo-600 rounded text-[9px] font-black uppercase border border-indigo-100 italic">Certificate OK</span>';
return '<span class="px-2 py-0.5 bg-indigo-50 text-indigo-600 rounded text-[9px] font-black uppercase border border-indigo-100 italic">Sertifikat tersedia</span>';
}
return '<span class="px-2 py-0.5 bg-slate-50 text-slate-400 rounded text-[9px] font-black uppercase border border-slate-100">Non-Ped</span>';
return '<span class="px-2 py-0.5 bg-slate-50 text-slate-400 rounded text-[9px] font-black uppercase border border-slate-100">Non-pedigree</span>';
}
);
}
+5 -7
View File
@@ -25,7 +25,7 @@ class Matting extends Model
}
/**
* SINKRONISASI OTOMATIS (The Engine)
* SINKRONISASI OTOMATIS (Mesin sinkronisasi)
* Saat data Matting disimpan, otomatis update data di tabel Cats.
*/
protected static function booted()
@@ -34,8 +34,7 @@ class Matting extends Model
// 1. Update data Sire (Bapak)
if ($matting->sire_id && $matting->sire) {
$matting->sire->update([
'pedigree' => $matting->no_pedigree_bapak,
'microchip' => $now_microchip_bapak ?? $matting->no_microchip_bapak,
'microchip' => $matting->no_microchip_bapak,
'tanggal_lahir' => $matting->tgl_lahir_sire,
// Pastikan kolom di tabel 'mattings' adalah ems_bapak
'kode_ems' => $matting->ems_bapak,
@@ -45,7 +44,6 @@ class Matting extends Model
// 2. Update data Dam (Ibu)
if ($matting->dam_id && $matting->dam) {
$matting->dam->update([
'pedigree' => $matting->no_pedigree_ibu,
'microchip' => $matting->no_microchip_ibu,
'tanggal_lahir' => $matting->tgl_lahir_dam,
// Pastikan kolom di tabel 'mattings' adalah ems_ibu
@@ -74,7 +72,7 @@ class Matting extends Model
}
/* -------------------------------------------------------------------------- */
/* ACCESSORS (UI & Breeding Intelligence) */
/* ACCESSORS (UI & BREEDING) */
/* -------------------------------------------------------------------------- */
/**
@@ -114,7 +112,7 @@ class Matting extends Model
get: function () {
$status = strtolower($this->status);
return match($status) {
'proses' => '<span class="px-3 py-1 bg-blue-50 text-blue-600 rounded-full text-[10px] font-black uppercase border border-blue-100">Monitoring</span>',
'proses' => '<span class="px-3 py-1 bg-blue-50 text-blue-600 rounded-full text-[10px] font-black uppercase border border-blue-100">Dipantau</span>',
'berhasil' => '<span class="px-3 py-1 bg-emerald-50 text-emerald-600 rounded-full text-[10px] font-black uppercase border border-emerald-100">Lahir</span>',
'gagal' => '<span class="px-3 py-1 bg-rose-50 text-rose-600 rounded-full text-[10px] font-black uppercase border border-rose-100">Gagal</span>',
default => '<span class="px-3 py-1 bg-slate-50 text-slate-600 rounded-full text-[10px] font-black uppercase">'.$status.'</span>',
@@ -131,7 +129,7 @@ class Matting extends Model
// Pastikan kolom di tabel 'mattings' adalah 'user_id'
// Jika kolomnya bernama 'pic_id', ganti 'user_id' menjadi 'pic_id'
return $this->belongsTo(User::class, 'user_id')->withDefault([
'name' => 'System / Admin'
'name' => 'Sistem / Admin'
]);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; // 1. TAMBAHKAN BARIS INI
class MedicalRecord extends Model
{
use HasFactory;
protected $fillable = [
'cat_id',
'user_id',
'type',
'treatment_name',
'dosage',
'unit',
'date_given',
'next_schedule',
'notes',
];
protected $casts = [
'date_given' => 'date',
'next_schedule' => 'date',
];
public function cat(): BelongsTo
{
return $this->belongsTo(Cat::class);
}
// 2. PASTIKAN RELASI INI BENAR
public function performer(): BelongsTo
{
// Karena kolomnya sekarang user_id (bukan performed_by),
// kita beri tahu Laravel secara eksplisit:
return $this->belongsTo(User::class, 'user_id');
}
}
+73 -30
View File
@@ -4,58 +4,77 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
class Medicine extends Model
{
/**
* Mass assignment untuk kolom-kolom inventori.
* Kolom 'kategori' tetap ada di fillable agar bisa diisi otomatis oleh sistem.
* Mass assignment.
* Menambahkan expired_at dan category_id agar sinkron dengan update database terbaru.
*/
protected $fillable = [
'nama_obat',
'foto_produk',
'kategori',
'category_id',
'qty',
'unit',
'expired_date'
'expired_date',
'expired_at'
];
/**
* Modern Casting (Laravel 11/12 style)
*/
protected function casts(): array
{
return [
'expired_date' => 'date',
'expired_at' => 'date',
'qty' => 'float', // Mendukung dosis desimal (misal 2.5 ml)
'created_at' => 'datetime',
];
}
/**
* Logic Otomatisasi & Integritas Data
*/
protected static function booted()
{
static::saving(function ($medicine) {
$name = strtolower($medicine->nama_obat);
// Ambil semua kategori dari database
$categories = \App\Models\Category::all();
foreach ($categories as $cat) {
// Pecah kata kunci berdasarkan koma
$keywords = explode(',', strtolower($cat->keywords));
{
static::saving(function ($medicine) {
$name = strtolower($medicine->nama_obat);
foreach ($keywords as $keyword) {
if (str_contains($name, trim($keyword))) {
$medicine->category_id = $cat->id;
$medicine->kategori = $cat->name; // Sinkronisasi teks kategori
return;
// 1. OTOMATISASI KATEGORI BERDASARKAN KEYWORDS
$categories = \App\Models\Category::all();
$matched = false;
foreach ($categories as $cat) {
$keywords = explode(',', strtolower($cat->keywords));
foreach ($keywords as $keyword) {
if (str_contains($name, trim($keyword))) {
$medicine->category_id = $cat->id;
$medicine->kategori = $cat->name;
$matched = true;
break 2;
}
}
}
}
$medicine->kategori = 'Umum';
});
}
if (!$matched) {
$medicine->kategori = $medicine->kategori ?: 'Umum';
}
/**
* Casting tipe data untuk presisi medis.
*/
protected $casts = [
'expired_date' => 'date', // Memperbaiki error isPast() di Blade
'qty' => 'float', // Mendukung dosis desimal seperti 2.5 ml
];
// 2. SINKRONISASI TANGGAL EXPIRED (Double Guard)
// Jika expired_at kosong tapi expired_date ada, samakan. Begitu juga sebaliknya.
if ($medicine->expired_date && !$medicine->expired_at) {
$medicine->expired_at = $medicine->expired_date;
} elseif ($medicine->expired_at && !$medicine->expired_date) {
$medicine->expired_date = $medicine->expired_at;
}
});
}
/**
* Accessor: Mempermudah pemanggilan foto produk dengan fallback image.
@@ -66,10 +85,26 @@ class Medicine extends Model
return asset('storage/' . $this->foto_produk);
}
// Menggunakan UI Avatars sebagai placeholder jika foto tidak ada
return 'https://ui-avatars.com/api/?name=' . urlencode($this->nama_obat) . '&background=f1f5f9&color=64748b';
}
/**
* Accessor: Cek Status Expired secara instan.
* Bisa digunakan di Blade: @if($medicine->is_expired) ... @endif
*/
public function getIsExpiredAttribute(): bool
{
return $this->expired_at ? $this->expired_at->isPast() : false;
}
/**
* Relasi ke Kategori Obat.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
/**
* Relasi ke riwayat pengobatan kucing.
*/
@@ -77,4 +112,12 @@ class Medicine extends Model
{
return $this->hasMany(Treatment::class, 'medicine_id');
}
/**
* Melihat riwayat penggunaan obat ini di berbagai rekam medis
*/
public function treatmentDetails()
{
return $this->hasMany(TreatmentDetail::class);
}
}
+70 -27
View File
@@ -9,59 +9,102 @@ use Illuminate\Support\Str;
class Supply extends Model
{
protected $table = 'supplies';
protected $guarded = ['id'];
// Menggunakan fillable untuk keamanan mass assignment
protected $fillable = [
'nama_perlengkapan',
'foto_produk',
'slug',
'qty',
'unit',
'category_id',
'kategori',
'expired_at'
];
/**
* Modern Casting (Laravel 11/12)
* Memastikan expired_at menjadi objek Carbon untuk mencegah error isPast()
*/
protected function casts(): array
{
return [
'expired_at' => 'date',
'qty' => 'float',
'created_at' => 'datetime',
];
}
/**
* Relasi ke model Category
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
/**
* Logic Otomatisasi & Integritas Data
*/
protected static function booted()
{
parent::booted();
static::saving(function ($supply) {
// 1. Sinkronisasi Slug (Mencegah error SQL sebelumnya)
// 1. OTOMATIS GENERATE SLUG
if ($supply->isDirty('nama_perlengkapan') || empty($supply->slug)) {
$supply->slug = Str::slug($supply->nama_perlengkapan) . '-' . Str::random(5);
$supply->slug = Str::slug($supply->nama_perlengkapan) . '-' . Str::lower(Str::random(5));
}
// 2. Auto-Categorization
if (empty($supply->category_id)) {
// 2. AUTO-CATEGORIZATION (SMART LOGIC)
// Jalankan jika category_id kosong atau nama perlengkapan berubah
if (empty($supply->category_id) || $supply->isDirty('nama_perlengkapan')) {
$name = strtolower($supply->nama_perlengkapan);
// Ambil kategori yang punya keywords
$categories = Category::whereNotNull('keywords')->get();
$found = false;
foreach ($categories as $cat) {
$keywords = explode(',', strtolower($cat->keywords));
$keywordsArray = explode(',', strtolower($cat->keywords));
foreach ($keywords as $keyword) {
$trimmed = trim($keyword);
foreach ($keywordsArray as $keyword) {
$cleanKeyword = trim($keyword);
// Cek apakah "pasir" atau keyword lain ada di nama produk
if (!empty($trimmed) && str_contains($name, $trimmed)) {
if (!empty($cleanKeyword) && str_contains($name, $cleanKeyword)) {
$supply->category_id = $cat->id;
// PENTING: Update kolom teks 'kategori' agar tampilan tidak 'Umum'
$supply->kategori = $cat->name;
return;
$found = true;
break 2; // Berhenti mencari jika sudah ketemu
}
}
}
// 3. Fallback jika tidak ada yang cocok
// Cari kategori yang mengandung kata 'Pasir' atau 'Umum'
$default = Category::where('name', 'LIKE', '%Pasir%')
->orWhere('name', 'Umum')
->first();
if ($default) {
$supply->category_id = $default->id;
$supply->kategori = $default->name;
} else {
$supply->kategori = 'Umum';
// 3. FALLBACK LOGIC
if (!$found && empty($supply->category_id)) {
$default = Category::where('name', 'LIKE', '%Pasir%')
->orWhere('name', 'Umum')
->first();
if ($default) {
$supply->category_id = $default->id;
$supply->kategori = $default->name;
} else {
$supply->kategori = $supply->kategori ?: 'Umum';
}
}
}
// 4. SINKRONISASI TEKS KATEGORI
// Jika ID kategori diubah manual di form, update teks kolom 'kategori'
if ($supply->isDirty('category_id') && !empty($supply->category_id)) {
$supply->kategori = $supply->category?->name ?? $supply->kategori;
}
});
}
/**
* Accessor: Membantu pengecekan expired di Blade secara rapi
* Contoh: @if($supply->is_expired) ... @endif
*/
public function getIsExpiredAttribute(): bool
{
return $this->expired_at ? $this->expired_at->isPast() : false;
}
}
+62 -19
View File
@@ -4,8 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Treatment extends Model
{
@@ -24,10 +27,12 @@ class Treatment extends Model
'foto_bukti',
'medicine_id',
'qty_used',
'status_perawatan', // Tambahan Baru
'remark_pasca_sakit'
];
/**
* Casting data agar formatnya sesuai (Tanggal jadi object Carbon).
* Casting data agar formatnya sesuai.
*/
protected $casts = [
'tanggal' => 'datetime',
@@ -35,7 +40,7 @@ class Treatment extends Model
];
/* -------------------------------------------------------------------------- */
/* RELASI (MENGATASI ERROR RELATION NOT FOUND) */
/* RELASI */
/* -------------------------------------------------------------------------- */
/**
@@ -43,25 +48,19 @@ class Treatment extends Model
*/
public function cat(): BelongsTo
{
return $this->belongsTo(Cat::class, 'cat_id')->withDefault([
'nama_kucing' => 'Kucing Terhapus'
]);
return $this->belongsTo(Cat::class)->withTrashed();
}
/**
* Relasi utama ke User (PIC).
* Nama fungsi 'user' wajib ada karena dipanggil di TreatmentController baris 31.
* Relasi utama ke User (PIC).
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id')->withDefault([
'name' => 'System / Admin'
]);
return $this->belongsTo(User::class);
}
/**
* Alias untuk relasi user.
* Nama fungsi 'pic' wajib ada karena dipanggil di DashboardController.
* Alias untuk relasi user (Digunakan di Dashboard).
*/
public function pic(): BelongsTo
{
@@ -69,22 +68,38 @@ class Treatment extends Model
}
/**
* Relasi ke Inventori Obat.
* PENTING: Relasi ke Medicine tunggal (Legacy Support).
* Ini yang memperbaiki error 'RelationNotFoundException' pada data lama.
*/
public function medicine(): BelongsTo
{
return $this->belongsTo(Medicine::class, 'medicine_id')->withDefault([
'nama_obat' => $this->nama_obat ?? 'Obat Manual'
]);
return $this->belongsTo(Medicine::class);
}
/**
* Relasi ke Detail Obat (Sistem Baru Multi-Medicine).
*/
public function details(): HasMany
{
return $this->hasMany(TreatmentDetail::class);
}
/**
* Relasi langsung ke banyak Medicine (Many-to-Many via pivot details).
*/
public function medicines(): BelongsToMany
{
return $this->belongsToMany(Medicine::class, 'treatment_details')
->withPivot('dosis', 'keterangan_pakai')
->withTimestamps();
}
/* -------------------------------------------------------------------------- */
/* ACCESSORS */
/* ACCESSORS */
/* -------------------------------------------------------------------------- */
/**
* Mengambil URL foto bukti.
* Cara akses di Blade: {{ $treatment->foto_url }}
*/
public function getFotoUrlAttribute(): string
{
@@ -95,8 +110,26 @@ class Treatment extends Model
return Storage::url($this->foto_bukti);
}
/**
* Menghitung Progres Pengobatan secara otomatis.
* Cara pakai di Blade: {{ $treatment->status_progress }}
*/
public function getStatusProgressAttribute(): string
{
$text = strtolower($this->diagnosa . ' ' . $this->keterangan);
// Keyword indikator membaik
$improvingKeywords = ['baik', 'makan', 'aktif', 'sembuh', 'lincah', 'normal', 'turun panas'];
if (Str::contains($text, $improvingKeywords)) {
return 'MEMBAIK';
}
return 'STUCK';
}
/* -------------------------------------------------------------------------- */
/* SCOPES */
/* SCOPES */
/* -------------------------------------------------------------------------- */
/**
@@ -106,4 +139,14 @@ class Treatment extends Model
{
$query->orderBy('tanggal', 'desc');
}
/**
* Scope khusus untuk mengambil rekam medis yang sedang aktif (Under Treatment).
*/
public function scopeActiveTreatment(Builder $query): Builder
{
return $query->whereHas('cat', function($q) {
$q->where('treatment_status', 1);
});
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TreatmentDetail extends Model
{
use HasFactory;
// Menentukan kolom yang boleh diisi secara massal
protected $fillable = [
'treatment_id',
'medicine_id',
'dosis',
'keterangan_pakai',
];
/**
* Relasi balik ke Header Treatment (Rekam Medis Utama)
*/
public function treatment()
{
return $this->belongsTo(Treatment::class);
}
/**
* Relasi ke data Obat (Inventory)
*/
public function medicine()
{
return $this->belongsTo(Medicine::class);
}
}
+20 -14
View File
@@ -8,12 +8,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\BelongsTo; // Tambahan: Import untuk relasi
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable; // Tambahkan ini
use Laragear\WebAuthn\WebAuthnAuthentication;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasPermissions; // Tambahan: Gunakan HasPermissions
use WebAuthnAuthentication;
/**
* The attributes that are mass assignable.
*
@@ -55,23 +59,25 @@ class User extends Authenticatable
* Tambahan: Relasi ke tabel Roles
* Setiap user memiliki satu Role.
*/
public function role(): BelongsTo
/**
* Relasi role memakai nama roleModel agar tidak bertabrakan dengan kolom legacy `role`.
*/
public function roleModel(): BelongsTo
{
return $this->belongsTo(Role::class);
return $this->belongsTo(Role::class, 'role_id');
}
public function hasAccess($module, $permission)
{
// Jika admin, berikan akses penuh secara otomatis
if ($this->role && $this->role->slug === 'admin') {
return true;
}
public function hasAccess($module, $permission): bool
{
$role = $this->roleModel;
if ($role?->slug === 'admin' || $this->getRawOriginal('role') === 'admin') {
return true;
}
// Cek di tabel role_permissions melalui relasi role
return $this->role->permissions()
->where('module_slug', $module)
->where($permission, 1) // $permission berisi 'can_read', 'can_update', dll
->exists();
}
return $role?->permissions()
->where('module_slug', $module)
->where($permission, 1)
->exists() ?? false;
}
}
+24 -2
View File
@@ -6,5 +6,27 @@ use Illuminate\Database\Eloquent\Model;
class WeightHistory extends Model
{
//
}
// Pastikan fillable sesuai dengan kolom di database
protected $fillable = [
'cat_id',
'berat',
'tanggal_timbang', // Pastikan baris ini ada
'created_at',
'updated_at'
];
protected function casts(): array
{
return [
'tanggal_timbang' => 'date',
'berat' => 'decimal:2',
];
}
public function cat()
{
return $this->belongsTo(Cat::class);
}
}