first commit

This commit is contained in:
akbarguci.pkl 2026-05-19 09:49:54 +07:00
parent 03229101d5
commit 8c5285b988
11 changed files with 497 additions and 118 deletions

View File

@ -1,6 +1,8 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { MdOutlineWavingHand } from "react-icons/md";
interface DashboardStats { interface DashboardStats {
permintaanHariIni: number; permintaanHariIni: number;
menungguPersetujuan: number; menungguPersetujuan: number;
@ -80,7 +82,7 @@ export default function Hero() {
<div className="flex justify-between items-start mb-8"> <div className="flex justify-between items-start mb-8">
<div> <div>
<p className="text-gray-500 text-lg"> <p className="text-gray-500 text-lg">
Selamat datang kembali 👋 Selamat datang kembali <MdOutlineWavingHand />
</p> </p>
<h1 className="text-5xl font-extrabold text-blue-700 mt-2"> <h1 className="text-5xl font-extrabold text-blue-700 mt-2">

View File

@ -1,5 +1,7 @@
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { IoIosSearch } from "react-icons/io";
interface FilterProps { interface FilterProps {
kataKunci: string; kataKunci: string;
setKataKunci: Dispatch<SetStateAction<string>>; setKataKunci: Dispatch<SetStateAction<string>>;
@ -9,7 +11,7 @@ export default function UsersFilter({ kataKunci, setKataKunci }: FilterProps) {
return ( return (
<div className="bg-white rounded-2xl p-4 shadow-sm border border-blue-100 mb-6 flex items-center justify-between gap-4"> <div className="bg-white rounded-2xl p-4 shadow-sm border border-blue-100 mb-6 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 bg-gray-50 rounded-xl px-4 py-2.5 border border-gray-200 w-full max-w-md"> <div className="flex items-center gap-3 bg-gray-50 rounded-xl px-4 py-2.5 border border-gray-200 w-full max-w-md">
<span className="text-gray-400 text-lg">🔍</span> <span className="text-gray-400 text-lg"><IoIosSearch /></span>
<input <input
type="text" type="text"
value={kataKunci} value={kataKunci}

View File

@ -1,12 +1,20 @@
import { useState } from "react"; import { useState } from "react";
import axios from "axios"; import axios from "axios";
import { CiCirclePlus } from "react-icons/ci";
import { FaUser } from "react-icons/fa";
interface HeroProps { interface HeroProps {
onUserDitambahkan: () => void; onUserDitambahkan: () => void;
} }
export default function UsersHero({ onUserDitambahkan }: HeroProps) { export default function UsersHero({
const [isModalOpen, setIsModalOpen] = useState(false); onUserDitambahkan,
}: HeroProps) {
const [isModalOpen, setIsModalOpen] =
useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama_lengkap: "", nama_lengkap: "",
username: "", username: "",
@ -15,144 +23,300 @@ export default function UsersHero({ onUserDitambahkan }: HeroProps) {
email: "", email: "",
lembaga: "", lembaga: "",
}); });
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const [loading, setLoading] =
useState(false);
// SUBMIT TAMBAH USER
const handleSubmit = async (
e: React.FormEvent
) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
const respon = await axios.post("http://localhost:5000/api/register", formData);
const respon = await axios.post(
"http://localhost:5000/api/register",
formData
);
if (respon.data.success) { if (respon.data.success) {
alert(respon.data.message); alert(respon.data.message);
setIsModalOpen(false); setIsModalOpen(false);
// Reset Form
setFormData({ nama_lengkap: "", username: "", password: "", role: "User", email: "", lembaga: "" }); // RESET FORM
// Refresh tabel di halaman utama otomatis setFormData({
nama_lengkap: "",
username: "",
password: "",
role: "User",
email: "",
lembaga: "",
});
// REFRESH TABEL
onUserDitambahkan(); onUserDitambahkan();
} }
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
alert(error.response?.data?.message || "Gagal menambah user baru!");
alert(
error.response?.data?.message ||
"Gagal menambah user baru!"
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
return ( return (
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-3xl p-8 shadow-lg text-white mb-8 flex justify-between items-center"> <div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-3xl p-8 shadow-lg text-white mb-8 flex justify-between items-center">
{/* KIRI */}
<div> <div>
<h1 className="text-4xl font-extrabold tracking-tight">Manajemen Pengguna</h1>
<p className="text-blue-100 mt-2 text-lg">Kelola hak akses data, penyerah, penerima, dan petugas BAST CIFO.</p> <h1 className="text-4xl font-extrabold tracking-tight">
Manajemen Pengguna
</h1>
<p className="text-blue-100 mt-2 text-lg">
Kelola hak akses data,
penyerah, penerima,
dan petugas BAST CIFO.
</p>
</div> </div>
{/* BUTTON TAMBAH USER */}
<button <button
onClick={() => setIsModalOpen(true)} onClick={() =>
setIsModalOpen(true)
}
className="bg-white text-blue-700 font-bold px-6 py-3 rounded-2xl shadow hover:bg-blue-50 transition-all duration-200 flex items-center gap-2 transform hover:scale-105" className="bg-white text-blue-700 font-bold px-6 py-3 rounded-2xl shadow hover:bg-blue-50 transition-all duration-200 flex items-center gap-2 transform hover:scale-105"
> >
➕ Tambah User Baru
<CiCirclePlus size={24} />
Tambah User Baru
</button> </button>
{/* MODAL POP UP FORM TAMBAH USER */} {/* MODAL */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50 p-4">
<div className="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-gray-100 text-gray-800 animate-in fade-in zoom-in-95 duration-200"> <div className="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-gray-100 text-gray-800 animate-in fade-in zoom-in-95 duration-200">
{/* HEADER MODAL */}
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2 text-blue-700"> <h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2 text-blue-700">
👤 Registrasi Pengguna Baru
<FaUser size={20} />
Registrasi Pengguna Baru
</h2> </h2>
<form onSubmit={handleSubmit} className="space-y-4"> {/* FORM */}
<form
onSubmit={handleSubmit}
className="space-y-4"
>
{/* NAMA */}
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Nama Lengkap *</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Nama Lengkap *
</label>
<input <input
type="text" type="text"
required required
value={formData.nama_lengkap} value={formData.nama_lengkap}
onChange={(e) => setFormData({ ...formData, nama_lengkap: e.target.value })} onChange={(e) =>
setFormData({
...formData,
nama_lengkap:
e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
placeholder="Contoh: Bu Sari" placeholder="Contoh: Bu Sari"
/> />
</div> </div>
{/* USERNAME & PASSWORD */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Username *</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Username *
</label>
<input <input
type="text" type="text"
required required
value={formData.username} value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })} onChange={(e) =>
setFormData({
...formData,
username:
e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
placeholder="sari123" placeholder="sari123"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Password *</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Password *
</label>
<input <input
type="password" type="password"
required required
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) =>
setFormData({
...formData,
password:
e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
placeholder="******" placeholder="******"
/> />
</div>
</div> </div>
</div>
{/* ROLE */}
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Role / Hak Akses</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Role / Hak Akses
</label>
<select <select
value={formData.role} value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })} onChange={(e) =>
setFormData({
...formData,
role: e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition bg-white" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition bg-white"
> >
<option value="Admin">Admin</option>
<option value="User">User</option> <option value="Admin">
Admin
</option>
<option value="User">
User
</option>
</select> </select>
</div> </div>
{/* EMAIL */}
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Email (Opsional)</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Email (Opsional)
</label>
<input <input
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) =>
setFormData({
...formData,
email:
e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
placeholder="sari@cifo.co.id" placeholder="sari@cifo.co.id"
/> />
</div> </div>
{/* LEMBAGA */}
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Instansi / Lembaga (Opsional)</label>
<label className="block text-sm font-semibold text-gray-700 mb-1">
Instansi / Lembaga
(Opsional)
</label>
<input <input
type="text" type="text"
value={formData.lembaga} value={formData.lembaga}
onChange={(e) => setFormData({ ...formData, lembaga: e.target.value })} onChange={(e) =>
setFormData({
...formData,
lembaga:
e.target.value,
})
}
className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" className="w-full px-4 py-2.5 rounded-xl border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
placeholder="SMKN 4 Bandung / PT CIFO" placeholder="SMKN 4 Bandung / PT CIFO"
/> />
</div> </div>
{/* BUTTON */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100 mt-6"> <div className="flex justify-end gap-3 pt-4 border-t border-gray-100 mt-6">
<button <button
type="button" type="button"
onClick={() => setIsModalOpen(false)} onClick={() =>
setIsModalOpen(false)
}
className="px-5 py-2.5 rounded-xl border border-gray-300 font-semibold text-gray-600 hover:bg-gray-50 transition" className="px-5 py-2.5 rounded-xl border border-gray-300 font-semibold text-gray-600 hover:bg-gray-50 transition"
> >
Batal Batal
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="px-5 py-2.5 rounded-xl bg-blue-600 font-semibold text-white hover:bg-blue-700 shadow transition disabled:opacity-50" className="px-5 py-2.5 rounded-xl bg-blue-600 font-semibold text-white hover:bg-blue-700 shadow transition disabled:opacity-50"
> >
{loading ? "Menyimpan..." : "Simpan Pengguna"} {loading
? "Menyimpan..."
: "Simpan Pengguna"}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
} }

View File

@ -4,6 +4,7 @@ import type { UserData } from "../../routes/ManajementUsers";
import { FaEdit } from "react-icons/fa"; import { FaEdit } from "react-icons/fa";
import { MdDelete } from "react-icons/md"; import { MdDelete } from "react-icons/md";
import { MdEmail } from "react-icons/md";
interface TableProps { interface TableProps {
dataUsers: UserData[]; dataUsers: UserData[];
@ -142,7 +143,7 @@ export default function UsersTable({
colSpan={7} colSpan={7}
className="p-8 text-center text-gray-400 font-medium" className="p-8 text-center text-gray-400 font-medium"
> >
Belum ada data pengguna terdaftar atau kata kunci salah, Bang! 📭 Belum ada data pengguna terdaftar atau kata kunci salah, Bang! <MdEmail />
</td> </td>
</tr> </tr>
@ -245,7 +246,7 @@ export default function UsersTable({
<div className="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-gray-100 text-gray-800 animate-in fade-in zoom-in-95 duration-200"> <div className="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl border border-gray-100 text-gray-800 animate-in fade-in zoom-in-95 duration-200">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2 text-amber-600"> <h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2 text-amber-600">
✏️ Sunting Data Pengguna <FaEdit /> Sunting Data Pengguna
</h2> </h2>
<form <form

View File

@ -1,13 +1,16 @@
export default function BarangHero() { export default function BarangHero() {
return ( return (
<div className="bg-gradient-to-r from-black via-blue-900 to-blue-600 rounded-3xl p-8 text-white"> <div className="bg-gradient-to-r from-black via-blue-900 to-blue-600 rounded-3xl p-8 text-white text-right">
<h1 className="text-4xl font-extrabold tracking-tight mb-2"> <h1 className="text-4xl font-extrabold tracking-tight mb-2">
Kelola Barang Sekolah Kelola Barang Sekolah
</h1> </h1>
<p className="text-blue-100 text-sm md:text-base"> <p className="text-blue-100 text-sm md:text-base">
Pantau sirkulasi stok, kodifikasi QR-Code otomatis, kondisi fisik, dan lokasi penempatan aset inventaris CIFO. Pantau sirkulasi stok, kodifikasi QR-Code otomatis, kondisi fisik,
dan lokasi penempatan aset inventaris CIFO.
</p> </p>
</div> </div>
); );
} }

View File

@ -62,7 +62,7 @@ export default function EditBarangModal({ itemSelected, onClose, onRefresh }: Ed
const res = await axios.put(`http://localhost:5000/api/barang/${itemSelected.barang_id}`, dataUpdate); const res = await axios.put(`http://localhost:5000/api/barang/${itemSelected.barang_id}`, dataUpdate);
if (res.data.success || res.status === 200) { if (res.data.success || res.status === 200) {
alert("Perubahan Inventaris Berhasil Diperbarui! ✏️"); alert("Perubahan Inventaris Berhasil Diperbarui! <FaEdit />");
onRefresh(); onRefresh();
onClose(); onClose();
} }

View File

@ -15,6 +15,9 @@ import axios from "axios";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import autoTable from "jspdf-autotable"; import autoTable from "jspdf-autotable";
import { FaHourglassEnd } from "react-icons/fa";
import { FaBox } from "react-icons/fa";
export default function Laporan() { export default function Laporan() {
/* ======================================================= /* =======================================================
@ -139,7 +142,7 @@ export default function Laporan() {
{/* RENDER KONDISIONAL KARTU LAPORAN */} {/* RENDER KONDISIONAL KARTU LAPORAN */}
{loading ? ( {loading ? (
<div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl text-blue-600 font-medium shadow animate-pulse"> <div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl text-blue-600 font-medium shadow animate-pulse">
Sedang menarik data rekapan laporan dari server... ⏳ Sedang menarik data rekapan laporan dari server... <FaHourglassEnd />
</div> </div>
) : dataLaporan && dataLaporan.length > 0 ? ( ) : dataLaporan && dataLaporan.length > 0 ? (
<div className="space-y-5"> <div className="space-y-5">
@ -212,7 +215,7 @@ export default function Laporan() {
</div> </div>
) : ( ) : (
<div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 text-gray-400 font-medium"> <div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 text-gray-400 font-medium">
Tidak ditemukan rekapan laporan transaksi barang, Bang! 📦 Tidak ditemukan rekapan laporan transaksi barang, Bang! <FaBox />
</div> </div>
)} )}
</div> </div>

View File

@ -1,3 +1,5 @@
import { IoIosSettings } from "react-icons/io";
export default function Settings() { export default function Settings() {
const settingsMenu = [ const settingsMenu = [
"Profil Toko", "Profil Toko",
@ -65,7 +67,7 @@ export default function Settings() {
text-2xl text-2xl
x x
"> ">
⚙️ <IoIosSettings />
</div> </div>
</div> </div>

View File

@ -16,6 +16,9 @@ import axios from "axios";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import autoTable from "jspdf-autotable"; import autoTable from "jspdf-autotable";
import { FaBox } from "react-icons/fa";
import { FaHourglassEnd } from "react-icons/fa";
export default function TransaksiBAST() { export default function TransaksiBAST() {
/* ======================================================= /* =======================================================
@ -279,7 +282,7 @@ export default function TransaksiBAST() {
{loading ? ( {loading ? (
<div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl text-blue-600 font-medium shadow-md animate-pulse"> <div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl text-blue-600 font-medium shadow-md animate-pulse">
Sedang menyinkronkan data dokumen BAST dari server CIFO... ⏳ Sedang menyinkronkan data dokumen BAST dari server CIFO... <FaHourglassEnd />
</div> </div>
) : dataBAST && dataBAST.length > 0 ? ( ) : dataBAST && dataBAST.length > 0 ? (
@ -477,7 +480,7 @@ export default function TransaksiBAST() {
) : ( ) : (
<div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 text-gray-400 font-medium"> <div className="p-12 text-center bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 text-gray-400 font-medium">
Belum ada riwayat dokumen transaksi BAST terdaftar, Bang! 📦 Belum ada riwayat dokumen transaksi BAST terdaftar, Bang! <FaBox />
</div> </div>
)} )}

View File

@ -5,6 +5,7 @@ import axios from "axios";
import UsersHero from "../components/manajemenUsers/UserHero"; import UsersHero from "../components/manajemenUsers/UserHero";
import UsersFilter from "../components/manajemenUsers/UserFilter"; import UsersFilter from "../components/manajemenUsers/UserFilter";
import UsersTable from "../components/manajemenUsers/UserTable"; import UsersTable from "../components/manajemenUsers/UserTable";
import { FaHourglassEnd } from "react-icons/fa";
// Definisikan tipe data User sesuai kolom database CIFO Abang // Definisikan tipe data User sesuai kolom database CIFO Abang
export interface UserData { export interface UserData {
@ -83,7 +84,7 @@ export default function ManajemenUsers() {
{loading ? ( {loading ? (
<div className="flex justify-center items-center p-10 bg-white rounded-2xl shadow-sm border border-blue-100"> <div className="flex justify-center items-center p-10 bg-white rounded-2xl shadow-sm border border-blue-100">
<p className="text-blue-600 font-semibold animate-pulse"> <p className="text-blue-600 font-semibold animate-pulse">
Sabar Bang, lagi narik data dari DB CIFO... ⏳ Sabar Bang, lagi narik data dari DB CIFO... <FaHourglassEnd />
</p> </p>
</div> </div>
) : ( ) : (

View File

@ -1,9 +1,14 @@
// src/routes/MasterData.tsx // src/routes/MasterData.tsx
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import { CiCirclePlus } from "react-icons/ci";
import { FaEdit } from "react-icons/fa";
import { FaBox } from "react-icons/fa";
import { FaHourglassEnd } from "react-icons/fa";
// COMPONENTS // COMPONENTS
import BarangHero from "../components/masterData/BarangHero";
import EditBarangModal from "../components/masterData/EditBarangModal"; import EditBarangModal from "../components/masterData/EditBarangModal";
import TambahBarang from "../components/masterData/TambahBarang"; import TambahBarang from "../components/masterData/TambahBarang";
@ -13,38 +18,69 @@ export interface BarangType {
kode_barang: string; kode_barang: string;
nama_barang: string; nama_barang: string;
stok: number; stok: number;
status_barang: string; // 'Baik' / 'Rusak' status_barang: string;
qr_image: string; // Base64 string dari backend Express qr_image: string;
foto_barang: string | null; foto_barang: string | null;
nama_kategori?: string; nama_kategori?: string;
nama_lokasi?: string; nama_lokasi?: string;
} }
export default function MasterData() { export default function MasterData() {
const [selectedKategori, setSelectedKategori] = useState("Semua");
const [selectedBarang, setSelectedBarang] = useState<BarangType | null>(null);
const [showTambah, setShowTambah] = useState(false);
const [barangList, setBarangList] = useState<BarangType[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// 1. FUNGSI READ: Mengambil data barang real-time dari DB backend Express const [selectedKategori, setSelectedKategori] =
useState("Semua");
const [selectedBarang, setSelectedBarang] =
useState<BarangType | null>(null);
const [showTambah, setShowTambah] =
useState(false);
const [barangList, setBarangList] =
useState<BarangType[]>([]);
const [loading, setLoading] =
useState<boolean>(true);
// LOAD DATA BARANG
const muatDataBarang = async () => { const muatDataBarang = async () => {
setLoading(true);
try {
const respon = await axios.get("http://localhost:5000/api/barang");
// Pengaman jika struktur response backend dibungkus { success: true, data: [...] } setLoading(true);
if (respon.data && respon.data.success && Array.isArray(respon.data.data)) {
try {
const respon = await axios.get(
"http://localhost:5000/api/barang"
);
if (
respon.data &&
respon.data.success &&
Array.isArray(respon.data.data)
) {
setBarangList(respon.data.data); setBarangList(respon.data.data);
} else if (Array.isArray(respon.data)) { } else if (Array.isArray(respon.data)) {
setBarangList(respon.data); setBarangList(respon.data);
} else { } else {
setBarangList([]); setBarangList([]);
} }
} catch (error) { } catch (error) {
console.error("Gagal memuat data dari API:", error);
console.error(
"Gagal memuat data dari API:",
error
);
setBarangList([]); setBarangList([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
@ -53,40 +89,87 @@ export default function MasterData() {
muatDataBarang(); muatDataBarang();
}, []); }, []);
// 2. FUNGSI FILTER KATEGORI SECARA DINAMIS // FILTER KATEGORI
const filteredBarang = barangList.filter((barang) => { const filteredBarang =
if (selectedKategori === "Semua") return true; barangList.filter((barang) => {
return barang.nama_kategori?.toLowerCase() === selectedKategori.toLowerCase();
if (selectedKategori === "Semua")
return true;
return (
barang.nama_kategori?.toLowerCase() ===
selectedKategori.toLowerCase()
);
}); });
// 3. TRIGGER SETELAH BERHASIL SAVE / EDIT / TAMBAH // REFRESH
const handleSaveSuccess = () => { const handleSaveSuccess = () => {
muatDataBarang(); // Refresh list barang dari DB
setSelectedBarang(null); // Tutup Modal Edit muatDataBarang();
setShowTambah(false); // Tutup Modal Tambah
setSelectedBarang(null);
setShowTambah(false);
}; };
return ( return (
<div className="p-8 bg-gray-100 min-h-screen text-gray-800"> <div className="p-8 bg-gray-100 min-h-screen text-gray-800">
{/* HEADER HERO CARD */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6"> {/* HEADER HERO */}
<div className="flex-1 w-full"> <div className="mb-6">
<BarangHero />
</div> <div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-3xl p-8 text-white flex flex-col lg:flex-row justify-between items-center gap-6 shadow-lg">
<button
onClick={() => setShowTambah(true)} {/* TEXT HERO */}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-4 rounded-2xl shadow-lg transition duration-200 whitespace-nowrap" <div className="flex-1">
>
➕ Tambah Barang UKK <h1 className="text-4xl font-extrabold tracking-tight mb-2">
</button> Kelola Barang Sekolah
</h1>
<p className="text-blue-100 text-sm md:text-base max-w-3xl">
Pantau sirkulasi stok, kodifikasi QR-Code otomatis,
kondisi fisik, dan lokasi penempatan aset inventaris CIFO.
</p>
</div> </div>
{/* FILTER KATEGORI BARANG */} {/* BUTTON TAMBAH */}
<button
onClick={() =>
setShowTambah(true)
}
className="bg-white hover:bg-gray-100 text-blue-700 font-bold px-7 py-4 rounded-2xl shadow-md transition duration-200 flex items-center gap-3 whitespace-nowrap"
>
<CiCirclePlus
size={28}
className="text-blue-600"
/>
Tambah Barang UKK
</button>
</div>
</div>
{/* FILTER */}
<div className="flex gap-2 mb-6 overflow-x-auto pb-2"> <div className="flex gap-2 mb-6 overflow-x-auto pb-2">
{["Semua", "Elektronik", "Buku dan Modul", "Alat Tulis"].map((kat) => (
{[
"Semua",
"Elektronik",
"Buku dan Modul",
"Alat Tulis",
].map((kat) => (
<button <button
key={kat} key={kat}
onClick={() => setSelectedKategori(kat)} onClick={() =>
setSelectedKategori(kat)
}
className={`px-5 py-2.5 rounded-xl font-semibold transition text-sm ${ className={`px-5 py-2.5 rounded-xl font-semibold transition text-sm ${
selectedKategori === kat selectedKategori === kat
? "bg-blue-600 text-white shadow-sm" ? "bg-blue-600 text-white shadow-sm"
@ -95,108 +178,223 @@ export default function MasterData() {
> >
{kat} {kat}
</button> </button>
))} ))}
</div> </div>
{/* GRID DAFTAR INVENTARIS UTAMA */} {/* GRID */}
{loading ? ( {loading ? (
<div className="p-12 text-center bg-white rounded-3xl border border-gray-200 font-medium text-blue-600 animate-pulse"> <div className="p-12 text-center bg-white rounded-3xl border border-gray-200 font-medium text-blue-600 animate-pulse">
Narik data & QR Code dari server CIFO... ⏳ Narik data & QR Code dari server CIFO... <FaHourglassEnd />
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{filteredBarang.length === 0 ? ( {filteredBarang.length === 0 ? (
<div className="col-span-full text-center py-12 bg-white rounded-3xl border border-gray-200 text-gray-400 font-medium"> <div className="col-span-full text-center py-12 bg-white rounded-3xl border border-gray-200 text-gray-400 font-medium">
Belum ada aset terdaftar di kategori ini, Bang! 📦 Belum ada aset terdaftar di kategori ini, Bang! <FaBox />
</div> </div>
) : ( ) : (
filteredBarang.map((barang) => ( filteredBarang.map((barang) => (
<div <div
key={barang.barang_id} key={barang.barang_id}
className="bg-white rounded-3xl p-5 shadow-sm border border-gray-200 flex flex-col justify-between hover:shadow-md transition" className="bg-white rounded-3xl p-5 shadow-sm border border-gray-200 flex flex-col justify-between hover:shadow-md transition"
> >
<div> <div>
{/* BOX FRAME IMAGE DENGAN PROTESKSI ANTI IMAGE PECAH */}
{/* IMAGE */}
<div className="w-full h-40 bg-gray-50 rounded-2xl overflow-hidden mb-4 border border-gray-100 flex items-center justify-center relative"> <div className="w-full h-40 bg-gray-50 rounded-2xl overflow-hidden mb-4 border border-gray-100 flex items-center justify-center relative">
{barang.foto_barang ? ( {barang.foto_barang ? (
<img <img
src={barang.foto_barang.startsWith('http') ? barang.foto_barang : `http://localhost:5000/${barang.foto_barang}`} src={
barang.foto_barang.startsWith(
"http"
)
? barang.foto_barang
: `http://localhost:5000/${barang.foto_barang}`
}
alt={barang.nama_barang} alt={barang.nama_barang}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none'; // Sembunyikan gambar yang rusak const target =
const parent = target.parentElement; e.target as HTMLImageElement;
target.style.display =
"none";
const parent =
target.parentElement;
if (parent) { if (parent) {
const fallback = document.createElement('div');
fallback.className = 'flex flex-col items-center justify-center text-gray-400'; const fallback =
fallback.innerHTML = '<span class="text-4xl mb-1">📦</span><span class="text-xs font-medium">No Image</span>'; document.createElement(
parent.appendChild(fallback); "div"
);
fallback.className =
"flex flex-col items-center justify-center text-gray-400";
fallback.innerHTML =
'<span class="text-4xl mb-1"><FaBox /></span><span class="text-xs font-medium">No Image</span>';
parent.appendChild(
fallback
);
} }
}} }}
/> />
) : ( ) : (
<div className="flex flex-col items-center justify-center text-gray-400"> <div className="flex flex-col items-center justify-center text-gray-400">
<span className="text-4xl mb-1">📦</span>
<span className="text-xs font-medium">No Image</span> <span className="text-4xl mb-1">
</div> <FaBox />
)}
<span className={`absolute top-3 right-3 text-xs font-bold px-2.5 py-1 rounded-full ${
barang.status_barang === 'Baik' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{barang.status_barang}
</span> </span>
<span className="text-xs font-medium">
No Image
</span>
</div> </div>
)}
{/* STATUS */}
<span
className={`absolute top-3 right-3 text-xs font-bold px-2.5 py-1 rounded-full ${
barang.status_barang ===
"Baik"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{barang.status_barang}
</span>
</div>
{/* KODE */}
<span className="text-xs font-mono font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded"> <span className="text-xs font-mono font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded">
{barang.kode_barang} {barang.kode_barang}
</span> </span>
<h3 className="text-lg font-bold text-gray-900 mt-1.5 line-clamp-1">{barang.nama_barang}</h3>
{/* NAMA */}
<h3 className="text-lg font-bold text-gray-900 mt-1.5 line-clamp-1">
{barang.nama_barang}
</h3>
{/* DETAIL */}
<div className="grid grid-cols-2 gap-2 mt-3 text-xs text-gray-500"> <div className="grid grid-cols-2 gap-2 mt-3 text-xs text-gray-500">
<div>Kategori: <strong className="text-gray-700 block">{barang.nama_kategori || '-'}</strong></div>
<div>Lokasi: <strong className="text-gray-700 block">{barang.nama_lokasi || '-'}</strong></div> <div>
</div> Kategori:
<strong className="text-gray-700 block">
{barang.nama_kategori || "-"}
</strong>
</div> </div>
{/* BOTTOM ACTION & BARCODE PREVIEW CARD */} <div>
Lokasi:
<strong className="text-gray-700 block">
{barang.nama_lokasi || "-"}
</strong>
</div>
</div>
</div>
{/* FOOTER */}
<div className="mt-5 pt-4 border-t border-gray-100 flex items-center justify-between gap-4"> <div className="mt-5 pt-4 border-t border-gray-100 flex items-center justify-between gap-4">
{/* QR */}
{barang.qr_image && ( {barang.qr_image && (
<img src={barang.qr_image} alt="QR" className="w-12 h-12 rounded-lg border p-0.5 bg-white shadow-sm" />
<img
src={barang.qr_image}
alt="QR"
className="w-12 h-12 rounded-lg border p-0.5 bg-white shadow-sm"
/>
)} )}
{/* STOK */}
<div className="text-right flex-1"> <div className="text-right flex-1">
<span className="text-xs text-gray-400 block">Stok</span>
<span className="text-base font-extrabold text-gray-800">{barang.stok} Unit</span> <span className="text-xs text-gray-400 block">
Stok
</span>
<span className="text-base font-extrabold text-gray-800">
{barang.stok} Unit
</span>
</div> </div>
{/* BUTTON EDIT */}
<button <button
onClick={() => setSelectedBarang(barang)} onClick={() =>
className="bg-gray-50 hover:bg-blue-50 hover:text-blue-600 text-gray-500 p-2.5 rounded-xl border border-gray-200 transition" setSelectedBarang(
barang
)
}
className="bg-gray-50 hover:bg-blue-50 hover:text-blue-600 text-gray-500 p-3 rounded-xl border border-gray-200 transition"
> >
✏️
<FaEdit size={16} />
</button> </button>
</div> </div>
</div> </div>
)) ))
)} )}
</div> </div>
)} )}
{/* RENDER MODAL EDIT BARANG (Props disinkronkan dengan EditBarangModal.tsx baru) */} {/* MODAL EDIT */}
{selectedBarang && ( {selectedBarang && (
<EditBarangModal <EditBarangModal
itemSelected={selectedBarang} itemSelected={selectedBarang}
onClose={() => setSelectedBarang(null)} onClose={() =>
setSelectedBarang(null)
}
onRefresh={handleSaveSuccess} onRefresh={handleSaveSuccess}
/> />
)} )}
{/* RENDER MODAL TAMBAH BARANG (Props disinkronkan dengan TambahBarang.tsx baru) */} {/* MODAL TAMBAH */}
{showTambah && ( {showTambah && (
<TambahBarang <TambahBarang
onClose={() => setShowTambah(false)} onClose={() =>
setShowTambah(false)
}
onRefresh={handleSaveSuccess} onRefresh={handleSaveSuccess}
/> />
)} )}
</div> </div>
); );
} }