2026-02-08 15:16:35 +07:00
|
|
|
<?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;
|
2026-09-23 12:45:02 +07:00
|
|
|
use Illuminate\Support\Facades\Gate;
|
2026-02-08 15:16:35 +07:00
|
|
|
|
|
|
|
|
class CategoryController extends Controller
|
|
|
|
|
{
|
|
|
|
|
use LogsActivity;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Menampilkan daftar kategori.
|
|
|
|
|
*/
|
2026-09-23 12:45:02 +07:00
|
|
|
public function index(Request $request): View
|
|
|
|
|
{
|
|
|
|
|
// 1. Proteksi Akses (Sesuai Matriks Role Management)
|
|
|
|
|
Gate::authorize('check-access', ['categories', 'read']);
|
|
|
|
|
|
|
|
|
|
$query = Category::query();
|
|
|
|
|
|
|
|
|
|
// 2. SEARCH GLOBAL (Mencari di Nama & Keywords)
|
|
|
|
|
if ($request->filled('search')) {
|
|
|
|
|
$search = $request->search;
|
|
|
|
|
$query->where(function($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('keywords', 'like', "%{$search}%");
|
|
|
|
|
});
|
2026-02-08 15:16:35 +07:00
|
|
|
}
|
|
|
|
|
|
2026-09-23 12:45:02 +07:00
|
|
|
// 3. LOGIKA SORTING DINAMIS
|
|
|
|
|
$allowedSorts = ['name', 'created_at'];
|
|
|
|
|
$sort = in_array($request->get('sort'), $allowedSorts) ? $request->get('sort') : 'name';
|
|
|
|
|
$direction = strtolower($request->get('direction')) === 'desc' ? 'desc' : 'asc';
|
|
|
|
|
|
|
|
|
|
// 4. EKSEKUSI DENGAN PAGINASI (Standarisasi 15 item per halaman)
|
|
|
|
|
$categories = $query->orderBy($sort, $direction)
|
|
|
|
|
->paginate(15)
|
|
|
|
|
->withQueryString();
|
|
|
|
|
|
|
|
|
|
// 5. STATISTIK RINGKAS
|
|
|
|
|
$totalCategories = Category::count();
|
|
|
|
|
$latestCategory = Category::latest()->first()?->name ?? '-';
|
|
|
|
|
|
|
|
|
|
return view('pages.inventory.categories.index', compact(
|
|
|
|
|
'categories', 'totalCategories', 'latestCategory'
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 15:16:35 +07:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-09-23 12:45:02 +07:00
|
|
|
{
|
|
|
|
|
|
|
|
|
|
Gate::authorize('check-access', ['categories', 'update']);
|
2026-02-08 15:16:35 +07:00
|
|
|
|
2026-09-23 12:45:02 +07:00
|
|
|
return view('pages.inventory.categories.edit', compact('category'));
|
|
|
|
|
}
|
2026-02-08 15:16:35 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.');
|
|
|
|
|
}
|
|
|
|
|
}
|