Initial commit: Struktur aplikasi ilycats
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = ['properties' => 'array'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Cat extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* SINKRONISASI TOTAL: 22 Kolom Database
|
||||
* Memasukkan berat_terakhir, lokasi, dan kolom identitas lainnya.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'nama_kucing', 'ras', 'registry', 'jenis_kelamin', 'tanggal_lahir',
|
||||
'foto', 'berat_terakhir', 'lokasi', 'riwayat_sakit_terakhir',
|
||||
'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
|
||||
];
|
||||
|
||||
/**
|
||||
* Casting Laravel 11/12 Style
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_lahir' => 'date',
|
||||
'jadwal_vaksin' => 'date',
|
||||
'jadwal_obat_cacing' => 'date',
|
||||
'jadwal_obat_kutu' => 'date',
|
||||
'jadwal_grooming' => 'date',
|
||||
'registry' => 'array',
|
||||
'berat_badan' => 'decimal:2',
|
||||
'berat_terakhir' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* RELATIONS */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
public function treatments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Treatment::class);
|
||||
}
|
||||
|
||||
public function weightHistories(): HasMany
|
||||
{
|
||||
return $this->hasMany(WeightHistory::class)->latest();
|
||||
}
|
||||
|
||||
public function sire(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'induk_jantan_id')->withDefault([
|
||||
'nama_kucing' => 'Unknown Sire'
|
||||
]);
|
||||
}
|
||||
|
||||
public function dam(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'induk_betina_id')->withDefault([
|
||||
'nama_kucing' => 'Unknown Dam'
|
||||
]);
|
||||
}
|
||||
|
||||
public function mattingsAsSire(): HasMany
|
||||
{
|
||||
return $this->hasMany(Matting::class, 'sire_id');
|
||||
}
|
||||
|
||||
public function mattingsAsDam(): HasMany
|
||||
{
|
||||
return $this->hasMany(Matting::class, 'dam_id');
|
||||
}
|
||||
|
||||
public function pic(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'pic_id')->withDefault([
|
||||
'name' => 'System'
|
||||
]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ACCESSORS */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
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>'
|
||||
);
|
||||
}
|
||||
|
||||
protected function fotoUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->foto
|
||||
? Storage::url($this->foto)
|
||||
: asset('images/default-cat.png')
|
||||
);
|
||||
}
|
||||
|
||||
protected function age(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!$this->tanggal_lahir) return 'N/A';
|
||||
$diff = $this->tanggal_lahir->diff(now());
|
||||
|
||||
if ($diff->y > 0) return "{$diff->y} Thn, {$diff->m} Bln";
|
||||
if ($diff->m > 0) return "{$diff->m} Bln, {$diff->d} Hari";
|
||||
return "{$diff->d} Hari";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function registryLabel(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->registry ?? 'NON-PED'
|
||||
);
|
||||
}
|
||||
|
||||
// Badges untuk registry
|
||||
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>';
|
||||
}
|
||||
|
||||
// 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'],
|
||||
];
|
||||
|
||||
$html = '<div class="flex flex-wrap gap-1">';
|
||||
$registries = is_array($this->registry) ? $this->registry : [$this->registry];
|
||||
|
||||
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>";
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'keywords'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($category) {
|
||||
// Otomatis membuat slug dari name sebelum data disimpan
|
||||
if (empty($category->slug)) {
|
||||
$category->slug = Str::slug($category->name);
|
||||
}
|
||||
});
|
||||
}
|
||||
public function medicines(): HasMany
|
||||
{
|
||||
return $this->hasMany(Medicine::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Food extends Model
|
||||
{
|
||||
protected $table = 'foods';
|
||||
|
||||
// Gunakan guarded atau fillable, pastikan 'slug' dan 'category_id' diizinkan
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* Relasi ke model Category
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Booting model events
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 2. AUTO-CATEGORIZATION (SMART LOGIC)
|
||||
// Hanya jalankan jika category_id belum diisi manual
|
||||
if (empty($food->category_id)) {
|
||||
$productName = strtolower($food->nama_makanan);
|
||||
$categories = Category::whereNotNull('keywords')->get();
|
||||
$found = false;
|
||||
|
||||
foreach ($categories as $cat) {
|
||||
$keywordsArray = explode(',', strtolower($cat->keywords));
|
||||
|
||||
foreach ($keywordsArray as $keyword) {
|
||||
$cleanKeyword = trim($keyword);
|
||||
|
||||
if (!empty($cleanKeyword) && str_contains($productName, $cleanKeyword)) {
|
||||
$food->category_id = $cat->id;
|
||||
$food->kategori = $cat->name;
|
||||
$found = true;
|
||||
break; // Keluar dari loop keyword
|
||||
}
|
||||
}
|
||||
if ($found) break; // Keluar dari loop kategori
|
||||
}
|
||||
|
||||
// 3. FALLBACK LOGIC
|
||||
// Jika tidak ada keyword yang cocok, cari kategori default
|
||||
if (!$found) {
|
||||
$default = Category::where('name', 'Dry Food')
|
||||
->orWhere('name', 'Umum')
|
||||
->first();
|
||||
|
||||
if ($default) {
|
||||
$food->category_id = $default->id;
|
||||
$food->kategori = $default->name;
|
||||
} else {
|
||||
$food->kategori = 'Umum';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Kitten extends Model
|
||||
{
|
||||
// Mengizinkan semua field kecuali ID untuk diisi secara massal
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* Casting Data: Memastikan tipe data sinkron antara DB dan PHP.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tgl_lahir' => 'date',
|
||||
'is_pedigree' => 'boolean',
|
||||
'registry' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* MODEL EVENTS
|
||||
* Membersihkan file storage secara otomatis saat record dihapus.
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($kitten) {
|
||||
// Hapus semua foto pedigree (slot 1-4) jika ada
|
||||
for ($i = 1; $i <= 4; $i++) {
|
||||
$field = "foto_ped_$i";
|
||||
if ($kitten->$field) {
|
||||
Storage::disk('public')->delete($kitten->$field);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* RELATIONS */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Menghubungkan ke Master Profil Kucing (Patient Table)
|
||||
*/
|
||||
public function cat(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'cat_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghubungkan ke Event Perkawinan (Breeding Event)
|
||||
*/
|
||||
public function matting(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Matting::class, 'matting_id');
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ACCESSORS (UI Helper) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Menghitung berapa banyak dokumen pedigree yang sudah diupload (0-4)
|
||||
*/
|
||||
protected function pedigreeDocsCount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$count = 0;
|
||||
for ($i = 1; $i <= 4; $i++) {
|
||||
if ($this->{"foto_ped_$i"}) $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge Status Pedigree untuk Tampilan Tabel
|
||||
*/
|
||||
protected function pedigreeStatusBadge(): Attribute
|
||||
{
|
||||
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-slate-50 text-slate-400 rounded text-[9px] font-black uppercase border border-slate-100">Non-Ped</span>';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil URL Foto Pedigree Slot 1 (Thumbnail)
|
||||
*/
|
||||
protected function firstPedigreeUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->foto_ped_1
|
||||
? asset('storage/' . $this->foto_ped_1)
|
||||
: asset('images/default-doc.png')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Matting extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_kawin' => 'date',
|
||||
'tgl_kawin_awal' => 'date',
|
||||
'tgl_kawin_akhir' => 'date',
|
||||
'tgl_lahir_sire' => 'date',
|
||||
'tgl_lahir_dam' => 'date',
|
||||
'jumlah_anak_hidup' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* SINKRONISASI OTOMATIS (The Engine)
|
||||
* Saat data Matting disimpan, otomatis update data di tabel Cats.
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::saved(function ($matting) {
|
||||
// 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,
|
||||
'tanggal_lahir' => $matting->tgl_lahir_sire,
|
||||
// Pastikan kolom di tabel 'mattings' adalah ems_bapak
|
||||
'kode_ems' => $matting->ems_bapak,
|
||||
]);
|
||||
}
|
||||
|
||||
// 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
|
||||
'kode_ems' => $matting->ems_ibu,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* RELATIONS */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
public function sire(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'sire_id');
|
||||
}
|
||||
|
||||
public function dam(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'dam_id');
|
||||
}
|
||||
|
||||
public function kittens(): HasMany
|
||||
{
|
||||
return $this->hasMany(Kitten::class, 'matting_id');
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ACCESSORS (UI & Breeding Intelligence) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* EDD (Expected Delivery Date) - H+63
|
||||
*/
|
||||
protected function edd(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->tgl_kawin_awal ? $this->tgl_kawin_awal->addDays(63) : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deskripsi Sisa Hari (Countdown Text)
|
||||
*/
|
||||
protected function countdownText(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!$this->edd || $this->status !== 'proses') return null;
|
||||
|
||||
$days = (int) now()->startOfDay()->diffInDays($this->edd->startOfDay(), false);
|
||||
|
||||
if ($days > 0) return "$days Hari Lagi";
|
||||
if ($days === 0) return "Prediksi Lahir Hari Ini";
|
||||
return "Overdue " . abs($days) . " Hari";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge Status Modern
|
||||
*/
|
||||
protected function statusBadge(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
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>',
|
||||
'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>',
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* PIC (Point of Contact)
|
||||
*/
|
||||
public function pic(): BelongsTo
|
||||
{
|
||||
// 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Medicine extends Model
|
||||
{
|
||||
/**
|
||||
* Mass assignment untuk kolom-kolom inventori.
|
||||
* Kolom 'kategori' tetap ada di fillable agar bisa diisi otomatis oleh sistem.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'nama_obat',
|
||||
'foto_produk',
|
||||
'kategori',
|
||||
'qty',
|
||||
'unit',
|
||||
'expired_date'
|
||||
];
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
if (str_contains($name, trim($keyword))) {
|
||||
$medicine->category_id = $cat->id;
|
||||
$medicine->kategori = $cat->name; // Sinkronisasi teks kategori
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
];
|
||||
|
||||
/**
|
||||
* Accessor: Mempermudah pemanggilan foto produk dengan fallback image.
|
||||
*/
|
||||
public function getFotoUrlAttribute(): string
|
||||
{
|
||||
if ($this->foto_produk && Storage::disk('public')->exists($this->foto_produk)) {
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke riwayat pengobatan kucing.
|
||||
*/
|
||||
public function treatments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Treatment::class, 'medicine_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'slug'];
|
||||
|
||||
public function permissions()
|
||||
{
|
||||
return $this->hasMany(RolePermission::class);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tambahkan metode ini agar View dapat mengecek izin langsung dari objek Role
|
||||
*/
|
||||
public function hasPermission($moduleSlug, $action)
|
||||
{
|
||||
return $this->permissions()
|
||||
->where('module_slug', $moduleSlug)
|
||||
->where($action, 1) // Cek kolom can_read, can_create, dll
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'role_id',
|
||||
'module_slug',
|
||||
'can_read',
|
||||
'can_create',
|
||||
'can_update',
|
||||
'can_delete'
|
||||
];
|
||||
|
||||
public function role()
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'value', 'group'];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Supply extends Model
|
||||
{
|
||||
protected $table = 'supplies';
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
parent::booted();
|
||||
|
||||
static::saving(function ($supply) {
|
||||
// 1. Sinkronisasi Slug (Mencegah error SQL sebelumnya)
|
||||
if ($supply->isDirty('nama_perlengkapan') || empty($supply->slug)) {
|
||||
$supply->slug = Str::slug($supply->nama_perlengkapan) . '-' . Str::random(5);
|
||||
}
|
||||
|
||||
// 2. Auto-Categorization
|
||||
if (empty($supply->category_id)) {
|
||||
$name = strtolower($supply->nama_perlengkapan);
|
||||
|
||||
// Ambil kategori yang punya keywords
|
||||
$categories = Category::whereNotNull('keywords')->get();
|
||||
|
||||
foreach ($categories as $cat) {
|
||||
$keywords = explode(',', strtolower($cat->keywords));
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
$trimmed = trim($keyword);
|
||||
|
||||
// Cek apakah "pasir" atau keyword lain ada di nama produk
|
||||
if (!empty($trimmed) && str_contains($name, $trimmed)) {
|
||||
$supply->category_id = $cat->id;
|
||||
// PENTING: Update kolom teks 'kategori' agar tampilan tidak 'Umum'
|
||||
$supply->kategori = $cat->name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Treatment extends Model
|
||||
{
|
||||
/**
|
||||
* Kolom yang boleh diisi secara mass-assignment.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'cat_id',
|
||||
'user_id',
|
||||
'tanggal',
|
||||
'keluhan',
|
||||
'diagnosa',
|
||||
'nama_obat',
|
||||
'dosis',
|
||||
'keterangan',
|
||||
'foto_bukti',
|
||||
'medicine_id',
|
||||
'qty_used',
|
||||
];
|
||||
|
||||
/**
|
||||
* Casting data agar formatnya sesuai (Tanggal jadi object Carbon).
|
||||
*/
|
||||
protected $casts = [
|
||||
'tanggal' => 'datetime',
|
||||
'qty_used' => 'decimal:2',
|
||||
];
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* RELASI (MENGATASI ERROR RELATION NOT FOUND) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Relasi ke Kucing.
|
||||
*/
|
||||
public function cat(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Cat::class, 'cat_id')->withDefault([
|
||||
'nama_kucing' => 'Kucing Terhapus'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi utama ke User (PIC).
|
||||
* Nama fungsi 'user' wajib ada karena dipanggil di TreatmentController baris 31.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id')->withDefault([
|
||||
'name' => 'System / Admin'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias untuk relasi user.
|
||||
* Nama fungsi 'pic' wajib ada karena dipanggil di DashboardController.
|
||||
*/
|
||||
public function pic(): BelongsTo
|
||||
{
|
||||
return $this->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relasi ke Inventori Obat.
|
||||
*/
|
||||
public function medicine(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Medicine::class, 'medicine_id')->withDefault([
|
||||
'nama_obat' => $this->nama_obat ?? 'Obat Manual'
|
||||
]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ACCESSORS */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mengambil URL foto bukti.
|
||||
* Cara akses di Blade: {{ $treatment->foto_url }}
|
||||
*/
|
||||
public function getFotoUrlAttribute(): string
|
||||
{
|
||||
if (!$this->foto_bukti) {
|
||||
return 'https://ui-avatars.com/api/?name=No+Image&background=f1f5f9&color=cbd5e1';
|
||||
}
|
||||
|
||||
return Storage::url($this->foto_bukti);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* SCOPES */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Scope untuk mengurutkan data terbaru.
|
||||
*/
|
||||
public function scopeLatestFirst(Builder $query): void
|
||||
{
|
||||
$query->orderBy('tanggal', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Traits\HasPermissions; // Tambahan: Import Trait Permission
|
||||
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
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, HasPermissions; // Tambahan: Gunakan HasPermissions
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role', // Kode existing Anda
|
||||
'role_id', // Tambahan: ID untuk relasi tabel Role
|
||||
'phone',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tambahan: Relasi ke tabel Roles
|
||||
* Setiap user memiliki satu Role.
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
public function hasAccess($module, $permission)
|
||||
{
|
||||
// Jika admin, berikan akses penuh secara otomatis
|
||||
if ($this->role && $this->role->slug === '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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WeightHistory extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
Reference in New Issue
Block a user