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
+228 -80
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Cat;
use App\Models\Treatment;
use App\Models\Medicine;
use App\Models\TreatmentDetail; // Menambahkan model Detail
use App\Traits\LogsActivity;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -18,167 +19,314 @@ class TreatmentController extends Controller
use LogsActivity;
/**
* Menampilkan daftar rekam medis dengan optimasi Eager Loading.
* Menampilkan daftar rekam medis dengan optimasi Eager Loading & Filtering.
*/
public function index(): View
{
$treatments = Treatment::with([
'cat:id,nama_kucing,foto',
'medicine:id,nama_obat,unit',
'user:id,name'
])
->latest('tanggal')
->paginate(15);
public function index(Request $request): View|\Symfony\Component\HttpFoundation\BinaryFileResponse
{
// 1. QUERY DASAR DENGAN OPTIMASI EAGER LOADING
$query = Treatment::with([
'cat:id,nama_kucing,foto,treatment_status',
'details.medicine:id,nama_obat,unit',
'user:id,name'
])
->whereHas('cat', function($q) {
$q->where('treatment_status', 1);
});
return view('pages.treatments.index', compact('treatments'));
// 2. LOGIKA FILTERING DINAMIS
if ($request->filled('search')) {
$query->whereHas('cat', function($q) use ($request) {
$q->where('nama_kucing', 'like', '%' . $request->search . '%');
});
}
if ($request->filled('pic_id')) {
$query->where('user_id', $request->pic_id);
}
if ($request->filled('start_date') && $request->filled('end_date')) {
$query->whereBetween('tanggal', [$request->start_date, $request->end_date]);
}
// 3. LOGIKA EXPORT (Pastikan blok IF ditutup dengan benar)
if ($request->filled('export')) {
$exportData = new \App\Exports\TreatmentExport($request);
$fileName = 'Report_Active_Treatment_' . now()->format('Ymd_His');
if ($request->export === 'excel') {
return \Maatwebsite\Excel\Facades\Excel::download($exportData, "{$fileName}.xlsx");
}
if ($request->export === 'pdf') {
return \Maatwebsite\Excel\Facades\Excel::download($exportData, "{$fileName}.pdf", \Maatwebsite\Excel\Excel::DOMPDF);
}
} // <--- Pastikan kurung ini menutup blok export, bukan menutup fungsi index
// 4. EKSEKUSI DATA UNTUK VIEW (Line 67)
$treatments = $query->latest('tanggal')->paginate(20)->withQueryString();
$pics = \App\Models\User::select('id', 'name')->orderBy('name')->get();
return view('pages.treatments.index', compact('treatments', 'pics'));
}
/**
* Form tambah rekam medis.
*/
public function create(Request $request): View
{
$selectedCat = $request->filled('cat_id') ? Cat::findOrFail($request->cat_id) : null;
$cats = Cat::select('id', 'nama_kucing', 'ras')->get();
$medicines = Medicine::where('qty', '>', 0)->get();
$selectedCat = null;
if ($request->filled('cat_id')) {
$selectedCat = Cat::select('id', 'nama_kucing', 'ras')->findOrFail($request->cat_id);
$cats = collect([$selectedCat]);
} else {
$cats = Cat::select('id', 'nama_kucing', 'ras')->get();
}
// Menampilkan semua obat (termasuk stok 0 untuk kebutuhan edit/view,
// namun di filter di frontend untuk input baru)
$medicines = Medicine::orderBy('nama_obat', 'asc')->get();
return view('pages.treatments.create', compact('selectedCat', 'cats', 'medicines'));
}
/**
* Simpan rekam medis baru & potong stok (mendukung desimal).
* Simpan rekam medis baru & potong stok (Mendukung Multi-Obat).
*/
public function store(Request $request): RedirectResponse
public function store(Request $request): \Illuminate\Http\RedirectResponse
{
$validated = $this->validateTreatment($request);
$fotoPath = null;
if ($request->hasFile('foto_bukti')) {
$fotoPath = $request->file('foto_bukti')->store('treatments', 'public');
}
try {
DB::transaction(function () use ($request, &$validated) {
$medicine = Medicine::findOrFail($request->medicine_id);
// Validasi kecukupan stok desimal
if ($medicine->qty < $request->qty_used) {
throw new Exception("Stok {$medicine->nama_obat} tidak mencukupi (Sisa: {$medicine->qty} {$medicine->unit}).");
}
if ($request->hasFile('foto_bukti')) {
$validated['foto_bukti'] = $request->file('foto_bukti')->store('treatments', 'public');
}
// Otomatisasi data sistem
$validated['user_id'] = auth()->id();
$validated['nama_obat'] = $medicine->nama_obat;
return \Illuminate\Support\Facades\DB::transaction(function () use ($request, $fotoPath) {
$treatment = Treatment::create($validated);
$userId = auth()->id() ?? 1;
$firstTreatment = null;
// Potong stok (decrement mendukung nilai float/decimal)
$medicine->decrement('qty', $request->qty_used);
if ($request->filled('items')) {
foreach ($request->items as $item) {
$medicine = \App\Models\Medicine::findOrFail($item['medicine_id']);
$this->logAction('TREATED', $treatment, "Mencatat pengobatan oleh " . auth()->user()->name);
if ($medicine->qty < $item['qty_used']) {
throw new \Exception("Stok {$medicine->nama_obat} tidak mencukupi (Sisa: {$medicine->qty}).");
}
$treatment = \App\Models\Treatment::create([
'cat_id' => $request->cat_id,
'user_id' => $userId,
'tanggal' => $request->tanggal,
'keluhan' => $request->keluhan,
// Langsung masukkan diagnosa tanpa embel-embel
'diagnosa' => $request->diagnosa,
'foto_bukti' => $fotoPath,
'status_perawatan' => 1,
'medicine_id' => $item['medicine_id'],
'qty_used' => $item['qty_used'],
]);
if (!$firstTreatment) {
$firstTreatment = $treatment;
}
$medicine->decrement('qty', $item['qty_used']);
}
}
\App\Models\Cat::where('id', $request->cat_id)->update(['treatment_status' => 1]);
if ($firstTreatment) {
$this->logAction('TREATED', $firstTreatment, "Mencatat pengobatan klinis untuk " . $firstTreatment->cat->nama_kucing);
}
return redirect()->route('cats.show', $request->cat_id)
->with('success', 'Rekam medis klinis berhasil dicatat dan stok dipotong otomatis.');
});
return redirect()->route('cats.show', $request->cat_id)
->with('success', 'Rekam medis berhasil dicatat dengan presisi.');
} catch (Exception $e) {
if (isset($validated['foto_bukti'])) {
Storage::disk('public')->delete($validated['foto_bukti']);
} catch (\Exception $e) {
if ($fotoPath && \Illuminate\Support\Facades\Storage::disk('public')->exists($fotoPath)) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($fotoPath);
}
return back()->withInput()->with('error', 'Gagal menyimpan: ' . $e->getMessage());
}
}
/**
* Menampilkan detail spesifik satu rekam medis.
*/
public function show(Treatment $treatment): View
{
// Load details untuk menampilkan list obat
$treatment->load(['cat', 'medicine', 'user', 'details.medicine']);
return view('pages.treatments.show', compact('treatment'));
}
/**
* Form edit rekam medis.
*/
public function edit(Treatment $treatment): View
{
$treatment->load('details');
$cats = Cat::select('id', 'nama_kucing', 'ras')->get();
$medicines = Medicine::all();
$medicines = Medicine::orderBy('nama_obat', 'asc')->get();
return view('pages.treatments.edit', compact('treatment', 'cats', 'medicines'));
}
/**
* Update rekam medis & sinkronisasi ulang stok desimal.
* Update rekam medis & sinkronisasi ulang stok desimal (Multi-Obat).
*/
public function update(Request $request, Treatment $treatment): RedirectResponse
public function update(Request $request, Treatment $treatment)
{
// 1. Validasi Input
$validated = $this->validateTreatment($request);
try {
DB::transaction(function () use ($request, $treatment, &$validated) {
// 1. Kembalikan stok lama (Desimal Revert)
$oldMedicine = Medicine::find($treatment->medicine_id);
return DB::transaction(function () use ($request, $treatment) {
// 2. Kelola Foto Bukti (Hapus yang lama jika ada foto baru)
$fotoPath = $treatment->foto_bukti;
if ($request->hasFile('foto_bukti')) {
if ($fotoPath && \Illuminate\Support\Facades\Storage::disk('public')->exists($fotoPath)) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($fotoPath);
}
$fotoPath = $request->file('foto_bukti')->store('treatments', 'public');
}
// 3. KEMBALIKAN STOK OBAT LAMA (Penting agar stok tidak bocor)
$oldMedicine = \App\Models\Medicine::find($treatment->medicine_id);
if ($oldMedicine) {
$oldMedicine->increment('qty', $treatment->qty_used);
}
// 2. Potong stok baru
$newMedicine = Medicine::findOrFail($request->medicine_id);
if ($newMedicine->qty < $request->qty_used) {
throw new Exception("Stok {$newMedicine->nama_obat} tidak mencukupi untuk perubahan ini.");
}
$newMedicine->decrement('qty', $request->qty_used);
// 4. Ambil data obat PERTAMA dari form untuk memperbarui data saat ini (ID 9)
$firstItem = $request->items[0];
$newMedicine = \App\Models\Medicine::findOrFail($firstItem['medicine_id']);
// 3. Kelola Foto
if ($request->hasFile('foto_bukti')) {
if ($treatment->foto_bukti) Storage::disk('public')->delete($treatment->foto_bukti);
$validated['foto_bukti'] = $request->file('foto_bukti')->store('treatments', 'public');
if ($newMedicine->qty < $firstItem['qty_used']) {
throw new Exception("Stok {$newMedicine->nama_obat} tidak mencukupi (Sisa: {$newMedicine->qty}).");
}
// 4. Update data sistem
$validated['user_id'] = auth()->id();
$validated['nama_obat'] = $newMedicine->nama_obat;
$treatment->update($validated);
// Format Diagnosa (Gabungan Diagnosa Utama + Dosis/Ket)
$catatanDosis = isset($firstItem['dosis']) ? " | Dosis: " . $firstItem['dosis'] : "";
$catatanKet = isset($firstItem['keterangan']) && $firstItem['keterangan'] != '' ? " (" . $firstItem['keterangan'] . ")" : "";
$diagnosaFinal = $request->diagnosa . $catatanDosis . $catatanKet;
$this->logAction('UPDATED_TREATMENT', $treatment, "Diperbarui oleh " . auth()->user()->name);
// EKSEKUSI UPDATE REKAM MEDIS (Tanpa kolom nama_obat!)
$treatment->update([
'cat_id' => $request->cat_id,
'tanggal' => $request->tanggal,
'keluhan' => $request->keluhan,
'diagnosa' => $diagnosaFinal,
'foto_bukti' => $fotoPath,
'medicine_id' => $firstItem['medicine_id'],
'qty_used' => $firstItem['qty_used'],
]);
// Potong stok obat pertama
$newMedicine->decrement('qty', $firstItem['qty_used']);
// 5. JIKA STAF MENAMBAHKAN OBAT EKSTRA DI HALAMAN EDIT
// Buat sebagai record baru agar data tidak hilang
if (count($request->items) > 1) {
for ($i = 1; $i < count($request->items); $i++) {
$extraItem = $request->items[$i];
$extraMed = \App\Models\Medicine::findOrFail($extraItem['medicine_id']);
if ($extraMed->qty < $extraItem['qty_used']) {
throw new Exception("Stok {$extraMed->nama_obat} tidak mencukupi.");
}
$catatanDosisEx = isset($extraItem['dosis']) ? " | Dosis: " . $extraItem['dosis'] : "";
$catatanKetEx = isset($extraItem['keterangan']) && $extraItem['keterangan'] != '' ? " (" . $extraItem['keterangan'] . ")" : "";
$diagnosaFinalEx = $request->diagnosa . $catatanDosisEx . $catatanKetEx;
\App\Models\Treatment::create([
'cat_id' => $request->cat_id,
'user_id' => auth()->id() ?? 1,
'tanggal' => $request->tanggal,
'keluhan' => $request->keluhan,
'diagnosa' => $diagnosaFinalEx,
'foto_bukti' => $fotoPath,
'status_perawatan' => $treatment->status_perawatan, // Ikuti status (Misal: 1 / Aktif)
'medicine_id' => $extraItem['medicine_id'],
'qty_used' => $extraItem['qty_used'],
]);
$extraMed->decrement('qty', $extraItem['qty_used']);
}
}
// 6. Sinkronisasi Kucing & Log Activity
\App\Models\Cat::where('id', $request->cat_id)->update(['treatment_status' => 1]);
$this->logAction('UPDATED', $treatment, "Memperbarui rekam medis klinis untuk " . $treatment->cat->nama_kucing);
return redirect()->route('cats.show', $request->cat_id)
->with('success', 'Rekam medis berhasil diperbarui dan stok obat disesuaikan ulang secara otomatis!');
});
return redirect()->route('cats.show', $request->cat_id)->with('success', 'Rekam medis & inventori berhasil di-sinkronisasi.');
} catch (Exception $e) {
return back()->withInput()->with('error', 'Gagal memperbarui: ' . $e->getMessage());
}
}
/**
* Hapus rekam medis & kembalikan stok.
* Hapus rekam medis & kembalikan semua stok obat terkait.
*/
public function destroy(Treatment $treatment): RedirectResponse
{
try {
DB::transaction(function () use ($treatment) {
if ($treatment->medicine_id) {
Medicine::where('id', $treatment->medicine_id)->increment('qty', $treatment->qty_used);
// Kembalikan semua stok dari tabel detail
foreach ($treatment->details as $detail) {
Medicine::where('id', $detail->medicine_id)->increment('qty', $detail->qty_used);
}
if ($treatment->foto_bukti) {
Storage::disk('public')->delete($treatment->foto_bukti);
}
if ($treatment->foto_bukti) Storage::disk('public')->delete($treatment->foto_bukti);
$this->logAction('DELETED_TREATMENT', $treatment, "Menghapus catatan medis kucing: {$treatment->cat->nama_kucing}");
// Delete otomatis menghapus details karena cascade di migration
$treatment->delete();
});
return back()->with('success', 'Data rekam medis dihapus dan stok dikembalikan.');
return back()->with('success', 'Data rekam medis dihapus dan semua stok obat dikembalikan.');
} catch (Exception $e) {
return back()->with('error', 'Gagal menghapus data.');
return back()->with('error', 'Gagal menghapus data: ' . $e->getMessage());
}
}
/**
* Helper Validasi: Terpusat untuk Clean Code & Mendukung Desimal.
*/
private function validateTreatment(Request $request)
private function validateTreatment(Request $request)
{
return $request->validate([
'cat_id' => 'required|exists:cats,id',
'medicine_id' => 'required|exists:medicines,id',
// 'numeric' mengizinkan input desimal (koma)
'qty_used' => 'required|numeric|min:0.01',
'tanggal' => 'required|date',
'keluhan' => 'required|string|max:255',
'diagnosa' => 'required|string',
'dosis' => 'required|string',
'keterangan' => 'nullable|string',
'foto_bukti' => 'nullable|image|max:2048',
// Validasi untuk Single Medicine
'medicine_id' => 'required_without:items|nullable|exists:medicines,id',
'qty_used' => 'required_without:items|nullable|numeric|min:0',
// Validasi untuk Multi Medicine
// FIX: Wajibkan array items jika mode single (medicine_id) kosong
'items' => 'required_without:medicine_id|nullable|array',
'items.*.medicine_id' => 'required_with:items|exists:medicines,id',
'items.*.qty_used' => 'required_with:items|numeric|min:0.01',
], [
// Pesan Error Custom (Opsional, agar lebih mudah di-debug)
'items.required_without' => 'Anda harus memasukkan minimal 1 obat.',
'items.*.medicine_id.required_with' => 'Ada pilihan obat yang masih kosong di daftar.',
'items.*.qty_used.required_with' => 'Jumlah (Qty) obat harus diisi.',
]);
}
}