Initial commit: Struktur aplikasi ilycats
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|View
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Breeding;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\{Matting, Cat, Kitten};
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\{Request, RedirectResponse, JsonResponse};
|
||||
use Illuminate\Support\Facades\{DB, Storage};
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MattingController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$mattings = Matting::with(['sire', 'dam'])
|
||||
->withCount('kittens')
|
||||
->latest()
|
||||
->paginate(15);
|
||||
|
||||
return view('pages.matting.index', compact('mattings'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$sires = Cat::where('jenis_kelamin', 'Jantan')->get(['id', 'nama_kucing', 'ras']);
|
||||
$dams = Cat::where('jenis_kelamin', 'Betina')->get(['id', 'nama_kucing', 'ras']);
|
||||
return view('pages.matting.create', compact('sires', 'dams'));
|
||||
}
|
||||
|
||||
public function getCatData($id): JsonResponse
|
||||
{
|
||||
$cat = Cat::find($id);
|
||||
if (!$cat) return response()->json(['error' => 'Kucing tidak ditemukan'], 404);
|
||||
|
||||
return response()->json([
|
||||
'tgl_lahir' => $cat->tanggal_lahir ? $cat->tanggal_lahir->format('Y-m-d') : '',
|
||||
'pedigree' => $cat->pedigree ?? '',
|
||||
'microchip' => $cat->microchip ?? '',
|
||||
'kode_ems' => $cat->kode_ems ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* STORE: Menyimpan Data Breeding Baru
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validateMatting($request);
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
// 1. Sinkronisasi Data Induk (Update profil bapak/ibu di tabel cats)
|
||||
$this->syncParentData($request);
|
||||
|
||||
// 2. Simpan Transaksi Matting
|
||||
$matting = Matting::create(array_merge($request->except('kittens'), [
|
||||
'tanggal_kawin' => $request->tgl_kawin_awal,
|
||||
'user_id' => auth()->id(),
|
||||
'ems_bapak' => $request->ems_bapak,
|
||||
'ems_ibu' => $request->ems_ibu,
|
||||
]));
|
||||
|
||||
// 3. Simpan Kittens
|
||||
if ($request->has('kittens')) {
|
||||
foreach ($request->kittens as $index => $data) {
|
||||
$this->saveKittenData($matting, $data, $index, $request);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logAction('MATTING_CREATED', $matting, "Registrasi Breeding Baru");
|
||||
return redirect()->route('matting.index')->with('success', 'Data Breeding & Anak berhasil disimpan!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* EDIT: Menampilkan Form Edit (Metode yang tadi hilang)
|
||||
*/
|
||||
public function edit(Matting $matting): View
|
||||
{
|
||||
// Load relasi agar data anak muncul di form edit
|
||||
$matting->load('kittens.cat');
|
||||
|
||||
$sires = Cat::where('jenis_kelamin', 'Jantan')->get(['id', 'nama_kucing', 'ras']);
|
||||
$dams = Cat::where('jenis_kelamin', 'Betina')->get(['id', 'nama_kucing', 'ras']);
|
||||
|
||||
return view('pages.matting.edit', compact('matting', 'sires', 'dams'));
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE: Memperbarui Data Breeding & Sinkronisasi Ulang Kittens
|
||||
*/
|
||||
public function update(Request $request, Matting $matting): RedirectResponse
|
||||
{
|
||||
$this->validateMatting($request);
|
||||
|
||||
return DB::transaction(function () use ($request, $matting) {
|
||||
// 1. Update profil Bapak/Ibu di tabel 'cats'
|
||||
$this->syncParentData($request);
|
||||
|
||||
// 2. Update data di tabel 'mattings'
|
||||
$matting->update(array_merge($request->except('kittens'), [
|
||||
'tanggal_kawin' => $request->tgl_kawin_awal,
|
||||
'ems_bapak' => $request->ems_bapak,
|
||||
'ems_ibu' => $request->ems_ibu,
|
||||
]));
|
||||
|
||||
// 3. Sinkronisasi Kittens (CRUD Logic)
|
||||
$requestKittenIds = collect($request->kittens)->pluck('id')->filter()->toArray();
|
||||
|
||||
// Hapus Kitten yang tidak ada di form lagi
|
||||
$kittensToDelete = $matting->kittens()->whereNotIn('id', $requestKittenIds)->get();
|
||||
foreach ($kittensToDelete as $kDelete) {
|
||||
if ($kDelete->cat_id) Cat::find($kDelete->cat_id)?->delete();
|
||||
$kDelete->delete();
|
||||
}
|
||||
|
||||
if ($request->has('kittens')) {
|
||||
foreach ($request->kittens as $index => $data) {
|
||||
if (isset($data['id']) && !empty($data['id'])) {
|
||||
$kitten = Kitten::find($data['id']);
|
||||
if ($kitten) {
|
||||
// Update profil master anak di tabel 'cats'
|
||||
Cat::where('id', $kitten->cat_id)->update([
|
||||
'nama_kucing' => $data['nama'],
|
||||
'jenis_kelamin' => $data['jenis_kelamin'],
|
||||
'tanggal_lahir' => $data['tgl_lahir'],
|
||||
'kode_ems' => $data['kode_ems'] ?? null,
|
||||
'registry' => $data['registry'] ?? [],
|
||||
]);
|
||||
// Update record silsilah di tabel 'kittens'
|
||||
$kitten->update([
|
||||
'nama' => $data['nama'],
|
||||
'tgl_lahir' => $data['tgl_lahir'],
|
||||
'jenis_kelamin' => $data['jenis_kelamin'],
|
||||
'kode_ems' => $data['kode_ems'] ?? null,
|
||||
'is_pedigree' => isset($data['is_pedigree']),
|
||||
'registry' => $data['registry'] ?? [],
|
||||
]);
|
||||
$this->handlePedigreeUploads($kitten, $index, $request);
|
||||
}
|
||||
} else {
|
||||
$this->saveKittenData($matting, $data, $index, $request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logAction('MATTING_UPDATED', $matting, "Update Breeding Record");
|
||||
return redirect()->route('matting.index')->with('success', 'Perubahan berhasil disimpan!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SINKRONISASI DATA INDUK (EMS Fix)
|
||||
*/
|
||||
private function syncParentData(Request $request)
|
||||
{
|
||||
Cat::where('id', $request->sire_id)->update([
|
||||
'pedigree' => $request->no_pedigree_bapak,
|
||||
'microchip' => $request->no_microchip_bapak,
|
||||
'tanggal_lahir' => $request->tgl_lahir_sire,
|
||||
'kode_ems' => $request->ems_bapak // Sinkron EMS Bapak
|
||||
]);
|
||||
|
||||
Cat::where('id', $request->dam_id)->update([
|
||||
'pedigree' => $request->no_pedigree_ibu,
|
||||
'microchip' => $request->no_microchip_ibu,
|
||||
'tanggal_lahir' => $request->tgl_lahir_dam,
|
||||
'kode_ems' => $request->ems_ibu // Sinkron EMS Ibu
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SIMPAN DATA KITTEN (EMS & Registry Fix)
|
||||
*/
|
||||
private function saveKittenData($matting, $data, $index, $request)
|
||||
{
|
||||
$newCat = Cat::create([
|
||||
'nama_kucing' => $data['nama'],
|
||||
'ras' => $matting->dam->ras ?? 'Persian',
|
||||
'jenis_kelamin' => $data['jenis_kelamin'],
|
||||
'induk_jantan_id' => $matting->sire_id,
|
||||
'induk_betina_id' => $matting->dam_id,
|
||||
'status' => 'Kitten',
|
||||
'tanggal_lahir' => $data['tgl_lahir'],
|
||||
'kode_ems' => $data['kode_ems'] ?? null,
|
||||
'registry' => $data['registry'] ?? [],
|
||||
'pic_id' => auth()->id(),
|
||||
'lokasi_kandang' => $matting->dam->lokasi_kandang ?? 'Lorong',
|
||||
]);
|
||||
|
||||
$kitten = new Kitten([
|
||||
'cat_id' => $newCat->id,
|
||||
'matting_id' => $matting->id,
|
||||
'nama' => $data['nama'],
|
||||
'tgl_lahir' => $data['tgl_lahir'],
|
||||
'jenis_kelamin' => $data['jenis_kelamin'],
|
||||
'kode_ems' => $data['kode_ems'] ?? null,
|
||||
'is_pedigree' => isset($data['is_pedigree']),
|
||||
'registry' => $data['registry'] ?? [],
|
||||
]);
|
||||
|
||||
$this->handlePedigreeUploads($kitten, $index, $request);
|
||||
$kitten->save();
|
||||
}
|
||||
|
||||
private function handlePedigreeUploads($kitten, $index, $request)
|
||||
{
|
||||
for ($i = 1; $i <= 4; $i++) {
|
||||
$field = "foto_ped_$i";
|
||||
if ($request->hasFile("kittens.$index.$field")) {
|
||||
if ($kitten->$field) Storage::disk('public')->delete($kitten->$field);
|
||||
$kitten->$field = $request->file("kittens.$index.$field")->store('kittens/pedigrees', 'public');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(Matting $matting): RedirectResponse
|
||||
{
|
||||
return DB::transaction(function () use ($matting) {
|
||||
try {
|
||||
$pairName = ($matting->sire->nama_kucing ?? 'Sire') . ' x ' . ($matting->dam->nama_kucing ?? 'Dam');
|
||||
|
||||
// 1. Loop setiap data anak yang terhubung
|
||||
foreach ($matting->kittens as $kitten) {
|
||||
if ($kitten->cat_id) {
|
||||
$cat = Cat::find($kitten->cat_id);
|
||||
|
||||
if ($cat) {
|
||||
// Cek apakah anak ini punya data medis atau riwayat berat
|
||||
$hasMedical = $cat->treatments()->exists();
|
||||
$hasWeight = $cat->weightHistories()->exists();
|
||||
|
||||
if ($hasMedical || $hasWeight) {
|
||||
// JIKA ADA DATA TERKAIT, BATALKAN DAN BERI INFO
|
||||
return back()->with('error', "Gagal menghapus! Anak ({$cat->nama_kucing}) sudah memiliki data Rekam Medis/Berat Badan. Hapus data medis anak tersebut terlebih dahulu.");
|
||||
}
|
||||
|
||||
$cat->delete();
|
||||
}
|
||||
}
|
||||
$kitten->delete();
|
||||
}
|
||||
|
||||
// 2. Hapus data Matting utama
|
||||
$matting->delete();
|
||||
|
||||
$this->logAction('MATTING_DELETED', $matting, "Menghapus riwayat breeding: $pairName");
|
||||
|
||||
return redirect()->route('matting.index')->with('success', "Riwayat $pairName berhasil dihapus.");
|
||||
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Menangkap error database (Foreign Key Constraint)
|
||||
return back()->with('error', "Sistem tidak dapat menghapus data ini karena integritas database terganggu. Silakan periksa data anak yang terkait.");
|
||||
}
|
||||
});
|
||||
}
|
||||
private function validateMatting(Request $request)
|
||||
{
|
||||
return $request->validate([
|
||||
'sire_id' => 'required|exists:cats,id',
|
||||
'dam_id' => 'required|exists:cats,id',
|
||||
'tgl_kawin_awal' => 'required|date',
|
||||
'status' => 'required|in:proses,berhasil,gagal',
|
||||
'ems_bapak' => 'nullable|string',
|
||||
'ems_ibu' => 'nullable|string',
|
||||
'no_pedigree_bapak' => 'nullable|string',
|
||||
'no_pedigree_ibu' => 'nullable|string',
|
||||
'no_microchip_bapak'=> 'nullable|string',
|
||||
'no_microchip_ibu' => 'nullable|string',
|
||||
'kittens' => 'nullable|array',
|
||||
'kittens.*.nama' => 'required_with:kittens|string',
|
||||
'kittens.*.kode_ems'=> 'nullable|string',
|
||||
'kittens.*.registry'=> 'nullable|array',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\{Cat, User, Matting};
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Support\Facades\{Storage, DB, Gate}; // Tambahkan Gate
|
||||
use Illuminate\Support\Carbon;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use SimpleSoftwareIO\QrCode\Facades\QrCode;
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
|
||||
class CatController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Menampilkan daftar kucing dengan filter cerdas.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
// Proteksi Akses: Read
|
||||
Gate::authorize('check-access', ['cats', 'read']);
|
||||
|
||||
$query = Cat::query()->with(['pic', 'sire', 'dam']);
|
||||
|
||||
// 1. Pencarian Global
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('nama_kucing', 'like', "%{$search}%")
|
||||
->orWhere('ras', 'like', "%{$search}%")
|
||||
->orWhere('microchip', 'like', "%{$search}%")
|
||||
->orWhere('pedigree', 'like', "%{$search}%")
|
||||
->orWhere('kode_ems', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Filter Spesifik
|
||||
if ($request->filled('status')) $query->where('status', $request->status);
|
||||
if ($request->filled('ras')) $query->where('ras', $request->ras);
|
||||
if ($request->filled('gender')) $query->where('jenis_kelamin', $request->gender);
|
||||
|
||||
$cats = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
// 3. Statistik Dashboard (Tetap Utuh)
|
||||
$totalCats = Cat::count();
|
||||
$safeWords = ['sehat', 'normal', 'aktif', 'sembuh', '-', 'tidak ada', 'clean'];
|
||||
$sickCats = Cat::whereNotNull('riwayat_sakit_terakhir')
|
||||
->where(function($q) use ($safeWords) {
|
||||
foreach ($safeWords as $word) {
|
||||
$q->where('riwayat_sakit_terakhir', 'NOT LIKE', "%{$word}%");
|
||||
}
|
||||
})->count();
|
||||
|
||||
$breeds = Cat::distinct()->pluck('ras');
|
||||
|
||||
return view('pages.cats.index', compact('cats', 'totalCats', 'sickCats', 'breeds'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
// Proteksi Akses: Create
|
||||
Gate::authorize('check-access', ['cats', 'create']);
|
||||
|
||||
$sires = Cat::where('jenis_kelamin', 'Jantan')->get(['id', 'nama_kucing']);
|
||||
$dams = Cat::where('jenis_kelamin', 'Betina')->get(['id', 'nama_kucing']);
|
||||
$users = User::all(['id', 'name']);
|
||||
|
||||
return view('pages.cats.create', compact('sires', 'dams', 'users'));
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['cats', 'create']);
|
||||
|
||||
$validated = $this->validateCat($request);
|
||||
|
||||
return DB::transaction(function () use ($request, $validated) {
|
||||
if ($request->hasFile('foto')) {
|
||||
$validated['foto'] = $this->handleImageUpload($request->file('foto'));
|
||||
}
|
||||
|
||||
if (isset($validated['berat_badan'])) {
|
||||
$validated['berat_terakhir'] = $validated['berat_badan'];
|
||||
}
|
||||
|
||||
if (empty($validated['status'])) {
|
||||
$validated['status'] = ($request->induk_jantan_id || $request->induk_betina_id) ? 'Kitten' : 'Active';
|
||||
}
|
||||
|
||||
// Memastikan registry disimpan sebagai JSON jika model tidak melakukan casting
|
||||
if (isset($validated['registry'])) {
|
||||
$validated['registry'] = $validated['registry'];
|
||||
}
|
||||
|
||||
$cat = Cat::create($validated);
|
||||
$this->logAction('CREATED', $cat, "Mendaftarkan pasien baru: {$cat->nama_kucing}", 'Cat');
|
||||
|
||||
return redirect()->route('cats.index')->with('success', "{$cat->nama_kucing} berhasil didaftarkan.");
|
||||
});
|
||||
}
|
||||
|
||||
public function show(Cat $cat): View
|
||||
{
|
||||
Gate::authorize('check-access', ['cats', 'read']);
|
||||
|
||||
$cat->load([
|
||||
'treatments' => fn($q) => $q->latest(),
|
||||
'sire', 'dam', 'pic',
|
||||
'weightHistories' => fn($q) => $q->latest()->limit(12)
|
||||
]);
|
||||
return view('pages.cats.show', compact('cat'));
|
||||
}
|
||||
|
||||
public function edit(Cat $cat): View
|
||||
{
|
||||
// Proteksi: Akses Update
|
||||
Gate::authorize('check-access', ['cats', 'update']);
|
||||
|
||||
$sires = Cat::where('jenis_kelamin', 'Jantan')->where('id', '!=', $cat->id)->get(['id', 'nama_kucing']);
|
||||
$dams = Cat::where('jenis_kelamin', 'Betina')->where('id', '!=', $cat->id)->get(['id', 'nama_kucing']);
|
||||
$users = User::all(['id', 'name']);
|
||||
|
||||
return view('pages.cats.edit', compact('cat', 'sires', 'dams', 'users'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Cat $cat): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['cats', 'update']);
|
||||
|
||||
$validated = $this->validateCat($request, $cat->id);
|
||||
|
||||
return DB::transaction(function () use ($request, $validated, $cat) {
|
||||
if ($request->hasFile('foto')) {
|
||||
if ($cat->foto) Storage::disk('public')->delete($cat->foto);
|
||||
$validated['foto'] = $this->handleImageUpload($request->file('foto'));
|
||||
}
|
||||
|
||||
if ($request->filled('berat_badan')) {
|
||||
$validated['berat_terakhir'] = $request->berat_badan;
|
||||
if ($cat->berat_badan != $request->berat_badan) {
|
||||
$cat->weightHistories()->create(['berat' => $request->berat_badan]);
|
||||
}
|
||||
}
|
||||
|
||||
$cat->update($validated);
|
||||
$this->logAction('UPDATED', $cat, "Update data medis/profil: {$cat->nama_kucing}", 'Cat');
|
||||
|
||||
return redirect()->route('cats.index')->with('success', 'Data berhasil diperbarui.');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(Cat $cat): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['cats', 'delete']);
|
||||
|
||||
$isParent = Matting::where('sire_id', $cat->id)->orWhere('dam_id', $cat->id)->exists();
|
||||
|
||||
if ($isParent) {
|
||||
return back()->with('error', "Gagal! {$cat->nama_kucing} tidak bisa dihapus karena sudah menjadi Induk.");
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($cat) {
|
||||
$name = $cat->nama_kucing;
|
||||
if ($cat->foto) Storage::disk('public')->delete($cat->foto);
|
||||
|
||||
$this->logAction('DELETED', $cat, "Menghapus data permanen: {$name}", 'Cat');
|
||||
$cat->delete();
|
||||
|
||||
return redirect()->route('cats.index')->with('success', "Data {$name} telah dihapus.");
|
||||
});
|
||||
}
|
||||
|
||||
public function getCatData($id): JsonResponse
|
||||
{
|
||||
$cat = Cat::find($id);
|
||||
if (!$cat) return response()->json(['error' => 'Not found'], 404);
|
||||
|
||||
return response()->json([
|
||||
'tgl_lahir' => $cat->tanggal_lahir ? $cat->tanggal_lahir->format('Y-m-d') : '',
|
||||
'pedigree' => $cat->pedigree ?? '',
|
||||
'microchip' => $cat->microchip ?? '',
|
||||
'kode_ems' => $cat->kode_ems ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF ID Card / Medical Pass
|
||||
* PENYEMPURNAAN: Perbaikan Barcode (Base64 SVG)
|
||||
*/
|
||||
public function downloadPdf(Cat $cat)
|
||||
{
|
||||
Gate::authorize('check-access', ['cats', 'read']);
|
||||
|
||||
$cat->load(['pic', 'sire', 'dam']);
|
||||
|
||||
// FIX: Konversi SVG ke Base64 agar tampil di domPDF tanpa Imagick
|
||||
$qrCodeRaw = QrCode::format('svg')
|
||||
->size(150)
|
||||
->margin(1)
|
||||
->generate(route('cats.show', $cat->id));
|
||||
|
||||
$qrCode = base64_encode($qrCodeRaw);
|
||||
|
||||
$pdf = Pdf::loadView('pages.cats.pdf-card', [
|
||||
'cat' => $cat,
|
||||
'qrCode' => $qrCode, // Dikirim sebagai Base64
|
||||
'printedAt' => Carbon::now()->translatedFormat('d F Y H:i')
|
||||
]);
|
||||
|
||||
return $pdf->setPaper([0, 0, 300, 500], 'portrait')
|
||||
->stream('MEDICAL_PASS_' . strtoupper($cat->nama_kucing) . '.pdf');
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* HELPERS (TETAP UTUH) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
private function validateCat(Request $request, $id = null)
|
||||
{
|
||||
return $request->validate([
|
||||
'nama_kucing' => 'required|string|max:255',
|
||||
'ras' => 'required|string',
|
||||
'jenis_kelamin' => 'required|in:Jantan,Betina',
|
||||
'tanggal_lahir' => 'nullable|date',
|
||||
'foto' => 'nullable|image|mimes:jpg,jpeg,png|max:3072',
|
||||
'registry' => 'nullable|array',
|
||||
'registry.*' => 'string',
|
||||
'pedigree' => 'nullable|string|max:255',
|
||||
'microchip' => 'nullable|string|max:255',
|
||||
'kode_ems' => 'nullable|string|max:255',
|
||||
'status' => 'nullable|string',
|
||||
'induk_jantan_id' => 'nullable|exists:cats,id',
|
||||
'induk_betina_id' => 'nullable|exists:cats,id',
|
||||
'lokasi_kandang' => 'nullable|string',
|
||||
'pic_id' => 'nullable|exists:users,id',
|
||||
'berat_badan' => 'nullable|numeric|min:0',
|
||||
'riwayat_sakit_terakhir' => 'nullable|string',
|
||||
'jadwal_vaksin' => 'nullable|date',
|
||||
'jadwal_obat_cacing' => 'nullable|date',
|
||||
'jadwal_obat_kutu' => 'nullable|date',
|
||||
'jadwal_grooming' => 'nullable|date',
|
||||
]);
|
||||
}
|
||||
|
||||
private function handleImageUpload($file): string
|
||||
{
|
||||
$filename = 'cat_' . uniqid() . '.png';
|
||||
$path = 'cats/' . $filename;
|
||||
$img = Image::read($file)->cover(500, 500, 'center')->toPng();
|
||||
Storage::disk('public')->put($path, (string) $img);
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Medicine;
|
||||
use App\Models\Food;
|
||||
use App\Models\Supply;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Menampilkan daftar kategori.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// Mengambil kategori beserta hitungan item di dalamnya (optional untuk insight)
|
||||
$categories = Category::latest()->get();
|
||||
return view('pages.inventory.categories.index', compact('categories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menyimpan kategori baru.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:categories,name',
|
||||
'keywords' => 'required|string'
|
||||
]);
|
||||
|
||||
$category = Category::create($validated);
|
||||
|
||||
$this->logAction('CREATED_CATEGORY', $category, "Menambah kategori master: {$category->name}");
|
||||
|
||||
return back()->with('success', 'Kategori baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan halaman edit (Hanya Admin).
|
||||
*/
|
||||
public function edit(Category $category): View
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Akses Ditolak.');
|
||||
}
|
||||
|
||||
return view('pages.inventory.categories.edit', compact('category'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Memperbarui data kategori.
|
||||
*/
|
||||
public function update(Request $request, Category $category): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:categories,name,' . $category->id,
|
||||
'keywords' => 'required|string',
|
||||
]);
|
||||
|
||||
$category->update($validated);
|
||||
|
||||
$this->logAction('UPDATED_CATEGORY', $category, "Memperbarui data kategori: {$category->name}");
|
||||
|
||||
return redirect()->route('categories.index')
|
||||
->with('success', "Kategori {$category->name} berhasil diperbarui.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sinkronisasi massal seluruh inventori berdasarkan kata kunci.
|
||||
*/
|
||||
public function recategorizeAll(): RedirectResponse
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Hanya Admin yang dapat melakukan sinkronisasi ulang.');
|
||||
}
|
||||
|
||||
try {
|
||||
$categories = Category::all();
|
||||
$updatedCount = 0;
|
||||
|
||||
// 1. Ambil semua data dari 3 modul inventori
|
||||
$inventories = [
|
||||
'Medicine' => Medicine::all(),
|
||||
'Food' => Food::all(),
|
||||
'Supply' => Supply::all(),
|
||||
];
|
||||
|
||||
foreach ($inventories as $type => $items) {
|
||||
foreach ($items as $item) {
|
||||
// Tentukan field nama berdasarkan model (nama_obat, nama_makanan, nama_perlengkapan)
|
||||
$fieldName = $this->getFieldName($type);
|
||||
$name = strtolower($item->$fieldName);
|
||||
$found = false;
|
||||
|
||||
foreach ($categories as $cat) {
|
||||
$keywords = explode(',', strtolower($cat->keywords));
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
$trimmed = trim($keyword);
|
||||
if (!empty($trimmed) && str_contains($name, $trimmed)) {
|
||||
$item->category_id = $cat->id;
|
||||
// Sinkronisasi kolom teks kategori jika ada
|
||||
if (isset($item->kategori)) { $item->kategori = $cat->name; }
|
||||
$item->save();
|
||||
$updatedCount++;
|
||||
$found = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak cocok, arahkan ke kategori 'Umum'
|
||||
if (!$found) {
|
||||
$umum = Category::where('name', 'Umum')->first();
|
||||
$item->category_id = $umum ? $umum->id : null;
|
||||
if (isset($item->kategori)) { $item->kategori = 'Umum'; }
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logAction('RECATEGORIZE_ALL', null, "Melakukan sinkronisasi massal kategori pada seluruh inventori");
|
||||
|
||||
return back()->with('success', "Sinkronisasi selesai! {$updatedCount} item berhasil diperbarui kategorinya.");
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', 'Gagal sinkronisasi: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper untuk menentukan field nama produk
|
||||
*/
|
||||
private function getFieldName($type) {
|
||||
return match($type) {
|
||||
'Medicine' => 'nama_obat',
|
||||
'Food' => 'nama_makanan',
|
||||
'Supply' => 'nama_perlengkapan',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghapus kategori (Hanya Admin).
|
||||
*/
|
||||
public function destroy(Category $category): RedirectResponse
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Akses Ditolak.');
|
||||
}
|
||||
|
||||
$categoryName = $category->name;
|
||||
|
||||
// Cek apakah masih ada item yang menggunakan kategori ini
|
||||
if ($category->medicines()->exists() || $category->foods()->exists() || $category->supplies()->exists()) {
|
||||
return back()->with('error', "Kategori '{$categoryName}' tidak bisa dihapus karena masih digunakan oleh beberapa item.");
|
||||
}
|
||||
|
||||
$this->logAction('DELETED_CATEGORY', null, "Menghapus kategori: {$categoryName}");
|
||||
|
||||
$category->delete();
|
||||
|
||||
return back()->with('success', 'Kategori berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\{Cat, Medicine, Food, Supply, Matting, Treatment};
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// 1. Statistik Utama
|
||||
$stats = [
|
||||
'total_cats' => Cat::count(),
|
||||
'total_staff' => \App\Models\User::count(),
|
||||
'sick_cats' => Cat::whereNotNull('riwayat_sakit_terakhir')->count(),
|
||||
'total_mating' => Matting::where('status', 'proses')->count(),
|
||||
];
|
||||
|
||||
// 2. Birth Watch: Persalinan 7 Hari ke Depan (Rumus: H+63)
|
||||
$upcomingBirths = Matting::with(['dam', 'sire', 'pic'])
|
||||
->where('status', 'proses')
|
||||
->whereNotNull('tanggal_kawin')
|
||||
->whereBetween('tanggal_kawin', [
|
||||
now()->subDays(70)->toDateString(),
|
||||
now()->subDays(56)->toDateString()
|
||||
])
|
||||
->get()
|
||||
->map(function ($mating) {
|
||||
$mating->edd = Carbon::parse($mating->tanggal_kawin)->addDays(63);
|
||||
$mating->days_to_go = (int) now()->diffInDays($mating->edd, false);
|
||||
return $mating;
|
||||
})
|
||||
->filter(fn($m) => $m->days_to_go >= -2 && $m->days_to_go <= 7)
|
||||
->sortBy('days_to_go');
|
||||
|
||||
// 3. Medical Alerts
|
||||
$medicalAlerts = [
|
||||
'vaccine' => Cat::whereNotNull('jadwal_vaksin')->where('jadwal_vaksin', '<=', now()->addDays(3))->count(),
|
||||
'flea' => Cat::whereNotNull('jadwal_obat_kutu')->where('jadwal_obat_kutu', '<=', now()->addDays(3))->count(),
|
||||
'deworming' => Cat::whereNotNull('jadwal_obat_cacing')->where('jadwal_obat_cacing', '<=', now()->addDays(3))->count(),
|
||||
'grooming' => Cat::whereNotNull('jadwal_grooming')->where('jadwal_grooming', '<=', now()->addDays(3))->count(),
|
||||
];
|
||||
|
||||
// 4. Inventory Critical (Stok < 5)
|
||||
$lowStock = [
|
||||
'medicines' => Medicine::where('qty', '<', 5)->get(['nama_obat as name', 'qty', 'unit']),
|
||||
'foods' => Food::where('qty', '<', 5)->get(['nama_makanan as name', 'qty', 'unit']),
|
||||
'supplies' => Supply::where('qty', '<', 5)->get(['nama_perlengkapan as name', 'qty', 'unit']),
|
||||
];
|
||||
$totalLowStock = collect($lowStock)->flatten(1)->count();
|
||||
|
||||
// 5. Chart Data: Sebaran Ras (Persian, British Shorthair, dll)
|
||||
$chartData = Cat::select('ras', DB::raw('count(*) as total'))->groupBy('ras')->get();
|
||||
|
||||
// 6. Weight Trends: Rata-rata Berat (6 Bulan Terakhir)
|
||||
$weightTrends = DB::table('weight_histories')
|
||||
->select(
|
||||
DB::raw('AVG(berat) as avg_weight'),
|
||||
DB::raw("DATE_FORMAT(tanggal_timbang, '%M') as month"),
|
||||
DB::raw('MONTH(tanggal_timbang) as month_num')
|
||||
)
|
||||
->where('tanggal_timbang', '>=', now()->subMonths(6))
|
||||
->groupBy('month', 'month_num')
|
||||
->orderBy('month_num')
|
||||
->get();
|
||||
|
||||
if ($weightTrends->isEmpty()) {
|
||||
$weightTrends = collect([(object)['month' => now()->format('F'), 'avg_weight' => 0]]);
|
||||
}
|
||||
|
||||
// 7. Recent Activity
|
||||
$recentTreatments = Treatment::with(['cat', 'pic'])->latest()->take(5)->get();
|
||||
|
||||
return view('pages.dashboard.index', compact(
|
||||
'stats',
|
||||
'upcomingBirths',
|
||||
'medicalAlerts',
|
||||
'lowStock',
|
||||
'totalLowStock',
|
||||
'chartData',
|
||||
'weightTrends',
|
||||
'recentTreatments'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Food;
|
||||
use App\Models\Category;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FoodController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Tampilkan daftar stok makanan.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Food::with('category');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->where('nama_makanan', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
$foods = $query->latest()->paginate(10);
|
||||
return view('pages.inventory.foods.index', compact('foods'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form tambah.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('pages.inventory.foods.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan data makanan baru.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_makanan' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'expired_date' => 'nullable|date|after_or_equal:today',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('foods', 'public');
|
||||
}
|
||||
|
||||
// Simpan data & tangkap objeknya untuk logging
|
||||
$food = Food::create($validated);
|
||||
|
||||
// Tambahkan Log Aktivitas (Sangat penting untuk audit stok)
|
||||
$this->logAction('CREATED_FOOD', $food, "Menambah stok pakan baru: {$food->nama_makanan}");
|
||||
|
||||
// FIX: Tambahkan .index agar tidak error RouteNotFound
|
||||
return redirect()->route('inventory.foods.index')->with('success', 'Stok pakan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form edit.
|
||||
*/
|
||||
public function edit(Food $food): View
|
||||
{
|
||||
$categories = Category::all();
|
||||
return view('pages.inventory.foods.edit', compact('food', 'categories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data makanan.
|
||||
*/
|
||||
public function update(Request $request, Food $food): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_makanan' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'category_id' => 'nullable|exists:categories,id',
|
||||
'expired_date' => 'nullable|date|after_or_equal:today',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
if ($food->foto_produk) {
|
||||
Storage::disk('public')->delete($food->foto_produk);
|
||||
}
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('foods', 'public');
|
||||
}
|
||||
|
||||
$food->update($validated);
|
||||
|
||||
$this->logAction('UPDATED_FOOD', $food, "Memperbarui stok pakan: {$food->nama_makanan}");
|
||||
|
||||
// FIX: Tambahkan .index
|
||||
return redirect()->route('inventory.foods.index')->with('success', 'Data stok pakan berhasil diperbarui.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus data makanan.
|
||||
*/
|
||||
public function destroy(Food $food): RedirectResponse
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Hanya Admin yang dapat menghapus data stok pakan.');
|
||||
}
|
||||
|
||||
$foodName = $food->nama_makanan;
|
||||
|
||||
if ($food->foto_produk) {
|
||||
Storage::disk('public')->delete($food->foto_produk);
|
||||
}
|
||||
|
||||
// Log dilakukan SEBELUM delete
|
||||
$this->logAction('DELETED_FOOD', $food, "Menghapus data pakan: {$foodName}");
|
||||
|
||||
$food->delete();
|
||||
|
||||
// FIX: Tambahkan .index
|
||||
return redirect()->route('inventory.foods.index')->with('success', 'Stok pakan berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Medicine;
|
||||
use App\Models\Food;
|
||||
use App\Models\Supply;
|
||||
use App\Traits\LogsActivity; // Tambahkan Trait Log
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Exception;
|
||||
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Tampilkan Inventori Obat.
|
||||
*/
|
||||
public function medicines(): View
|
||||
{
|
||||
// Model Medicine sudah mendukung casting expired_date & qty float
|
||||
$medicines = Medicine::latest()->paginate(10);
|
||||
return view('pages.inventory.medicines.index', compact('medicines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan Inventori Makanan.
|
||||
*/
|
||||
public function foods(): View
|
||||
{
|
||||
$foods = Food::latest()->paginate(10);
|
||||
return view('pages.inventory.foods.index', compact('foods'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan Inventori Perlengkapan.
|
||||
*/
|
||||
public function supplies(): View
|
||||
{
|
||||
$supplies = Supply::latest()->paginate(10);
|
||||
return view('pages.inventory.supplies.index', compact('supplies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stok secara cepat (Fast Update).
|
||||
* Mendukung desimal (ml/butir) dan Audit Trail.
|
||||
*/
|
||||
public function updateStock(Request $request, string $type, int $id): RedirectResponse
|
||||
{
|
||||
// Pastikan validasi tetap mendukung desimal untuk obat cair
|
||||
$request->validate([
|
||||
'qty' => 'required|numeric|min:0'
|
||||
]);
|
||||
|
||||
try {
|
||||
$item = match($type) {
|
||||
'medicine' => \App\Models\Medicine::findOrFail($id),
|
||||
'food' => \App\Models\Food::findOrFail($id),
|
||||
'supply' => \App\Models\Supply::findOrFail($id),
|
||||
default => abort(404),
|
||||
};
|
||||
|
||||
$oldQty = $item->qty;
|
||||
$item->update(['qty' => $request->qty]);
|
||||
|
||||
// FIX: Evaluasi nama barang di luar string agar tidak menyebabkan ParseError
|
||||
$itemName = $item->nama_obat ?? $item->nama_makanan ?? $item->nama_barang ?? 'Item';
|
||||
|
||||
$this->logAction(
|
||||
'STOCK_ADJUSTMENT',
|
||||
$item,
|
||||
"Penyesuaian stok " . ucfirst($type) . ": {$itemName} (Dari {$oldQty} ke {$request->qty})"
|
||||
);
|
||||
|
||||
return back()->with('success', 'Stok ' . ucfirst($type) . ' berhasil diperbarui.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
return back()->with('error', 'Gagal memperbarui stok: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\{Matting, User, Cat};
|
||||
use App\Exports\MattingExport;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MattingReportController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// 1. Eager Loading Lengkap (Sire, Dam, PIC, dan Detail Anak)
|
||||
$query = Matting::with(['sire', 'dam', 'pic', 'kittens.cat'])->withCount('kittens');
|
||||
// 2. Filter Nama Indukan (Sire atau Dam)
|
||||
if ($request->filled('parent_name')) {
|
||||
$name = $request->parent_name;
|
||||
$query->where(function($q) use ($name) {
|
||||
$q->whereHas('sire', fn($sq) => $sq->where('nama_kucing', 'like', "%{$name}%"))
|
||||
->orWhereHas('dam', fn($sq) => $sq->where('nama_kucing', 'like', "%{$name}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Filter Ras Dinamis
|
||||
if ($request->filled('ras')) {
|
||||
$ras = $request->ras;
|
||||
$query->where(function($q) use ($ras) {
|
||||
$q->whereHas('sire', fn($sq) => $sq->where('ras', $ras))
|
||||
->orWhereHas('dam', fn($sq) => $sq->where('ras', $ras));
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Filter Registry (ICA, CFA, WCF, dll)
|
||||
if ($request->filled('registry')) {
|
||||
$reg = $request->registry;
|
||||
$query->where(function($q) use ($reg) {
|
||||
$q->where('registry', $reg)
|
||||
->orWhereHas('sire', fn($sq) => $sq->where('registry', $reg))
|
||||
->orWhereHas('dam', fn($sq) => $sq->where('registry', $reg));
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Filter Periode Perkawinan
|
||||
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||
$query->whereBetween('tgl_kawin_awal', [
|
||||
Carbon::parse($request->start_date)->startOfDay(),
|
||||
Carbon::parse($request->end_date)->endOfDay()
|
||||
]);
|
||||
}
|
||||
|
||||
// 6. Filter Status Induk Betina
|
||||
if ($request->filled('status')) {
|
||||
$status = $request->status;
|
||||
$query->whereHas('dam', fn($q) => $q->where('status', $status));
|
||||
}
|
||||
|
||||
// 7. Handle Export
|
||||
if ($request->filled('export')) {
|
||||
$data = $query->latest()->get();
|
||||
return $this->processExport($data, $request->export);
|
||||
}
|
||||
|
||||
// 8. Eksekusi dengan Pagination
|
||||
$mattings = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
// 9. Data Pendukung UI (Agar Dropdown di Blade berfungsi)
|
||||
$breeds = Cat::distinct()->whereNotNull('ras')->pluck('ras');
|
||||
$registries = ['ICA', 'CFA', 'WCF', 'FFF', 'LOOF'];
|
||||
$statuses = ['Breeding', 'Hamil', 'Teradopsi', 'RIP'];
|
||||
|
||||
return view('pages.reports.matting.index', compact(
|
||||
'mattings', 'breeds', 'registries', 'statuses'
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
protected function processExport($data, $format)
|
||||
{
|
||||
$fileName = "Lineage_Report_" . now()->format('Ymd_His');
|
||||
|
||||
if ($format === 'xlsx') {
|
||||
return Excel::download(new MattingExport($data), "{$fileName}.xlsx");
|
||||
}
|
||||
|
||||
// Penyempurnaan PDF: Tambahkan opsi font dan akses file lokal (untuk logo)
|
||||
$pdf = Pdf::loadView('pages.reports.matting.pdf', [
|
||||
'data' => $data,
|
||||
// Kirim data setting jika ingin menampilkan logo/branding dinamis di PDF
|
||||
'setting' => \App\Models\Setting::pluck('value', 'key')
|
||||
]);
|
||||
|
||||
return $pdf->setPaper('a4', 'landscape')
|
||||
->setOption([
|
||||
'tempDir' => public_path(),
|
||||
'chroot' => public_path(),
|
||||
])
|
||||
->setWarnings(false) // Menghindari PDF korup karena error kecil di blade
|
||||
->download("{$fileName}.pdf");
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
// Eager loading mendalam untuk silsilah dan data anak
|
||||
$matting = Matting::with(['sire', 'dam', 'pic', 'kittens.cat'])->findOrFail($id);
|
||||
|
||||
return view('pages.reports.matting.show', compact('matting'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Treatment;
|
||||
use App\Models\Medicine;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MedicalDashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// 1. Tren Penyakit Bulanan (Data 6 Bulan Terakhir)
|
||||
$monthlyTrends = Treatment::select(
|
||||
DB::raw('MONTHNAME(tanggal) as month'),
|
||||
DB::raw('COUNT(*) as total')
|
||||
)
|
||||
->where('tanggal', '>=', now()->subMonths(6))
|
||||
->groupBy('month', DB::raw('MONTH(tanggal)'))
|
||||
->orderBy(DB::raw('MONTH(tanggal)'))
|
||||
->get();
|
||||
|
||||
// 2. Daftar Obat Paling Sering Digunakan (Top 5)
|
||||
$topMedicines = Treatment::select('nama_obat', DB::raw('SUM(qty_used) as total_used'))
|
||||
->groupBy('nama_obat')
|
||||
->orderByDesc('total_used')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// 3. Keluhan/Penyakit yang Paling Sering Muncul
|
||||
$topComplaints = Treatment::select('keluhan', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('keluhan')
|
||||
->orderByDesc('total')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return view('pages.dashboard.medical', compact('monthlyTrends', 'topMedicines', 'topComplaints'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Medicine;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Imports\MedicineImport;
|
||||
use App\Exports\MedicineTemplateExport;
|
||||
|
||||
class MedicineController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Tampilkan daftar obat-obatan (Index).
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Medicine::query();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->where('nama_obat', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
$medicines = $query->latest()->paginate(10);
|
||||
|
||||
return view('pages.inventory.medicines.index', compact('medicines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form tambah obat.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('pages.inventory.medicines.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan data obat baru.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_obat' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'expired_date' => 'nullable|date|after_or_equal:today',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
// Catatan: Logika pembuatan slug sudah dipindah ke Model Medicine (event saving)
|
||||
// agar Controller lebih ringkas.
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('medicines', 'public');
|
||||
}
|
||||
|
||||
$medicine = Medicine::create($validated);
|
||||
|
||||
$this->logAction('CREATED_MEDICINE', $medicine, "Menambah obat baru: {$medicine->nama_obat}");
|
||||
|
||||
// FIX: Tambahkan .index agar rute terdeteksi
|
||||
return redirect()->route('inventory.medicines.index')->with('success', 'Obat berhasil ditambahkan ke inventori.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form edit obat.
|
||||
*/
|
||||
public function edit(Medicine $medicine): View
|
||||
{
|
||||
return view('pages.inventory.medicines.edit', compact('medicine'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data obat.
|
||||
*/
|
||||
public function update(Request $request, Medicine $medicine): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_obat' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'expired_date' => 'nullable|date|after_or_equal:today',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
if ($medicine->foto_produk) {
|
||||
Storage::disk('public')->delete($medicine->foto_produk);
|
||||
}
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('medicines', 'public');
|
||||
}
|
||||
|
||||
$medicine->update($validated);
|
||||
|
||||
$this->logAction('UPDATED_MEDICINE', $medicine, "Memperbarui data obat: {$medicine->nama_obat}");
|
||||
|
||||
// FIX: Tambahkan .index
|
||||
return redirect()->route('inventory.medicines.index')->with('success', 'Data obat berhasil diperbarui.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus obat.
|
||||
*/
|
||||
public function destroy(Medicine $medicine): RedirectResponse
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Hanya Admin yang diizinkan menghapus data.');
|
||||
}
|
||||
|
||||
// Proteksi Integritas rekam medis
|
||||
if ($medicine->treatments()->exists()) {
|
||||
return back()->with('error', "Gagal: Obat '{$medicine->nama_obat}' sudah tercatat di rekam medis kucing.");
|
||||
}
|
||||
|
||||
if ($medicine->foto_produk) {
|
||||
Storage::disk('public')->delete($medicine->foto_produk);
|
||||
}
|
||||
|
||||
$medicineName = $medicine->nama_obat;
|
||||
|
||||
// PENTING: Log dilakukan SEBELUM delete
|
||||
$this->logAction('DELETED_MEDICINE', $medicine, "Menghapus data obat: {$medicineName}");
|
||||
|
||||
$medicine->delete();
|
||||
|
||||
// FIX: Tambahkan .index
|
||||
return redirect()->route('inventory.medicines.index')->with('success', 'Data obat berhasil dihapus.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fitur Impor Excel
|
||||
*/
|
||||
public function batchUpload(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate(['file_excel' => 'required|mimes:xlsx,xls,csv|max:2048']);
|
||||
|
||||
try {
|
||||
Excel::import(new MedicineImport, $request->file('file_excel'));
|
||||
$this->logAction('BATCH_UPLOAD', null, "Melakukan impor massal data obat");
|
||||
|
||||
// FIX: Tambahkan .index
|
||||
return redirect()->route('inventory.medicines.index')->with('success', 'Data obat berhasil diimpor.');
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', 'Gagal impor: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download Template Excel
|
||||
*/
|
||||
public function downloadTemplate()
|
||||
{
|
||||
return Excel::download(new MedicineTemplateExport, 'template_obat_earrin_cats.xlsx');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\{Cat, Medicine, Food, Supply, Treatment, Matting, User, ActivityLog};
|
||||
use App\Exports\UnifiedReportExport;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Gate; // WAJIB DIIMPORT untuk sistem permission
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// PERBAIKAN UTAMA: Proteksi menggunakan matriks permission slug 'reports'
|
||||
// Ini menggantikan pengecekan manual 'admin'
|
||||
Gate::authorize('check-access', ['reports', 'read']);
|
||||
|
||||
$module = $request->get('module', 'cats');
|
||||
$query = $this->initializeQuery($module);
|
||||
|
||||
$this->applyGlobalFilters($query, $request, $module);
|
||||
$this->applyModuleFilters($query, $request, $module);
|
||||
|
||||
if ($request->filled('export')) {
|
||||
$filteredData = $query->latest()->get();
|
||||
return $this->processExport($filteredData, $module, $request->export);
|
||||
}
|
||||
|
||||
$results = $query->latest()->paginate(15)->withQueryString();
|
||||
$users = User::orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return view('pages.reports.index', compact('results', 'module', 'users'));
|
||||
}
|
||||
|
||||
protected function initializeQuery($module)
|
||||
{
|
||||
return match ($module) {
|
||||
'cats' => Cat::with(['pic']),
|
||||
'medicine' => Medicine::query(),
|
||||
'food' => Food::query(),
|
||||
'supplies' => Supply::query(),
|
||||
'treatment' => Treatment::with(['cat', 'pic']),
|
||||
'vaccine', 'deworming', 'flea', 'grooming' => Cat::with(['pic']),
|
||||
default => Cat::query(),
|
||||
};
|
||||
}
|
||||
|
||||
protected function applyGlobalFilters($query, $request, $module)
|
||||
{
|
||||
if ($request->filled('q')) {
|
||||
$search = $request->q;
|
||||
$query->where(function($q) use ($module, $search) {
|
||||
$searchCol = $this->getSearchColumn($module);
|
||||
$q->where($searchCol, 'like', "%{$search}%");
|
||||
if ($module === 'treatment') {
|
||||
$q->orWhereHas('cat', fn($query) => $query->where('nama_kucing', 'like', "%{$search}%"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('pic_id')) {
|
||||
$query->where('pic_id', $request->pic_id);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||
$dateCol = $this->getDateColumn($module);
|
||||
$query->whereBetween($dateCol, [
|
||||
Carbon::parse($request->start_date)->startOfDay(),
|
||||
Carbon::parse($request->end_date)->endOfDay()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyModuleFilters($query, $request, $module)
|
||||
{
|
||||
$status = $request->status;
|
||||
|
||||
// Modul Maintenance
|
||||
if (in_array($module, ['vaccine', 'deworming', 'flea', 'grooming']) && $request->filled('status')) {
|
||||
$dateCol = $this->getDateColumn($module);
|
||||
match($status) {
|
||||
'overdue' => $query->whereNotNull($dateCol)->where($dateCol, '<', now()->toDateString()),
|
||||
'soon' => $query->whereNotNull($dateCol)->whereBetween($dateCol, [now()->toDateString(), now()->addDays(7)->toDateString()]),
|
||||
'not_set' => $query->whereNull($dateCol),
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
// Modul Inventori
|
||||
if (in_array($module, ['medicine', 'food', 'supplies']) && $request->filled('status')) {
|
||||
match($status) {
|
||||
'critical' => $query->where('qty', '<', 5),
|
||||
'expired' => $query->whereNotNull('expired_date')->where('expired_date', '<', now()->toDateString()),
|
||||
'safe' => $query->where('qty', '>=', 5),
|
||||
default => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function getSearchColumn($module) {
|
||||
return match($module) {
|
||||
'cats', 'vaccine', 'deworming', 'flea', 'grooming' => 'nama_kucing',
|
||||
'medicine' => 'nama_obat',
|
||||
'food' => 'nama_makanan',
|
||||
'supplies' => 'nama_perlengkapan',
|
||||
default => 'id'
|
||||
};
|
||||
}
|
||||
|
||||
private function getDateColumn($module) {
|
||||
return match($module) {
|
||||
'cats' => 'tanggal_lahir',
|
||||
'medicine', 'food', 'supplies' => 'expired_date',
|
||||
'vaccine' => 'jadwal_vaksin',
|
||||
'deworming' => 'jadwal_obat_cacing',
|
||||
'flea' => 'jadwal_obat_kutu',
|
||||
'grooming' => 'jadwal_grooming',
|
||||
default => 'created_at'
|
||||
};
|
||||
}
|
||||
|
||||
protected function processExport($data, $module, $format)
|
||||
{
|
||||
// Proteksi Tambahan: Pastikan user punya izin Export (opsional, bisa disatukan dengan Read)
|
||||
$fileName = "Report_{$module}_" . now()->format('Ymd_His');
|
||||
if ($format === 'xlsx') {
|
||||
return Excel::download(new UnifiedReportExport($data, $module), "{$fileName}.xlsx");
|
||||
}
|
||||
$pdf = Pdf::loadView('pages.reports.pdf_template', [
|
||||
'data' => $data,
|
||||
'module' => $module,
|
||||
'title' => "Laporan " . ucfirst($module)
|
||||
])->setPaper('a4', 'landscape');
|
||||
return $pdf->download("{$fileName}.pdf");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan daftar Role
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$roles = Role::all();
|
||||
// Menambahkan prefix 'pages.' sesuai struktur folder Anda
|
||||
return view('pages.admin.roles.index', compact('roles'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
// Mengarahkan ke folder pages sesuai standarisasi Anda
|
||||
return view('pages.admin.roles.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'slug' => 'required|string|max:255|unique:roles,slug',
|
||||
]);
|
||||
|
||||
$role = Role::create([
|
||||
'name' => $request->name,
|
||||
'slug' => str()->slug($request->slug), // Memastikan format slug benar
|
||||
]);
|
||||
|
||||
// Audit Trail: Mencatat pembuatan role baru dengan menyertakan field 'model'
|
||||
if (class_exists('\App\Models\ActivityLog')) {
|
||||
\App\Models\ActivityLog::create([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'CREATE_ROLE',
|
||||
'model' => 'Role', // Ditambahkan untuk memenuhi constraint database
|
||||
'description' => 'Menambahkan role baru: ' . $role->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// Setelah role dibuat, arahkan ke halaman edit untuk mengatur izin modul
|
||||
return redirect()->route('admin.roles.edit', $role->id)
|
||||
->with('success', 'Role berhasil dibuat. Silakan atur hak akses modul.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$role = Role::with('permissions')->findOrFail($id);
|
||||
|
||||
/**
|
||||
* Daftar modul disesuaikan dengan Sidebar (image_8b75a1.png)
|
||||
* Menambahkan modul Logistik & Intelligence yang sebelumnya belum terdaftar
|
||||
*/
|
||||
$modules = [
|
||||
// --- OPERASIONAL ---
|
||||
'cats' => 'Data Kucing',
|
||||
'matting' => 'Matting Cat (Breeding)',
|
||||
|
||||
// --- KESEHATAN ---
|
||||
'medical_insights' => 'Medical Insight',
|
||||
'treatments' => 'Rekam Medis',
|
||||
|
||||
// --- LOGISTIK (LENGKAP) ---
|
||||
'medicines' => 'Obat & Vitamin',
|
||||
'foods' => 'Stok Makanan',
|
||||
'supplies' => 'Perlengkapan',
|
||||
'categories' => 'Master Kategori',
|
||||
|
||||
// --- INTELLIGENCE ---
|
||||
'reports' => 'Pusat Laporan',
|
||||
'matting_reports' => 'Laporan Breeding',
|
||||
|
||||
// --- KONFIGURASI ---
|
||||
'users' => 'Manajemen User',
|
||||
'roles' => 'Hak Akses (Role)',
|
||||
'logs' => 'Audit Trail (Log)',
|
||||
'settings' => 'Pengaturan Sistem'
|
||||
];
|
||||
|
||||
// Menambahkan prefix 'pages.'
|
||||
return view('pages.admin.roles.edit', compact('role', 'modules'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menyimpan/Sync Matriks Permission
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$role = Role::findOrFail($id);
|
||||
|
||||
DB::transaction(function () use ($request, $role) {
|
||||
// Hapus permission lama (Sync)
|
||||
$role->permissions()->delete();
|
||||
|
||||
// Simpan permission baru jika ada input perms
|
||||
if ($request->has('perms')) {
|
||||
foreach ($request->perms as $moduleSlug => $actions) {
|
||||
$role->permissions()->create([
|
||||
'role_id' => $role->id,
|
||||
'module_slug' => $moduleSlug,
|
||||
'can_read' => isset($actions['read']) ? 1 : 0,
|
||||
'can_create' => isset($actions['create']) ? 1 : 0,
|
||||
'can_update' => isset($actions['update']) ? 1 : 0,
|
||||
'can_delete' => isset($actions['delete']) ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pencatatan Log Audit (Audit Trail) dengan field 'model' agar tidak error
|
||||
if (class_exists('\App\Models\ActivityLog')) {
|
||||
\App\Models\ActivityLog::create([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'UPDATE_PERMISSION',
|
||||
'model' => 'Role', // Ditambahkan untuk sinkronisasi database
|
||||
'description' => 'Mengubah hak akses untuk role: ' . $role->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Hak akses ' . $role->name . ' berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(Role $role)
|
||||
{
|
||||
// Cek izin di matriks Role Management
|
||||
\Illuminate\Support\Facades\Gate::authorize('check-access', ['roles', 'delete']);
|
||||
|
||||
// Keamanan: Role Admin tidak boleh dihapus
|
||||
if ($role->slug === 'admin') {
|
||||
return back()->with('error', 'Akses Ditolak! Role Administrator utama tidak dapat dihapus.');
|
||||
}
|
||||
|
||||
$roleName = $role->name;
|
||||
|
||||
// Audit Trail
|
||||
if (class_exists('\App\Models\ActivityLog')) {
|
||||
\App\Models\ActivityLog::create([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'DELETED_ROLE',
|
||||
'model' => 'Role',
|
||||
'description' => "Menghapus role: $roleName",
|
||||
]);
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
|
||||
return redirect()->route('admin.roles.index')->with('success', "Role $roleName berhasil dihapus.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Daftar kunci pengaturan yang diizinkan untuk di-update secara massal.
|
||||
*/
|
||||
protected $allowedSettings = [
|
||||
'app_name',
|
||||
'app_slogan',
|
||||
'app_version', // Tambahan baru
|
||||
'app_address',
|
||||
'wa_number'
|
||||
];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$settings = Setting::pluck('value', 'key')->all();
|
||||
|
||||
// Tambahkan default jika version belum ada
|
||||
if (!isset($settings['app_version'])) {
|
||||
$settings['app_version'] = '1.0.0';
|
||||
}
|
||||
|
||||
$waStatus = $this->checkWhatsAppService();
|
||||
$emailStatus = $this->checkEmailService();
|
||||
|
||||
$logs = ActivityLog::with('user')
|
||||
->where('action', 'LIKE', '%SETTING%')
|
||||
->latest()
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return view('pages.admin.settings', compact('settings', 'waStatus', 'emailStatus', 'logs'));
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'app_logo' => 'nullable|image|mimes:png,jpg,jpeg|max:2048',
|
||||
'app_favicon' => 'nullable|image|mimes:png,jpg,ico|max:512',
|
||||
'app_name' => 'required|string|max:100',
|
||||
'app_slogan' => 'nullable|string|max:255',
|
||||
'app_version' => 'required|string|max:20', // Validasi versi
|
||||
]);
|
||||
|
||||
// 1. Proses Logo & Favicon
|
||||
if ($request->hasFile('app_logo')) {
|
||||
$this->uploadAndProcessImage($request->file('app_logo'), 'app_logo', 512);
|
||||
}
|
||||
|
||||
if ($request->hasFile('app_favicon')) {
|
||||
$this->uploadAndProcessImage($request->file('app_favicon'), 'app_favicon', 128);
|
||||
}
|
||||
|
||||
// 2. Update Pengaturan Teks (Hanya yang ada di whitelist)
|
||||
foreach ($this->allowedSettings as $key) {
|
||||
if ($request->has($key)) {
|
||||
Setting::updateOrCreate(['key' => $key], ['value' => $request->get($key)]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logAction('UPDATED_SETTINGS', null, "Memperbarui identitas sistem ke versi " . $request->app_version);
|
||||
|
||||
return back()->with('success', 'Konfigurasi ' . $request->app_name . ' berhasil diperbarui!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Image Processing (Intervention Image v3)
|
||||
*/
|
||||
private function uploadAndProcessImage($file, $key, $size)
|
||||
{
|
||||
$oldFile = Setting::where('key', $key)->value('value');
|
||||
if ($oldFile && Storage::disk('public')->exists($oldFile)) {
|
||||
Storage::disk('public')->delete($oldFile);
|
||||
}
|
||||
|
||||
$filename = $key . '_' . time() . '.png';
|
||||
$path = 'settings/' . $filename;
|
||||
|
||||
// Intervention Image v3 Logic
|
||||
$image = Image::read($file);
|
||||
$image->cover($size, $size);
|
||||
|
||||
Storage::disk('public')->put($path, (string) $image->toPng());
|
||||
Setting::updateOrCreate(['key' => $key], ['value' => $path]);
|
||||
}
|
||||
|
||||
private function checkWhatsAppService()
|
||||
{
|
||||
// Gunakan cache atau timeout pendek agar page load tidak berat
|
||||
try {
|
||||
$response = Http::timeout(3)->withHeaders([
|
||||
'Authorization' => config('services.fonnte.token') ?? env('FONNTE_TOKEN'),
|
||||
])->post('https://api.fonnte.com/device');
|
||||
|
||||
$data = $response->json();
|
||||
return [
|
||||
'active' => ($data['status'] ?? false) == true,
|
||||
'quota' => $data['quota'] ?? 0,
|
||||
'message' => ($data['status'] ?? false) ? 'API Terhubung' : ($data['reason'] ?? 'Perangkat tidak aktif')
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return ['active' => false, 'quota' => 0, 'message' => 'Layanan WA Offline'];
|
||||
}
|
||||
}
|
||||
|
||||
private function checkEmailService()
|
||||
{
|
||||
try {
|
||||
$transport = Mail::getSymfonyTransport();
|
||||
|
||||
// 1. Cek jika menggunakan driver 'log' (biasanya di localhost)
|
||||
if ($transport instanceof \Illuminate\Mail\Transport\LogTransport) {
|
||||
return [
|
||||
'active' => true,
|
||||
'message' => 'Mail Mode: LOG (Local Development)'
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Jika menggunakan SMTP, coba lakukan jabat tangan (handshake)
|
||||
if (method_exists($transport, 'start')) {
|
||||
$transport->stop();
|
||||
$transport->start();
|
||||
}
|
||||
|
||||
return ['active' => true, 'message' => 'SMTP Terhubung'];
|
||||
} catch (\Exception $e) {
|
||||
// Jika gagal konek ke Mailtrap/Gmail, kembalikan status false
|
||||
return [
|
||||
'active' => false,
|
||||
'message' => 'SMTP Error: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Supply;
|
||||
use App\Models\Category;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SupplyController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Tampilkan daftar perlengkapan.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Supply::with('category');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->where('nama_perlengkapan', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
$supplies = $query->latest()->paginate(10);
|
||||
return view('pages.inventory.supplies.index', compact('supplies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form tambah.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
// Category ditentukan otomatis oleh Model Supply
|
||||
return view('pages.inventory.supplies.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan data perlengkapan baru.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_perlengkapan' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'category_id' => 'nullable|exists:categories,id',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('supplies', 'public');
|
||||
}
|
||||
|
||||
$supply = Supply::create($validated);
|
||||
|
||||
$this->logAction('CREATED_SUPPLY', $supply, "Menambah perlengkapan: {$supply->nama_perlengkapan}");
|
||||
|
||||
// FIX: Menggunakan .index agar tidak RouteNotFound
|
||||
return redirect()->route('inventory.supplies.index')->with('success', 'Perlengkapan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tampilkan form edit.
|
||||
*/
|
||||
public function edit(Supply $supply): View
|
||||
{
|
||||
$categories = Category::all();
|
||||
return view('pages.inventory.supplies.edit', compact('supply', 'categories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data perlengkapan.
|
||||
*/
|
||||
public function update(Request $request, Supply $supply): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nama_perlengkapan' => 'required|string|max:255',
|
||||
'qty' => 'required|numeric|min:0',
|
||||
'unit' => 'required|string|max:50',
|
||||
'category_id' => 'nullable|exists:categories,id',
|
||||
'foto_produk' => 'nullable|image|max:1024',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('foto_produk')) {
|
||||
if ($supply->foto_produk) {
|
||||
Storage::disk('public')->delete($supply->foto_produk);
|
||||
}
|
||||
$validated['foto_produk'] = $request->file('foto_produk')->store('supplies', 'public');
|
||||
}
|
||||
|
||||
$supply->update($validated);
|
||||
|
||||
$this->logAction('UPDATED_SUPPLY', $supply, "Memperbarui data perlengkapan: {$supply->nama_perlengkapan}");
|
||||
|
||||
// FIX: Menggunakan .index
|
||||
return redirect()->route('inventory.supplies.index')->with('success', 'Data perlengkapan berhasil diperbarui.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus data perlengkapan.
|
||||
*/
|
||||
public function destroy(Supply $supply): RedirectResponse
|
||||
{
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'Akses Ditolak.');
|
||||
}
|
||||
|
||||
$supplyName = $supply->nama_perlengkapan;
|
||||
|
||||
if ($supply->foto_produk) {
|
||||
Storage::disk('public')->delete($supply->foto_produk);
|
||||
}
|
||||
|
||||
// PENTING: Log dilakukan SEBELUM delete agar ID tidak null
|
||||
$this->logAction('DELETED_SUPPLY', $supply, "Menghapus perlengkapan: {$supplyName}");
|
||||
|
||||
$supply->delete();
|
||||
|
||||
// FIX: Menggunakan .index
|
||||
return redirect()->route('inventory.supplies.index')->with('success', 'Data perlengkapan berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Cat;
|
||||
use App\Models\Treatment;
|
||||
use App\Models\Medicine;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Exception;
|
||||
|
||||
class TreatmentController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Menampilkan daftar rekam medis dengan optimasi Eager Loading.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$treatments = Treatment::with([
|
||||
'cat:id,nama_kucing,foto',
|
||||
'medicine:id,nama_obat,unit',
|
||||
'user:id,name'
|
||||
])
|
||||
->latest('tanggal')
|
||||
->paginate(15);
|
||||
|
||||
return view('pages.treatments.index', compact('treatments'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
return view('pages.treatments.create', compact('selectedCat', 'cats', 'medicines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan rekam medis baru & potong stok (mendukung desimal).
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $this->validateTreatment($request);
|
||||
|
||||
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;
|
||||
|
||||
$treatment = Treatment::create($validated);
|
||||
|
||||
// Potong stok (decrement mendukung nilai float/decimal)
|
||||
$medicine->decrement('qty', $request->qty_used);
|
||||
|
||||
$this->logAction('TREATED', $treatment, "Mencatat pengobatan oleh " . auth()->user()->name);
|
||||
});
|
||||
|
||||
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']);
|
||||
}
|
||||
return back()->withInput()->with('error', 'Gagal menyimpan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form edit rekam medis.
|
||||
*/
|
||||
public function edit(Treatment $treatment): View
|
||||
{
|
||||
$cats = Cat::select('id', 'nama_kucing', 'ras')->get();
|
||||
$medicines = Medicine::all();
|
||||
return view('pages.treatments.edit', compact('treatment', 'cats', 'medicines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rekam medis & sinkronisasi ulang stok desimal.
|
||||
*/
|
||||
public function update(Request $request, Treatment $treatment): RedirectResponse
|
||||
{
|
||||
$validated = $this->validateTreatment($request);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($request, $treatment, &$validated) {
|
||||
// 1. Kembalikan stok lama (Desimal Revert)
|
||||
$oldMedicine = 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);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 4. Update data sistem
|
||||
$validated['user_id'] = auth()->id();
|
||||
$validated['nama_obat'] = $newMedicine->nama_obat;
|
||||
$treatment->update($validated);
|
||||
|
||||
$this->logAction('UPDATED_TREATMENT', $treatment, "Diperbarui oleh " . auth()->user()->name);
|
||||
});
|
||||
|
||||
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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
if ($treatment->foto_bukti) Storage::disk('public')->delete($treatment->foto_bukti);
|
||||
|
||||
$this->logAction('DELETED_TREATMENT', $treatment, "Menghapus catatan medis kucing: {$treatment->cat->nama_kucing}");
|
||||
$treatment->delete();
|
||||
});
|
||||
|
||||
return back()->with('success', 'Data rekam medis dihapus dan stok dikembalikan.');
|
||||
} catch (Exception $e) {
|
||||
return back()->with('error', 'Gagal menghapus data.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Validasi: Terpusat untuk Clean Code & Mendukung Desimal.
|
||||
*/
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use App\Traits\LogsActivity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Gate; // WAJIB DIIMPORT
|
||||
use Illuminate\Validation\Rules;
|
||||
use App\Models\ActivityLog;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\ActivityLogExport;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Menampilkan daftar user.
|
||||
* Izin: users (read)
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
// GANTI: Otorisasi berdasarkan Matriks Permission
|
||||
Gate::authorize('check-access', ['users', 'read']);
|
||||
|
||||
$users = User::query()
|
||||
->with('role')
|
||||
->when($request->search, function ($query, $search) {
|
||||
$query->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('pages.users.index', compact('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form tambah user baru.
|
||||
* Izin: users (create)
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
Gate::authorize('check-access', ['users', 'create']);
|
||||
|
||||
$roles = Role::all();
|
||||
return view('pages.users.create', compact('roles'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan user baru.
|
||||
* Izin: users (create)
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['users', 'create']);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:users',
|
||||
'phone' => 'nullable|string|max:15',
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$role = Role::find($validated['role_id']);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'role_id' => $validated['role_id'],
|
||||
'role' => $role->slug,
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
// Catat aktivitas (Gunakan parameter model untuk log)
|
||||
$this->logAction('CREATED', $user, "Mendaftarkan user baru: {$user->name} sebagai {$role->name}", 'User');
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', "User {$user->name} berhasil ditambahkan.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Halaman Edit User.
|
||||
* Izin: users (update)
|
||||
*/
|
||||
public function edit(User $user): View
|
||||
{
|
||||
Gate::authorize('check-access', ['users', 'update']);
|
||||
|
||||
$roles = Role::all();
|
||||
return view('pages.users.edit', compact('user', 'roles'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Data User.
|
||||
* Izin: users (update)
|
||||
*/
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['users', 'update']);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . $user->id,
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
'phone' => 'nullable|string|max:15',
|
||||
]);
|
||||
|
||||
$newRole = Role::find($validated['role_id']);
|
||||
|
||||
// Keamanan Logika: Tetap pertahankan agar user tidak merusak akun sendiri
|
||||
if ($user->id === auth()->id() && $newRole->slug !== 'admin' && auth()->user()->role === 'admin') {
|
||||
return back()->with('error', 'Akses Ditolak! Anda tidak bisa menurunkan otoritas Admin Anda sendiri.');
|
||||
}
|
||||
|
||||
$user->update(array_merge($validated, [
|
||||
'role' => $newRole->slug
|
||||
]));
|
||||
|
||||
$this->logAction('UPDATED', $user, "Memperbarui data profil user: {$user->name}", 'User');
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', "Data {$user->name} berhasil diperbarui.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus User.
|
||||
* Izin: users (delete)
|
||||
*/
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
Gate::authorize('check-access', ['users', 'delete']);
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return back()->with('error', 'Akses Ditolak! Anda tidak dapat menghapus akun Anda sendiri.');
|
||||
}
|
||||
|
||||
$userName = $user->name;
|
||||
$this->logAction('DELETED', $user, "Menghapus user: {$userName}", 'User');
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', "User {$userName} telah dihapus.");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* AUDIT TRAIL / LOGS SECTION */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Daftar Aktivitas.
|
||||
* Izin: logs (read)
|
||||
*/
|
||||
public function activityLogs(Request $request)
|
||||
{
|
||||
Gate::authorize('check-access', ['logs', 'read']);
|
||||
|
||||
$logs = ActivityLog::with('user')
|
||||
->when($request->search, function ($query, $search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($qu) use ($search) {
|
||||
$qu->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->when($request->action, function ($query, $action) {
|
||||
$query->where('action', 'like', "%{$action}%");
|
||||
})
|
||||
->latest()
|
||||
->paginate(20);
|
||||
|
||||
return view('pages.admin.logs', compact('logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export Logs ke Excel.
|
||||
* Izin: logs (read)
|
||||
*/
|
||||
public function exportLogs()
|
||||
{
|
||||
Gate::authorize('check-access', ['logs', 'read']);
|
||||
return Excel::download(new ActivityLogExport, 'audit_trail_'.now()->format('Ymd').'.xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan Logs.
|
||||
* Izin: logs (delete)
|
||||
*/
|
||||
public function clearLogs()
|
||||
{
|
||||
Gate::authorize('check-access', ['logs', 'delete']);
|
||||
ActivityLog::truncate();
|
||||
return back()->with('success', 'Seluruh riwayat aktivitas telah dibersihkan.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user