diff --git a/src/Dashboard.js b/src/Dashboard.js index 2243644..1f5a300 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -1,8 +1,9 @@ +// Dashboard.jsx import React, { useState, useRef, useEffect } from "react"; import styles from "./Dashboard.module.css"; import { useNavigate } from "react-router-dom"; -// Pastikan Anda sudah menginstal Recharts: npm install recharts -// import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; // Contoh Recharts +import FileListComponent from "./FileListComponent"; +// import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; const Dashboard = () => { const navigate = useNavigate(); @@ -52,10 +53,6 @@ const Dashboard = () => { setUser(data[0].payload); - // Pastikan API Anda mengembalikan data ini, contoh: - // data[0].payload.stats = { today: 120, month: 2500, overall: 15000 }; - // data[0].payload.officerPerformance = [{ name: "Budi", filesSent: 50 }, { name: "Ani", filesSent: 70 }]; - if (data[0].payload.stats) { setTotalFilesSentToday(data[0].payload.stats.today); setTotalFilesSentMonth(data[0].payload.stats.month); @@ -112,7 +109,6 @@ const Dashboard = () => { setUsername(""); setPassword(""); setErrorMessage(""); - // Pertimbangkan untuk memuat ulang data performa jika penambahan officer baru mempengaruhi grafik } catch (error) { setErrorMessage(error.message || "Gagal menambahkan officer"); setSuccessMessage(""); @@ -251,24 +247,9 @@ const Dashboard = () => { )} - {/* Chart Section - Visible to both Admin and Officer */}

Performa Pengiriman File

{officerPerformanceData.length > 0 ? ( - // Contoh implementasi Recharts: - /* - - - - - - - - - */
📊 Grafik performa akan ditampilkan di sini
@@ -281,6 +262,9 @@ const Dashboard = () => { )}
+ + {/* ✅ Tambahkan FileListComponent di sini */} +
diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css index 0139a58..7db97ca 100644 --- a/src/Dashboard.module.css +++ b/src/Dashboard.module.css @@ -1,3 +1,5 @@ +/* Dashboard.module.css - Cleaned Version */ + /* Modern Color Palette */ :root { --primary-blue: #3b82f6; diff --git a/src/FileListComponent.js b/src/FileListComponent.js new file mode 100644 index 0000000..dfebe2b --- /dev/null +++ b/src/FileListComponent.js @@ -0,0 +1,236 @@ +import React, { useState, useEffect } from "react"; +import styles from "./FileListComponent.module.css"; + +const FileListComponent = () => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedFile, setSelectedFile] = useState(null); + const [successMessage, setSuccessMessage] = useState(""); + + useEffect(() => { + const fetchFiles = async () => { + try { + const response = await fetch( + "https://bot.kediritechnopark.com/webhook/api/files" + ); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const text = await response.text(); + if (!text) { + throw new Error("Server membalas kosong."); + } + + const data = JSON.parse(text); + + if (!data.success || !Array.isArray(data.data)) { + throw new Error("Format respons tidak valid."); + } + + setFiles(data.data); + setLoading(false); + } catch (error) { + console.error("Gagal mengambil data dari server:", error.message); + setLoading(false); + } + }; + + fetchFiles(); + }, []); + + const formatPhoneNumber = (phone) => + phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3"); + + const handleRowClick = async (file) => { + try { + const response = await fetch( + `https://bot.kediritechnopark.com/webhook/8a68d17e-c987-468c-853a-1c7d8104b5ba/api/files/${file.nik}` + ); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const text = await response.text(); + if (!text) { + throw new Error("Respons kosong dari server."); + } + + const data = JSON.parse(text); + + if (data.error) { + alert(data.error); + return; + } + + setSelectedFile(data); + } catch (error) { + console.error("Gagal mengambil detail:", error.message || error); + alert("Gagal mengambil detail. Pastikan data tersedia."); + } + }; + + const closeModal = () => { + setSelectedFile(null); + }; + + if (loading) { + return ( +
+
+
+
Memuat file...
+
+
+ ); + } + + return ( +
+
+

📁 Daftar File

+ {files.length} file tersedia +
+ + {successMessage && ( +
+ + {successMessage} +
+ )} + +
+ {files.length === 0 ? ( +
+
Belum ada data
+

+ Tidak ada data KTP yang tersedia saat ini. +

+
+ ) : ( + + + + + + + + + + + + {files.map((file, index) => ( + handleRowClick(file)} + className={styles.tableRow} + > + + + + + + + ))} + +
IDNIKNama LengkapNo. HPEmail
{index + 1}{file.nik}{file.nama_lengkap}{formatPhoneNumber(file.no_hp)}{file.email}
+ )} +
+ + {/* Modal Detail */} + {selectedFile && ( +
+
e.stopPropagation()} + > +

🪪 Detail Data KTP

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NIK{selectedFile.nik}
Nama Lengkap{selectedFile.nama_lengkap}
Tempat Lahir{selectedFile.tempat_lahir}
Tanggal Lahir{selectedFile.tanggal_lahir}
Jenis Kelamin{selectedFile.jenis_kelamin}
Alamat{selectedFile.alamat}
RT/RW{selectedFile.rt_rw}
Kelurahan/Desa{selectedFile.kel_desa}
Kecamatan{selectedFile.kecamatan}
Agama{selectedFile.agama}
Status Perkawinan{selectedFile.status_perkawinan}
Pekerjaan{selectedFile.pekerjaan}
Kewarganegaraan{selectedFile.kewarganegaraan}
No HP{selectedFile.no_hp}
Email{selectedFile.email}
Berlaku Hingga{selectedFile.berlaku_hingga}
Tanggal Pembuatan{selectedFile.pembuatan}
Kota Pembuatan{selectedFile.kota_pembuatan}
+ +
+
+ )} +
+ ); +}; + +export default FileListComponent; diff --git a/src/FileListComponent.module.css b/src/FileListComponent.module.css new file mode 100644 index 0000000..5415ee8 --- /dev/null +++ b/src/FileListComponent.module.css @@ -0,0 +1,483 @@ +/* FileListComponent.module.css - Updated to match Dashboard design */ + +/* Use the same color palette as Dashboard */ +:root { + --primary-blue: #3b82f6; + --secondary-blue: #60a5fa; + --dark-blue: #1e40af; + --neutral-50: #fafafa; + --neutral-100: #f5f5f5; + --neutral-200: #e5e5e5; + --neutral-300: #d4d4d4; + --neutral-500: #737373; + --neutral-700: #404040; + --neutral-800: #262626; + --neutral-900: #171717; + --white: #ffffff; + --success-green: #10b981; + --warning-amber: #f59e0b; + --error-red: #ef4444; + --text-primary: #0f172a; + --text-secondary: #64748b; + --text-light: #ffffff; + --border-light: #e2e8f0; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +/* File List Section */ +.fileListSection { + background-color: var(--white); + padding: 2rem; + border-radius: 1rem; + border: 1px solid var(--border-light); + box-shadow: var(--shadow-sm); + margin: 2rem auto; + max-width: 1200px; + width: 100%; + overflow: hidden; + max-height: 600px; + height: auto; + min-height: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + transition: all 0.2s ease; +} + +.fileListSection:hover { + box-shadow: var(--shadow-md); +} + +.fileListHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + flex-shrink: 0; + width: 100%; +} + +.fileListTitle { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.025em; +} + +.fileCount { + font-size: 0.875rem; + color: #ffffff; + font-weight: 500; + background-color: #ef4444; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + border: 1px solid var(--border-light); +} + +.successMessage { + background-color: rgb(16 185 129 / 0.1); + color: var(--success-green); + border: 1px solid rgb(16 185 129 / 0.2); + padding: 0.75rem 1rem; + border-radius: 0.5rem; + margin-bottom: 1rem; + font-size: 0.875rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; + width: 100%; +} + +.tableContainer { + flex: 1; + overflow: auto; + border-radius: 0.75rem; + border: 1px solid var(--border-light); + background-color: var(--white); + width: 100%; +} + +.fileTable { + width: 100%; + min-width: 600px; + table-layout: auto; + border-collapse: collapse; + font-size: 0.875rem; + background-color: var(--white); +} + +.fileTable th { + background-color: #ef4444; + padding: 0.75rem; + text-align: center; + font-weight: 600; + color: #ffffff; + border-bottom: 1px solid var(--border-light); + white-space: nowrap; + position: sticky; + top: 0; + z-index: 1; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.fileTable td { + padding: 0.75rem; + border-bottom: 1px solid var(--border-light); + color: var(--text-primary); + vertical-align: middle; +} + +.tableRow { + cursor: pointer; + transition: background-color 0.2s ease; +} + +.tableRow:hover { + background-color: var(--neutral-50); +} + +.nameColumn { + font-weight: 500; + color: var(--text-primary); + min-width: 200px; +} + +.emptyState { + text-align: center; + padding: 3rem 2rem; + color: var(--text-secondary); +} + +.emptyStateTitle { + font-size: 1.125rem; + margin-bottom: 0.5rem; + color: var(--text-primary); + font-weight: 600; +} + +.emptyStateText { + font-size: 0.875rem; + margin: 0; + color: var(--text-secondary); +} + +.spinner { + width: 2rem; + height: 2rem; + border: 3px solid var(--neutral-300); + border-top: 3px solid #ef4444; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Custom Scrollbar */ +.tableContainer::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.tableContainer::-webkit-scrollbar-track { + background: var(--neutral-100); + border-radius: 4px; +} + +.tableContainer::-webkit-scrollbar-thumb { + background: #ef4444; + border-radius: 4px; + transition: background 0.2s ease; +} + +.tableContainer::-webkit-scrollbar-thumb:hover { + background: #dc2626; +} + +.tableContainer::-webkit-scrollbar-corner { + background: var(--neutral-100); +} + +.tableContainer { + scrollbar-width: thin; + scrollbar-color: #ef4444 var(--neutral-100); +} + +/* Modal Styles - Matching Dashboard Design */ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modalContent { + background: var(--white); + padding: 2rem; + border-radius: 1rem; + max-width: 600px; + width: 90%; + max-height: 85vh; + overflow-y: auto; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-light); +} + +.modalContent h3 { + margin: 0 0 1.5rem 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.025em; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-light); +} + +.detailTable { + width: 100%; + border-collapse: collapse; + margin-bottom: 1.5rem; + font-size: 0.875rem; +} + +.detailTable tr:nth-child(even) { + background-color: var(--neutral-50); +} + +.detailTable td { + padding: 0.75rem; + border-bottom: 1px solid var(--border-light); + vertical-align: top; +} + +.detailTable td:first-child { + font-weight: 600; + color: var(--text-secondary); + width: 35%; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.detailTable td:last-child { + color: var(--text-primary); + font-weight: 500; +} + +.closeButton { + background-color: #ef4444; + color: var(--text-light); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + width: 100%; + transition: all 0.2s ease; + letter-spacing: 0.025em; +} + +.closeButton:hover { + background-color: #dc2626; + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.closeButton:active { + transform: translateY(0); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .fileListSection { + padding: 1.5rem; + margin: 1rem; + max-width: 100%; + height: auto; + max-height: 70vh; + } + + .fileListHeader { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .fileListTitle { + font-size: 1.125rem; + } + + /* Mobile: Show only NIK and Name columns */ + .fileTable { + min-width: 100%; + } + + .fileTable th:not(:nth-child(2)):not(:nth-child(3)), + .fileTable td:not(:nth-child(2)):not(:nth-child(3)) { + display: none; + } + + .fileTable th, + .fileTable td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } + + .fileTable th:nth-child(2) { + width: 40%; + } + + .fileTable th:nth-child(3) { + width: 60%; + } + + .nameColumn { + min-width: unset; + } + + /* Modal responsive */ + .modalContent { + padding: 1.5rem; + width: 95%; + max-height: 90vh; + border-radius: 0.75rem; + } + + .modalContent h3 { + font-size: 1.125rem; + } + + .detailTable { + font-size: 0.8125rem; + } + + .detailTable td { + padding: 0.625rem 0.5rem; + } + + .detailTable td:first-child { + width: 40%; + } +} + +@media (max-width: 480px) { + .fileListSection { + padding: 1rem; + margin: 0.5rem; + border-radius: 0.75rem; + } + + .fileListTitle { + font-size: 1rem; + } + + .fileCount { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + .fileTable th, + .fileTable td { + padding: 0.5rem 0.375rem; + font-size: 0.8125rem; + } + + .modalContent { + padding: 1rem; + width: 98%; + border-radius: 0.5rem; + } + + .modalContent h3 { + font-size: 1rem; + margin-bottom: 1rem; + } + + .detailTable { + font-size: 0.75rem; + } + + .detailTable td { + padding: 0.5rem 0.375rem; + } + + .closeButton { + padding: 0.625rem 1rem; + font-size: 0.8125rem; + } +} + +/* Tablet and Desktop enhancements */ +@media (min-width: 769px) { + .fileListSection { + padding: 2.5rem; + margin: 2.5rem auto; + } + + .fileListTitle { + font-size: 1.5rem; + } + + .fileTable th, + .fileTable td { + padding: 1rem 0.75rem; + } + + .modalContent { + padding: 2.5rem; + border-radius: 1rem; + } + + .modalContent h3 { + font-size: 1.5rem; + margin-bottom: 2rem; + } + + .detailTable { + font-size: 0.875rem; + } + + .detailTable td { + padding: 1rem 0.75rem; + } + + .closeButton { + padding: 0.875rem 2rem; + width: auto; + margin-left: auto; + display: block; + } +} + +@media (min-width: 1024px) { + .fileListSection { + padding: 3rem; + margin: 3rem auto; + } +} diff --git a/src/FormComponent.js b/src/FormComponent.js index 452cda9..17e958e 100644 --- a/src/FormComponent.js +++ b/src/FormComponent.js @@ -17,6 +17,8 @@ const fieldLabels = { validUntil: "Berlaku Hingga", issuedCity: "Kota Terbit", issuedDate: "Tanggal Terbit", + phoneNumber: "No. HP", + email: "Email", }; function Modal({ isOpen, onClose, loading, fileTemp, onSave, onDelete }) { @@ -88,7 +90,14 @@ function Modal({ isOpen, onClose, loading, fileTemp, onSave, onDelete }) { ["nik", "fullName", "birthPlace", "birthDate"], ["gender", "address", "neighborhoodCode", "village", "subDistrict"], ["religion", "maritalStatus", "occupation"], - ["nationality", "validUntil", "issuedCity", "issuedDate"], + [ + "nationality", + "validUntil", + "issuedCity", + "issuedDate", + "phoneNumber", + "email", + ], ]; // Filter field yang disable/hide