first commit
This commit is contained in:
parent
03229101d5
commit
8c5285b988
|
|
@ -1,6 +1,8 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
import { MdOutlineWavingHand } from "react-icons/md";
|
||||
|
||||
interface DashboardStats {
|
||||
permintaanHariIni: number;
|
||||
menungguPersetujuan: number;
|
||||
|
|
@ -80,7 +82,7 @@ export default function Hero() {
|
|||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p className="text-gray-500 text-lg">
|
||||
Selamat datang kembali 👋
|
||||
Selamat datang kembali <MdOutlineWavingHand />
|
||||
</p>
|
||||
|
||||
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { IoIosSearch } from "react-icons/io";
|
||||
|
||||
interface FilterProps {
|
||||
kataKunci: string;
|
||||
setKataKunci: Dispatch<SetStateAction<string>>;
|
||||
|
|
@ -9,7 +11,7 @@ export default function UsersFilter({ kataKunci, setKataKunci }: FilterProps) {
|
|||
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="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
|
||||
type="text"
|
||||
value={kataKunci}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import { CiCirclePlus } from "react-icons/ci";
|
||||
import { FaUser } from "react-icons/fa";
|
||||
|
||||
interface HeroProps {
|
||||
onUserDitambahkan: () => void;
|
||||
}
|
||||
|
||||
export default function UsersHero({ onUserDitambahkan }: HeroProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
export default function UsersHero({
|
||||
onUserDitambahkan,
|
||||
}: HeroProps) {
|
||||
|
||||
const [isModalOpen, setIsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
nama_lengkap: "",
|
||||
username: "",
|
||||
|
|
@ -15,144 +23,300 @@ export default function UsersHero({ onUserDitambahkan }: HeroProps) {
|
|||
email: "",
|
||||
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();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
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) {
|
||||
|
||||
alert(respon.data.message);
|
||||
|
||||
setIsModalOpen(false);
|
||||
// Reset Form
|
||||
setFormData({ nama_lengkap: "", username: "", password: "", role: "User", email: "", lembaga: "" });
|
||||
// Refresh tabel di halaman utama otomatis
|
||||
|
||||
// RESET FORM
|
||||
setFormData({
|
||||
nama_lengkap: "",
|
||||
username: "",
|
||||
password: "",
|
||||
role: "User",
|
||||
email: "",
|
||||
lembaga: "",
|
||||
});
|
||||
|
||||
// REFRESH TABEL
|
||||
onUserDitambahkan();
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
|
||||
console.error(error);
|
||||
alert(error.response?.data?.message || "Gagal menambah user baru!");
|
||||
|
||||
alert(
|
||||
error.response?.data?.message ||
|
||||
"Gagal menambah user baru!"
|
||||
);
|
||||
|
||||
} finally {
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
|
||||
{/* KIRI */}
|
||||
<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>
|
||||
|
||||
{/* BUTTON TAMBAH USER */}
|
||||
<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"
|
||||
>
|
||||
➕ Tambah User Baru
|
||||
|
||||
<CiCirclePlus size={24} />
|
||||
|
||||
Tambah User Baru
|
||||
|
||||
</button>
|
||||
|
||||
{/* MODAL POP UP FORM TAMBAH USER */}
|
||||
{/* MODAL */}
|
||||
{isModalOpen && (
|
||||
|
||||
<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">
|
||||
|
||||
{/* HEADER MODAL */}
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* FORM */}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
|
||||
{/* NAMA */}
|
||||
<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
|
||||
type="text"
|
||||
required
|
||||
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"
|
||||
placeholder="Contoh: Bu Sari"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* USERNAME & PASSWORD */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
<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
|
||||
type="text"
|
||||
required
|
||||
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"
|
||||
placeholder="sari123"
|
||||
/>
|
||||
|
||||
</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
|
||||
type="password"
|
||||
required
|
||||
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"
|
||||
placeholder="******"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ROLE */}
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<option value="Admin">Admin</option>
|
||||
<option value="User">User</option>
|
||||
|
||||
<option value="Admin">
|
||||
Admin
|
||||
</option>
|
||||
|
||||
<option value="User">
|
||||
User
|
||||
</option>
|
||||
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
{/* EMAIL */}
|
||||
<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
|
||||
type="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"
|
||||
placeholder="sari@cifo.co.id"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LEMBAGA */}
|
||||
<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
|
||||
type="text"
|
||||
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"
|
||||
placeholder="SMKN 4 Bandung / PT CIFO"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100 mt-6">
|
||||
|
||||
<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"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? "Menyimpan..." : "Simpan Pengguna"}
|
||||
{loading
|
||||
? "Menyimpan..."
|
||||
: "Simpan Pengguna"}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import type { UserData } from "../../routes/ManajementUsers";
|
|||
|
||||
import { FaEdit } from "react-icons/fa";
|
||||
import { MdDelete } from "react-icons/md";
|
||||
import { MdEmail } from "react-icons/md";
|
||||
|
||||
interface TableProps {
|
||||
dataUsers: UserData[];
|
||||
|
|
@ -142,7 +143,7 @@ export default function UsersTable({
|
|||
colSpan={7}
|
||||
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>
|
||||
|
||||
</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">
|
||||
|
||||
<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>
|
||||
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
export default function BarangHero() {
|
||||
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">
|
||||
Kelola Barang Sekolah
|
||||
</h1>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
if (res.data.success || res.status === 200) {
|
||||
alert("Perubahan Inventaris Berhasil Diperbarui! ✏️");
|
||||
alert("Perubahan Inventaris Berhasil Diperbarui! <FaEdit />");
|
||||
onRefresh();
|
||||
onClose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import axios from "axios";
|
|||
import jsPDF from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
|
||||
import { FaHourglassEnd } from "react-icons/fa";
|
||||
import { FaBox } from "react-icons/fa";
|
||||
|
||||
export default function Laporan() {
|
||||
|
||||
/* =======================================================
|
||||
|
|
@ -139,7 +142,7 @@ export default function Laporan() {
|
|||
{/* RENDER KONDISIONAL KARTU LAPORAN */}
|
||||
{loading ? (
|
||||
<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>
|
||||
) : dataLaporan && dataLaporan.length > 0 ? (
|
||||
<div className="space-y-5">
|
||||
|
|
@ -212,7 +215,7 @@ export default function Laporan() {
|
|||
</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">
|
||||
Tidak ditemukan rekapan laporan transaksi barang, Bang! 📦
|
||||
Tidak ditemukan rekapan laporan transaksi barang, Bang! <FaBox />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { IoIosSettings } from "react-icons/io";
|
||||
|
||||
export default function Settings() {
|
||||
const settingsMenu = [
|
||||
"Profil Toko",
|
||||
|
|
@ -65,7 +67,7 @@ export default function Settings() {
|
|||
text-2xl
|
||||
x
|
||||
">
|
||||
⚙️
|
||||
<IoIosSettings />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import axios from "axios";
|
|||
import jsPDF from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
|
||||
import { FaBox } from "react-icons/fa";
|
||||
import { FaHourglassEnd } from "react-icons/fa";
|
||||
|
||||
export default function TransaksiBAST() {
|
||||
|
||||
/* =======================================================
|
||||
|
|
@ -279,7 +282,7 @@ export default function TransaksiBAST() {
|
|||
{loading ? (
|
||||
|
||||
<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>
|
||||
|
||||
) : 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">
|
||||
Belum ada riwayat dokumen transaksi BAST terdaftar, Bang! 📦
|
||||
Belum ada riwayat dokumen transaksi BAST terdaftar, Bang! <FaBox />
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import axios from "axios";
|
|||
import UsersHero from "../components/manajemenUsers/UserHero";
|
||||
import UsersFilter from "../components/manajemenUsers/UserFilter";
|
||||
import UsersTable from "../components/manajemenUsers/UserTable";
|
||||
import { FaHourglassEnd } from "react-icons/fa";
|
||||
|
||||
// Definisikan tipe data User sesuai kolom database CIFO Abang
|
||||
export interface UserData {
|
||||
|
|
@ -83,7 +84,7 @@ export default function ManajemenUsers() {
|
|||
{loading ? (
|
||||
<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">
|
||||
Sabar Bang, lagi narik data dari DB CIFO... ⏳
|
||||
Sabar Bang, lagi narik data dari DB CIFO... <FaHourglassEnd />
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
// src/routes/MasterData.tsx
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
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
|
||||
import BarangHero from "../components/masterData/BarangHero";
|
||||
import EditBarangModal from "../components/masterData/EditBarangModal";
|
||||
import TambahBarang from "../components/masterData/TambahBarang";
|
||||
|
||||
|
|
@ -13,38 +18,69 @@ export interface BarangType {
|
|||
kode_barang: string;
|
||||
nama_barang: string;
|
||||
stok: number;
|
||||
status_barang: string; // 'Baik' / 'Rusak'
|
||||
qr_image: string; // Base64 string dari backend Express
|
||||
status_barang: string;
|
||||
qr_image: string;
|
||||
foto_barang: string | null;
|
||||
nama_kategori?: string;
|
||||
nama_lokasi?: string;
|
||||
}
|
||||
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const respon = await axios.get("http://localhost:5000/api/barang");
|
||||
|
||||
// Pengaman jika struktur response backend dibungkus { success: true, data: [...] }
|
||||
if (respon.data && respon.data.success && Array.isArray(respon.data.data)) {
|
||||
setLoading(true);
|
||||
|
||||
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);
|
||||
|
||||
} else if (Array.isArray(respon.data)) {
|
||||
|
||||
setBarangList(respon.data);
|
||||
|
||||
} else {
|
||||
|
||||
setBarangList([]);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Gagal memuat data dari API:", error);
|
||||
|
||||
console.error(
|
||||
"Gagal memuat data dari API:",
|
||||
error
|
||||
);
|
||||
|
||||
setBarangList([]);
|
||||
|
||||
} finally {
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -53,40 +89,87 @@ export default function MasterData() {
|
|||
muatDataBarang();
|
||||
}, []);
|
||||
|
||||
// 2. FUNGSI FILTER KATEGORI SECARA DINAMIS
|
||||
const filteredBarang = barangList.filter((barang) => {
|
||||
if (selectedKategori === "Semua") return true;
|
||||
return barang.nama_kategori?.toLowerCase() === selectedKategori.toLowerCase();
|
||||
// FILTER KATEGORI
|
||||
const filteredBarang =
|
||||
barangList.filter((barang) => {
|
||||
|
||||
if (selectedKategori === "Semua")
|
||||
return true;
|
||||
|
||||
return (
|
||||
barang.nama_kategori?.toLowerCase() ===
|
||||
selectedKategori.toLowerCase()
|
||||
);
|
||||
});
|
||||
|
||||
// 3. TRIGGER SETELAH BERHASIL SAVE / EDIT / TAMBAH
|
||||
// REFRESH
|
||||
const handleSaveSuccess = () => {
|
||||
muatDataBarang(); // Refresh list barang dari DB
|
||||
setSelectedBarang(null); // Tutup Modal Edit
|
||||
setShowTambah(false); // Tutup Modal Tambah
|
||||
|
||||
muatDataBarang();
|
||||
|
||||
setSelectedBarang(null);
|
||||
|
||||
setShowTambah(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="flex-1 w-full">
|
||||
<BarangHero />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowTambah(true)}
|
||||
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"
|
||||
>
|
||||
➕ Tambah Barang UKK
|
||||
</button>
|
||||
|
||||
{/* HEADER HERO */}
|
||||
<div className="mb-6">
|
||||
|
||||
<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">
|
||||
|
||||
{/* TEXT HERO */}
|
||||
<div className="flex-1">
|
||||
|
||||
<h1 className="text-4xl font-extrabold tracking-tight mb-2">
|
||||
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>
|
||||
|
||||
{/* 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">
|
||||
{["Semua", "Elektronik", "Buku dan Modul", "Alat Tulis"].map((kat) => (
|
||||
|
||||
{[
|
||||
"Semua",
|
||||
"Elektronik",
|
||||
"Buku dan Modul",
|
||||
"Alat Tulis",
|
||||
].map((kat) => (
|
||||
|
||||
<button
|
||||
key={kat}
|
||||
onClick={() => setSelectedKategori(kat)}
|
||||
onClick={() =>
|
||||
setSelectedKategori(kat)
|
||||
}
|
||||
className={`px-5 py-2.5 rounded-xl font-semibold transition text-sm ${
|
||||
selectedKategori === kat
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
|
|
@ -95,108 +178,223 @@ export default function MasterData() {
|
|||
>
|
||||
{kat}
|
||||
</button>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
{/* GRID DAFTAR INVENTARIS UTAMA */}
|
||||
{/* GRID */}
|
||||
{loading ? (
|
||||
|
||||
<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 className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
|
||||
{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">
|
||||
Belum ada aset terdaftar di kategori ini, Bang! 📦
|
||||
Belum ada aset terdaftar di kategori ini, Bang! <FaBox />
|
||||
</div>
|
||||
|
||||
) : (
|
||||
|
||||
filteredBarang.map((barang) => (
|
||||
|
||||
<div
|
||||
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"
|
||||
>
|
||||
|
||||
<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">
|
||||
|
||||
{barang.foto_barang ? (
|
||||
|
||||
<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}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none'; // Sembunyikan gambar yang rusak
|
||||
const parent = target.parentElement;
|
||||
|
||||
const target =
|
||||
e.target as HTMLImageElement;
|
||||
|
||||
target.style.display =
|
||||
"none";
|
||||
|
||||
const parent =
|
||||
target.parentElement;
|
||||
|
||||
if (parent) {
|
||||
const fallback = document.createElement('div');
|
||||
fallback.className = 'flex flex-col items-center justify-center text-gray-400';
|
||||
fallback.innerHTML = '<span class="text-4xl mb-1">📦</span><span class="text-xs font-medium">No Image</span>';
|
||||
parent.appendChild(fallback);
|
||||
|
||||
const fallback =
|
||||
document.createElement(
|
||||
"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">
|
||||
<span className="text-4xl mb-1">📦</span>
|
||||
<span className="text-xs font-medium">No Image</span>
|
||||
</div>
|
||||
)}
|
||||
<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 className="text-4xl mb-1">
|
||||
<FaBox />
|
||||
</span>
|
||||
|
||||
<span className="text-xs font-medium">
|
||||
No Image
|
||||
</span>
|
||||
|
||||
</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">
|
||||
{barang.kode_barang}
|
||||
</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>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>
|
||||
|
||||
{/* 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">
|
||||
|
||||
{/* QR */}
|
||||
{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">
|
||||
<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>
|
||||
|
||||
{/* BUTTON EDIT */}
|
||||
<button
|
||||
onClick={() => setSelectedBarang(barang)}
|
||||
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"
|
||||
onClick={() =>
|
||||
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>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
))
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{/* RENDER MODAL EDIT BARANG (Props disinkronkan dengan EditBarangModal.tsx baru) */}
|
||||
{/* MODAL EDIT */}
|
||||
{selectedBarang && (
|
||||
|
||||
<EditBarangModal
|
||||
itemSelected={selectedBarang}
|
||||
onClose={() => setSelectedBarang(null)}
|
||||
onClose={() =>
|
||||
setSelectedBarang(null)
|
||||
}
|
||||
onRefresh={handleSaveSuccess}
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
{/* RENDER MODAL TAMBAH BARANG (Props disinkronkan dengan TambahBarang.tsx baru) */}
|
||||
{/* MODAL TAMBAH */}
|
||||
{showTambah && (
|
||||
|
||||
<TambahBarang
|
||||
onClose={() => setShowTambah(false)}
|
||||
onClose={() =>
|
||||
setShowTambah(false)
|
||||
}
|
||||
onRefresh={handleSaveSuccess}
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue