This commit is contained in:
john aperkat
2025-07-30 04:18:44 +00:00
parent 3206db6010
commit afe9b24f56
19 changed files with 1983 additions and 657 deletions

View File

@@ -43,7 +43,7 @@ const Dashboard = () => {
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/dashboard/psi",
"https://bot.kediritechnopark.com/webhook/solid-data/dashboard",
{
method: "GET",
headers: {
@@ -75,7 +75,7 @@ const Dashboard = () => {
const token = localStorage.getItem("token");
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/list-user/psi",
"https://bot.kediritechnopark.com/webhook/solid-data/list-user",
{
method: "GET",
headers: {
@@ -110,7 +110,7 @@ const Dashboard = () => {
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/add-officer",
"https://bot.kediritechnopark.com/webhook/solid-data/add-officer",
{
method: "POST",
headers: {
@@ -163,7 +163,7 @@ const Dashboard = () => {
try {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/psi/delete-officer`,
`https://bot.kediritechnopark.com/webhook/solid-data/delete-officer`,
{
method: "DELETE",
headers: {
@@ -193,8 +193,11 @@ const Dashboard = () => {
<div className={styles.dashboardContainer}>
<div className={styles.dashboardHeader}>
<div className={styles.logoAndTitle}>
<img src="/PSI.png" alt="Bot Avatar" />
<h1 className={styles.h1}>Kawal PSI Dashboard</h1>
<img src="/ikasapta.png" alt="Bot Avatar" />
<h1 className={styles.h1}>SOLID</h1>
<h1 className={styles.h1} styles="color: #43a0a7;">
DATA
</h1>
</div>
<div className={styles.dropdownContainer} ref={menuRef}>
@@ -343,7 +346,7 @@ const Dashboard = () => {
)}
<div className={styles.chartSection}>
<h2>Grafik Pertumbuhan Anggota</h2>
<h2>Grafik Upload Document</h2>
{officerPerformanceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={officerPerformanceData}>
@@ -355,7 +358,7 @@ const Dashboard = () => {
</ResponsiveContainer>
) : (
<div className={styles.warning}>
📋 Belum ada data performa untuk ditampilkan
📋 Belum ada data upload untuk ditampilkan
</div>
)}
</div>
@@ -370,7 +373,7 @@ const Dashboard = () => {
</div>
<div className={styles.footer}>
© 2025 Kediri Technopark Dashboard PSI
© 2025 Kediri Technopark Dashboard SOLID DATA
</div>
</div>
);

View File

@@ -14,7 +14,7 @@
--neutral-800: #262626;
--neutral-900: #171717;
--white: #ffffff;
--success-green: #10b981;
--success-green: #43a0a7;
--warning-amber: #f59e0b;
--error-red: #ef4444;
--text-primary: #0f172a;
@@ -59,7 +59,7 @@ body {
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-sm);
border-bottom: 3px solid #ef4444;
border-bottom: 3px solid #43a0a7;
position: sticky;
top: 0;
z-index: 50;
@@ -81,10 +81,18 @@ body {
}
.dashboardHeader .h1 {
margin: 2px;
font-size: 1.5rem;
font-weight: 700;
color: #43a0a7;
letter-spacing: -0.025em;
}
.data {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #ed4344;
color: #154666;
letter-spacing: -0.025em;
}
@@ -207,7 +215,7 @@ body {
.summaryCard p {
font-size: 2rem;
font-weight: 700;
color: #ef4444;
color: #43a0a7;
margin: 0;
line-height: 1;
}
@@ -270,7 +278,7 @@ body {
}
.submitButton {
background-color: #ef4444;
background-color: #43a0a7;
color: var(--text-light);
border: none;
padding: 0.75rem 1.5rem;
@@ -285,7 +293,7 @@ body {
}
.submitButton:hover {
background-color: #d03b3b;
background-color: #357734;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
@@ -296,9 +304,9 @@ body {
/* Messages */
.success {
background-color: rgb(16 185 129 / 0.1);
background-color: rgb(67 160 167 / 0.1);
color: var(--success-green);
border: 1px solid rgb(16 185 129 / 0.2);
border: 1px solid rgb(67 160 167 / 0.2);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
margin-top: 1rem;
@@ -318,9 +326,9 @@ body {
}
.warning {
background-color: #ef444417;
color: #ef4444;
border: 1px solid #ef444433;
background-color: rgb(67 160 167 / 0.1);
color: #43a0a7;
border: 1px solid rgb(67 160 167 / 0.2);
padding: 1rem;
border-radius: 0.5rem;
margin-top: 1rem;

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
import styles from "./FileListComponent.module.css";
import * as XLSX from "xlsx";
import { PDFDownloadLink } from "@react-pdf/renderer";
import KTPPDF from "./KTPPDF";
const FileListComponent = ({
setTotalFilesSentToday,
@@ -9,17 +11,18 @@ const FileListComponent = ({
setOfficerPerformanceData,
}) => {
const [files, setFiles] = useState([]);
const [filteredFiles, setFilteredFiles] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedFile, setSelectedFile] = useState(null);
const [successMessage, setSuccessMessage] = useState("");
const [selectedDocumentType, setSelectedDocumentType] = useState("");
useEffect(() => {
const fetchFiles = async () => {
const token = localStorage.getItem("token");
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/files",
"https://bot.kediritechnopark.com/webhook/solid-data/files",
{
method: "GET",
headers: {
@@ -29,35 +32,25 @@ const FileListComponent = ({
}
);
if (!response.ok) {
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`);
}
const text = await response.text();
if (!text) {
throw new Error("Server membalas kosong.");
}
if (!text) throw new Error("Server membalas kosong.");
const data = JSON.parse(text);
if (!data.success || !Array.isArray(data.data)) {
if (!data.success || !Array.isArray(data.data))
throw new Error("Format respons tidak valid.");
}
const fileData = data.data;
// 1. Set ke state
setFiles(fileData);
setFilteredFiles(fileData);
// 2. Hitung total file hari ini
const today = new Date().toISOString().slice(0, 10);
const totalToday = fileData.filter((f) =>
f.created_at.startsWith(today)
).length;
setTotalFilesSentToday(totalToday);
// 3. Hitung total bulan ini
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
@@ -69,10 +62,8 @@ const FileListComponent = ({
}).length;
setTotalFilesSentMonth(totalThisMonth);
// 4. Total keseluruhan
setTotalFilesSentOverall(fileData.length);
// 5. Grafik performa per bulan (dinamis)
const dateObjects = fileData.map((item) => new Date(item.created_at));
if (dateObjects.length > 0) {
const minDate = new Date(Math.min(...dateObjects));
@@ -95,19 +86,17 @@ const FileListComponent = ({
const monthKey = `${d.getFullYear()}-${String(
d.getMonth() + 1
).padStart(2, "0")}`;
if (monthlyDataMap[monthKey] !== undefined) {
if (monthlyDataMap[monthKey] !== undefined)
monthlyDataMap[monthKey]++;
}
});
const performanceArray = Object.entries(monthlyDataMap).map(
([month, count]) => {
const [year, monthNum] = month.split("-");
const dateObj = new Date(`${month}-01`);
const label = new Intl.DateTimeFormat("id-ID", {
month: "long",
year: "numeric",
}).format(dateObj); // hasil: "Juli 2025"
}).format(dateObj);
return { month: label, count };
}
);
@@ -124,11 +113,18 @@ const FileListComponent = ({
fetchFiles();
}, []);
const formatPhoneNumber = (phone) =>
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3");
useEffect(() => {
if (selectedDocumentType) {
setFilteredFiles(
files.filter((file) => file.document_type === selectedDocumentType)
);
} else {
setFilteredFiles(files);
}
}, [selectedDocumentType, files]);
const handleRowClick = async (file) => {
const token = localStorage.getItem("token");
if (!token) {
alert("Token tidak ditemukan. Silakan login kembali.");
return;
@@ -136,60 +132,51 @@ const FileListComponent = ({
try {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/6915ea36-e1f4-49ad-a7f1-a27ce0bf2279/ktp/${file.nik}`,
`https://bot.kediritechnopark.com/webhook/solid-data/merged?nama_lengkap=${encodeURIComponent(
file.nama_lengkap
)}`,
{
method: "GET",
headers: {
Authorization: token, // atau `Bearer ${token}` jika diperlukan
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
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.");
}
if (!text) throw new Error("Respons kosong dari server.");
const data = JSON.parse(text);
if (data.error) {
alert(data.error);
return;
}
const item = data[0];
if (!item) {
alert("Data tidak ditemukan.");
return;
}
// Validasi jika ada image URL
if (item.foto_url && !item.foto_url.match(/\.(jpg|jpeg|png|webp)$/i)) {
console.warn(
"URL foto bukan format gambar yang didukung:",
item.foto_url
);
}
setSelectedFile(item); // tampilkan di modal misalnya
setSelectedFile(data[0]);
} catch (error) {
console.error("Gagal mengambil detail:", error.message || error);
console.error("Gagal mengambil detail:", error.message);
alert("Gagal mengambil detail. Pastikan data tersedia.");
}
};
const closeModal = () => {
setSelectedFile(null);
const getImageSrc = (base64) => {
if (!base64) return null;
const cleaned = base64.replace(/\s/g, "");
if (cleaned.startsWith("iVBOR")) return `data:image/png;base64,${cleaned}`;
if (cleaned.startsWith("/9j/")) return `data:image/jpeg;base64,${cleaned}`;
if (cleaned.startsWith("UklGR")) return `data:image/webp;base64,${cleaned}`;
return `data:image/*;base64,${cleaned}`;
};
const exportToExcel = (data) => {
const domain = window.location.origin;
const closeModal = () => setSelectedFile(null);
const formatPhoneNumber = (phone) =>
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3");
const exportToExcel = (data) => {
const modifiedData = data.map((item) => ({
ID: item.id,
Petugas_ID: item.petugas_id,
@@ -200,7 +187,8 @@ const FileListComponent = ({
Tanggal_Lahir: new Date(item.tanggal_lahir),
Jenis_Kelamin: item.jenis_kelamin,
Alamat: item.alamat,
RT_RW: item.rt_rw,
RT: item.rt,
RW: item.rw,
Kel_Desa: item.kel_desa,
Kecamatan: item.kecamatan,
Agama: item.agama,
@@ -213,61 +201,40 @@ const FileListComponent = ({
Pembuatan: new Date(item.pembuatan),
Kota_Pembuatan: item.kota_pembuatan,
Created_At: new Date(item.created_at),
ImageURL: `${domain}/${item.nik}`,
}));
const worksheet = XLSX.utils.json_to_sheet(modifiedData);
// Add hyperlink to ImageURL column (last column)
modifiedData.forEach((item, index) => {
const cellAddress = `W${index + 2}`; // Column W (ImageURL), starts at row 2
if (worksheet[cellAddress]) {
worksheet[cellAddress].l = {
Target: item.ImageURL,
Tooltip: "Lihat Gambar",
};
}
});
// Optional: Auto column widths (you can fine-tune)
worksheet["!cols"] = new Array(Object.keys(modifiedData[0]).length).fill({
wch: 20,
});
// Add autofilter
worksheet["!autofilter"] = { ref: `A1:W1` }; // Covers all columns (A to W)
// Export
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
XLSX.writeFile(workbook, "data-export.xlsx");
};
if (loading) {
return (
<div className={styles.fileListSection}>
<div className={styles.emptyState}>
<div className={styles.spinner}></div>
<div className={styles.emptyStateTitle}>Memuat file...</div>
</div>
</div>
);
}
return (
<div className={styles.fileListSection}>
<div className={styles.fileListHeader}>
<h2 className={styles.fileListTitle}>📁 Daftar Anggota</h2>
<h2 className={styles.fileListTitle}>📁 Daftar Document</h2>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<select
value={selectedDocumentType}
onChange={(e) => setSelectedDocumentType(e.target.value)}
className={styles.fileCount}
>
<option value="">Semua</option>
<option value="ktp">KTP</option>
<option value="kk">KK</option>
<option value="akta_kelahiran">Akta Kelahiran</option>
</select>
<button
onClick={() => {
exportToExcel(files);
exportToExcel(filteredFiles);
}}
className={styles.downloadButton}
>
Unduh Excel
</button>
<span className={styles.fileCount}>{files.length} anggota</span>
<span className={styles.fileCount}>
{filteredFiles.length} document
</span>
</div>
</div>
@@ -279,11 +246,11 @@ const FileListComponent = ({
)}
<div className={styles.tableContainer}>
{files.length === 0 ? (
{filteredFiles.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyStateTitle}>Belum ada data</div>
<p className={styles.emptyStateText}>
Tidak ada data KTP yang tersedia saat ini.
Tidak ada data KK yang tersedia saat ini.
</p>
</div>
) : (
@@ -292,13 +259,12 @@ const FileListComponent = ({
<tr>
<th>ID</th>
<th>NIK</th>
<th>Jenis</th>
<th className={styles.nameColumn}>Nama Lengkap</th>
<th>No. HP</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{files.map((file, index) => (
{filteredFiles.map((file, index) => (
<tr
key={file.id}
onClick={() => handleRowClick(file)}
@@ -306,27 +272,26 @@ const FileListComponent = ({
>
<td>{index + 1}</td>
<td>{file.nik}</td>
<td>{file.document_type}</td>
<td className={styles.nameColumn}>{file.nama_lengkap}</td>
<td>{formatPhoneNumber(file.no_hp)}</td>
<td>{file.email}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Modal Detail */}
{/* Modal dan komponen lainnya tetap seperti sebelumnya */}
{selectedFile && (
<div className={styles.modalOverlay} onClick={closeModal}>
{" "}
<div
className={styles.modalContent}
onClick={(e) => e.stopPropagation()}
>
{/* Foto KTP */}
{" "}
{selectedFile.data && (
<img
src={`data:image/jpeg;base64,${selectedFile.data}`}
src={getImageSrc(selectedFile.data)}
alt={`Foto KTP - ${selectedFile.nik}`}
style={{
width: "100%",
@@ -337,89 +302,80 @@ const FileListComponent = ({
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
}}
/>
)}
<h3>🪪 Detail Data Anggota</h3>
)}{" "}
<h3>🪪 Detail Data Document</h3>
<div style={{ marginBottom: "1rem" }}>
<PDFDownloadLink
document={
<KTPPDF
data={{
...selectedFile,
data:
selectedFile.data?.startsWith("/") ||
selectedFile.data?.length < 50
? null
: selectedFile.data.replace(/\s/g, ""),
fallbackImage: selectedFile.foto_url,
}}
/>
}
fileName={`KTP_${selectedFile.nik}.pdf`}
style={{
textDecoration: "none",
padding: "8px 16px",
color: "#fff",
backgroundColor: "#00adef",
borderRadius: "6px",
display: "inline-block",
}}
>
{({ loading }) =>
loading ? "Menyiapkan PDF..." : "⬇️ Unduh PDF"
}
</PDFDownloadLink>
</div>
<table className={styles.detailTable}>
<tbody>
<tr>
<td>NIK</td>
<td>{selectedFile.nik}</td>
</tr>
<tr>
<td>Nama Lengkap</td>
<td>{selectedFile.nama_lengkap}</td>
</tr>
<tr>
<td>Tempat Lahir</td>
<td>{selectedFile.tempat_lahir}</td>
</tr>
<tr>
<td>Tanggal Lahir</td>
<td>{selectedFile.tanggal_lahir}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>{selectedFile.jenis_kelamin}</td>
</tr>
<tr>
<td>Alamat</td>
<td>{selectedFile.alamat}</td>
</tr>
<tr>
<td>RT/RW</td>
<td>{selectedFile.rt_rw}</td>
</tr>
<tr>
<td>Kelurahan/Desa</td>
<td>{selectedFile.kel_desa}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>{selectedFile.kecamatan}</td>
</tr>
<tr>
<td>Agama</td>
<td>{selectedFile.agama}</td>
</tr>
<tr>
<td>Status Perkawinan</td>
<td>{selectedFile.status_perkawinan}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>{selectedFile.pekerjaan}</td>
</tr>
<tr>
<td>Kewarganegaraan</td>
<td>{selectedFile.kewarganegaraan}</td>
</tr>
<tr>
<td>No HP</td>
<td>{selectedFile.no_hp}</td>
</tr>
<tr>
<td>Email</td>
<td>{selectedFile.email}</td>
</tr>
<tr>
<td>Berlaku Hingga</td>
<td>{selectedFile.berlaku_hingga}</td>
</tr>
<tr>
<td>Tanggal Pembuatan</td>
<td>{selectedFile.pembuatan}</td>
</tr>
<tr>
<td>Kota Pembuatan</td>
<td>{selectedFile.kota_pembuatan}</td>
</tr>
{[
["NIK", selectedFile.nik],
["No.Al", selectedFile.no_al],
["Nomor Akta Kelahiran", selectedFile.akta_kelahiran_nomor],
["Nama Lengkap", selectedFile.nama_lengkap],
["Anak Ke", selectedFile.anak_ke],
["Tempat Lahir", selectedFile.tempat_lahir],
["Tanggal Lahir", selectedFile.tanggal_lahir],
["Jenis Kelamin", selectedFile.jenis_kelamin],
["Alamat", selectedFile.alamat],
["Ayah", selectedFile.ayah],
["ibu", selectedFile.ibu],
["RT", selectedFile.rt],
["RW", selectedFile.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],
]
.filter(([_, value]) => value !== null && value !== "")
.map(([label, value]) => (
<tr key={label}>
<td>{label}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
<button className={styles.closeButton} onClick={closeModal}>
Tutup
</button>
</div>
{" "}
Tutup{" "}
</button>{" "}
</div>{" "}
</div>
)}
</div>

View File

@@ -14,7 +14,7 @@
--neutral-800: #262626;
--neutral-900: #171717;
--white: #ffffff;
--success-green: #10b981;
--success-green: #43a0a7;
--warning-amber: #f59e0b;
--error-red: #ef4444;
--text-primary: #0f172a;
@@ -72,19 +72,19 @@
}
.fileCount {
font-size: 0.875rem;
font-size: 0.6rem;
color: #ffffff;
font-weight: 500;
background-color: #ef4444;
background-color: #43a0a7;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
border: 1px solid var(--border-light);
}
.successMessage {
background-color: rgb(16 185 129 / 0.1);
background-color: rgb(67 160 167 / 0.1);
color: var(--success-green);
border: 1px solid rgb(16 185 129 / 0.2);
border: 1px solid rgb(67 160 167 / 0.2);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
@@ -116,7 +116,7 @@
}
.fileTable th {
background-color: #ef4444;
background-color: #43a0a7;
padding: 0.75rem;
text-align: center;
font-weight: 600;
@@ -176,7 +176,7 @@
width: 2rem;
height: 2rem;
border: 3px solid var(--neutral-300);
border-top: 3px solid #ef4444;
border-top: 3px solid #43a0a7;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
@@ -203,13 +203,13 @@
}
.tableContainer::-webkit-scrollbar-thumb {
background: #ef4444;
background: #43a0a7;
border-radius: 4px;
transition: background 0.2s ease;
}
.tableContainer::-webkit-scrollbar-thumb:hover {
background: #dc2626;
background: #306a2f;
}
.tableContainer::-webkit-scrollbar-corner {
@@ -218,7 +218,7 @@
.tableContainer {
scrollbar-width: thin;
scrollbar-color: #ef4444 var(--neutral-100);
scrollbar-color: #43a0a7 var(--neutral-100);
}
/* Modal Styles - Matching Dashboard Design */
@@ -291,7 +291,7 @@
}
.closeButton {
background-color: #ef4444;
background-color: #43a0a7;
color: var(--text-light);
border: none;
padding: 0.75rem 1.5rem;
@@ -399,7 +399,7 @@
}
.fileCount {
font-size: 0.75rem;
font-size: 0.6rem;
padding: 0.25rem 0.5rem;
}
@@ -482,14 +482,14 @@
}
.downloadButton {
background-color: #00adef;
background-color: #164665;
color: white;
border: none;
padding: 6px 12px;
border-radius: 8px;
padding: 0.25rem 0.5rem;
border-radius: 1rem;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
font-size: 0.6rem;
transition: background-color 0.3s ease;
}

123
src/KTPPDF.js Normal file
View File

@@ -0,0 +1,123 @@
// components/KTPPDF.js
import React from "react";
import FileListComponent from "./FileListComponent";
import {
Page,
Text,
Image,
Document,
StyleSheet,
View,
} from "@react-pdf/renderer";
const styles = StyleSheet.create({
page: { padding: 30, fontSize: 12 },
section: { marginBottom: 10 },
title: { fontSize: 18, marginBottom: 10 },
image: {
width: 180,
height: 120,
marginBottom: 10,
objectFit: "contain",
border: "1 solid #000",
},
label: { fontWeight: "bold" },
});
const getImageSrc = (base64) => {
if (!base64) return null;
const cleaned = base64.replace(/\s/g, "");
if (cleaned.startsWith("iVBOR")) {
return `data:image/png;base64,${cleaned}`;
} else if (cleaned.startsWith("/9j/")) {
return `data:image/jpeg;base64,${cleaned}`;
} else if (cleaned.startsWith("UklGR")) {
return `data:image/webp;base64,${cleaned}`;
} else {
return `data:image/*;base64,${cleaned}`;
}
};
const KTPPDF = ({ data }) => (
<Document>
<Page size="A4" style={styles.page}>
<Text style={styles.title}>Biodata Anggota</Text>
{data.data ? (
<Image style={styles.image} src={getImageSrc(data.data)} />
) : data.fallbackImage ? (
<Image style={styles.image} src={data.fallbackImage} />
) : (
<Text>Tidak ada foto KTP tersedia</Text>
)}
<View style={styles.section}>
<Text>
<Text style={styles.label}>NIK:</Text> {data.nik}
</Text>
<Text>
<Text style={styles.label}>Nama Lengkap:</Text> {data.nama_lengkap}
</Text>
<Text>
<Text style={styles.label}>Tempat Lahir:</Text>{" "}
{data.tempat_lahir || "-"}
</Text>
<Text>
<Text style={styles.label}>Tanggal Lahir:</Text>{" "}
{data.tanggal_lahir || "-"}
</Text>
<Text>
<Text style={styles.label}>Jenis Kelamin:</Text>{" "}
{data.jenis_kelamin || "-"}
</Text>
<Text>
<Text style={styles.label}>Alamat:</Text> {data.alamat || "-"}
</Text>
<Text>
<Text style={styles.label}>RT/RW:</Text> {data.rt_rw || "-"}
</Text>
<Text>
<Text style={styles.label}>Kel/Desa:</Text> {data.kel_desa || "-"}
</Text>
<Text>
<Text style={styles.label}>Kecamatan:</Text> {data.kecamatan || "-"}
</Text>
<Text>
<Text style={styles.label}>Agama:</Text> {data.agama || "-"}
</Text>
<Text>
<Text style={styles.label}>Status Perkawinan:</Text>{" "}
{data.status_perkawinan || "-"}
</Text>
<Text>
<Text style={styles.label}>Pekerjaan:</Text> {data.pekerjaan || "-"}
</Text>
<Text>
<Text style={styles.label}>Kewarganegaraan:</Text>{" "}
{data.kewarganegaraan || "-"}
</Text>
<Text>
<Text style={styles.label}>No HP:</Text> {data.no_hp || "-"}
</Text>
<Text>
<Text style={styles.label}>Email:</Text> {data.email || "-"}
</Text>
<Text>
<Text style={styles.label}>Berlaku Hingga:</Text>{" "}
{data.berlaku_hingga || "-"}
</Text>
<Text>
<Text style={styles.label}>Tanggal Pembuatan:</Text>{" "}
{data.pembuatan || "-"}
</Text>
<Text>
<Text style={styles.label}>Kota Pembuatan:</Text>{" "}
{data.kota_pembuatan || "-"}
</Text>
</View>
</Page>
</Document>
);
export default KTPPDF;

View File

@@ -1,6 +1,6 @@
.overlay-box {
position: absolute;
border: 3px dashed red;
border: 3px dashed #43a0a7;
width: 80%; /* atau sesuaikan */
aspect-ratio: 85.6 / 53.98;
top: 50%;

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ const Login = () => {
try {
const loginResponse = await fetch(
"https://bot.kediritechnopark.com/webhook/login/psi",
"https://bot.kediritechnopark.com/webhook/solid-data/login",
{
method: "POST",
headers: {
@@ -48,8 +48,8 @@ const Login = () => {
return (
<div className={styles.loginContainer}>
<div className={styles.loginBox}>
<img src="/PSI.png" alt="Logo" className={styles.logo} />
<h1 className={styles.h1}>Kawal PSI</h1>
<img src="/ikasapta.png" alt="Logo" className={styles.logo} />
<h1 className={styles.h1}>SOLID DATA</h1>
<p className={styles.subtitle}>
Silakan masuk untuk melanjutkan ke dashboard
</p>

View File

@@ -28,9 +28,8 @@
font-size: 28px;
font-weight: 700;
margin-bottom: 10px;
color: #ef4444; /* 🔴 Warna merah PSI */
color: #43a0a7;
}
.subtitle {
font-size: 14px;
color: #6b7280;
@@ -56,7 +55,7 @@
}
.button {
background-color: #ef4444; /* 🔴 Warna merah PSI */
background-color: #43a0a7;
color: #ffffff;
padding: 12px 24px;
border-radius: 24px;
@@ -69,7 +68,7 @@
}
.button:hover {
background-color: #b71c1c; /* versi lebih gelap saat hover */
background-color: #357734; /* darker shade of #43a0a7 */
}
.error {

View File

@@ -1,14 +1,15 @@
import React, { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import styles from "./ProfileTab.module.css";
import dashboardStyles from "./Dashboard.module.css";
import profileStyles from "./ProfileTab.module.css";
const ProfileTab = () => {
const menuRef = useRef(null);
const navigate = useNavigate();
const [isEditing, setIsEditing] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [profile, setProfile] = useState({});
const [profileTemp, setProfileTemp] = useState({});
const [user, setUser] = useState({});
const [userTemp, setUserTemp] = useState({});
useEffect(() => {
const handleClickOutside = (event) => {
@@ -27,34 +28,62 @@ const ProfileTab = () => {
};
useEffect(() => {
const dummyProfile = {
username: "admin",
const verifyTokenAndFetchData = async () => {
const token = localStorage.getItem("token");
if (!token) {
window.location.href = "/login";
return;
}
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/dashboard",
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json();
if (!response.ok || !data[0].username) {
throw new Error("Unauthorized");
}
setUser(data[0]);
setUserTemp(data[0]);
} catch (error) {
console.error("Token tidak valid:", error.message);
localStorage.removeItem("token");
window.location.href = "/login";
}
};
setProfile(dummyProfile);
setProfileTemp(dummyProfile);
verifyTokenAndFetchData();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setProfile((prev) => ({ ...prev, [name]: value }));
setUser((prev) => ({ ...prev, [name]: value }));
};
const handleSave = async () => {
try {
if (!profile.oldPassword || !profile.newPassword) {
if (!user.oldPassword || !user.newPassword) {
alert("Password lama dan baru tidak boleh kosong.");
return;
}
const payload = {
username: profile.username,
oldPassword: profile.oldPassword,
newPassword: profile.newPassword,
username: user.username,
oldPassword: user.oldPassword,
newPassword: user.newPassword,
};
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/reset-password/psi",
"https://bot.kediritechnopark.com/webhook/solid-data/reset-password",
{
method: "PUT",
headers: {
@@ -77,21 +106,26 @@ const ProfileTab = () => {
const handleCancel = () => {
setIsEditing(false);
setProfile(profileTemp);
setUser(userTemp);
};
return (
<div className={styles.dashboardContainer}>
<div className={styles.dashboardHeader}>
<div className={styles.logoAndTitle}>
<img src="/PSI.png" alt="Profile Avatar" />
<h1 className={styles.h1}>Kawal PSI Profile</h1>
<div className={dashboardStyles.dashboardContainer}>
<div className={dashboardStyles.dashboardHeader}>
<div className={dashboardStyles.logoAndTitle}>
<img src="/ikasapta.png" alt="Bot Avatar" />
<h1 className={dashboardStyles.h1}>SOLID</h1>
<h1 className={dashboardStyles.h1} styles="color: #43a0a7;">
DATA
</h1>
</div>
<div className={styles.dropdownContainer} ref={menuRef}>
<div className={dashboardStyles.dropdownContainer} ref={menuRef}>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className={styles.dropdownToggle}
className={dashboardStyles.dropdownToggle}
aria-expanded={isMenuOpen ? "true" : "false"}
aria-haspopup="true"
>
<svg
width="15"
@@ -108,24 +142,32 @@ const ProfileTab = () => {
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
{isMenuOpen && (
<div className={styles.dropdownMenu}>
<div className={dashboardStyles.dropdownMenu}>
<button
onClick={() => {
navigate("/dashboard");
setIsMenuOpen(false);
}}
className={styles.dropdownItem}
className={dashboardStyles.dropdownItem}
>
Dashboard
</button>
<button
onClick={() => {
navigate("/scan");
setIsMenuOpen(false);
}}
className={dashboardStyles.dropdownItem}
>
Scan
</button>
<button
onClick={() => {
handleLogout();
setIsMenuOpen(false);
}}
className={styles.dropdownItem}
className={dashboardStyles.dropdownItem}
>
Logout
</button>
@@ -134,43 +176,43 @@ const ProfileTab = () => {
</div>
</div>
<div className={styles.mainContent}>
<div className={styles.profileSection}>
<div className={styles.profileCard}>
<div className={styles.profileHeader}>
<div className={profileStyles.mainContent}>
<div className={profileStyles.profileSection}>
<div className={profileStyles.profileCard}>
<div className={profileStyles.profileHeader}>
<h2>Account</h2>
{!isEditing ? (
<button
onClick={() => setIsEditing(true)}
className={styles.editButton}
className={profileStyles.editButton}
>
Change Password
</button>
) : (
<div className={styles.actionButtons}>
<div className={profileStyles.actionButtons}>
<button
onClick={handleCancel}
className={styles.cancelButton}
className={profileStyles.cancelButton}
>
Cancel
</button>
<button onClick={handleSave} className={styles.saveButton}>
<button onClick={handleSave} className={profileStyles.saveButton}>
Save Changes
</button>
</div>
)}
</div>
<div className={styles.profileForm}>
<div className={profileStyles.profileForm}>
{!isEditing && (
<div className={styles.inputGroup}>
<label className={styles.inputLabel}>Username</label>
<div className={profileStyles.inputGroup}>
<label className={profileStyles.inputLabel}>Username</label>
<input
type="text"
name="username"
value={profile.username}
className={`${styles.input} ${
!isEditing ? styles.readOnly : ""
value={user.username}
className={`${profileStyles.input} ${
!isEditing ? profileStyles.readOnly : ""
}`}
disabled
/>
@@ -179,25 +221,25 @@ const ProfileTab = () => {
{isEditing && (
<>
<div className={styles.inputGroup}>
<label className={styles.inputLabel}>
<div className={profileStyles.inputGroup}>
<label className={profileStyles.inputLabel}>
Current Password
</label>
<input
type="password"
name="oldPassword"
onChange={handleChange}
className={styles.input}
className={profileStyles.input}
placeholder="Enter current password"
/>
</div>
<div className={styles.inputGroup}>
<label className={styles.inputLabel}>New Password</label>
<div className={profileStyles.inputGroup}>
<label className={profileStyles.inputLabel}>New Password</label>
<input
type="password"
name="newPassword"
onChange={handleChange}
className={styles.input}
className={profileStyles.input}
placeholder="Enter new password"
/>
</div>
@@ -208,8 +250,8 @@ const ProfileTab = () => {
</div>
</div>
<div className={styles.footer}>
© 2025 Kediri Technopark Dermalounge AI Admin
<div className={dashboardStyles.footer}>
© 2025 Kediri Technopark Dashboard SOLID DATA
</div>
</div>
);

View File

@@ -1,4 +1,4 @@
/* ProfileTab.module.css - Modern Design */
/* ProfileTab.module.css - Modern Design with Unified Header */
/* Modern Color Palette */
:root {
@@ -14,7 +14,7 @@
--neutral-800: #262626;
--neutral-900: #171717;
--white: #ffffff;
--success-green: #10b981;
--success-green: #43a0a7;
--warning-amber: #f59e0b;
--error-red: #ef4444;
--text-primary: #0f172a;
@@ -50,6 +50,7 @@ body {
flex-direction: column;
}
/* --- UNIFIED HEADER (sama dengan Dashboard.css) --- */
.dashboardHeader {
background-color: var(--white);
color: var(--text-primary);
@@ -58,7 +59,7 @@ body {
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-sm);
border-bottom: 3px solid #ef4444;
border-bottom: 3px solid #43a0a7; /* Warna dari Dashboard.css */
position: sticky;
top: 0;
z-index: 50;
@@ -80,10 +81,18 @@ body {
}
.dashboardHeader .h1 {
margin: 2px; /* Sama dengan Dashboard.css */
font-size: 1.5rem;
font-weight: 700;
color: #43a0a7; /* Warna dari Dashboard.css */
letter-spacing: -0.025em;
}
.data {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #ed4344;
color: #154666;
letter-spacing: -0.025em;
}
@@ -96,6 +105,12 @@ body {
flex-shrink: 0;
}
.userDisplayName {
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
}
.dropdownToggle {
background-color: var(--neutral-100);
color: var(--text-primary);
@@ -155,7 +170,7 @@ body {
margin-bottom: 0;
}
/* Main Content */
/* --- MAIN CONTENT --- */
.mainContent {
flex-grow: 1;
padding: 2rem 1.5rem;
@@ -205,7 +220,7 @@ body {
}
.editButton {
background-color: #ef4444;
background-color: #43a0a7; /* Diseragamkan dengan warna header */
color: var(--text-light);
border: none;
padding: 0.75rem 1.5rem;
@@ -218,7 +233,7 @@ body {
}
.editButton:hover {
background-color: var(--dark-blue);
background-color: #357734;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
@@ -328,7 +343,11 @@ body {
}
.licenseCard {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
background: linear-gradient(
135deg,
#43a0a7 0%,
#357734 100%
); /* Diseragamkan dengan warna header */
color: var(--text-light);
padding: 1.5rem;
border-radius: 1rem;
@@ -421,7 +440,7 @@ body {
border-top: 1px solid var(--border-light);
}
/* Responsive Design */
/* --- RESPONSIVE DESIGN --- */
@media (min-width: 768px) {
.dashboardHeader {
padding: 1rem 2rem;
@@ -436,6 +455,10 @@ body {
font-size: 1.75rem;
}
.userDisplayName {
font-size: 0.875rem;
}
.mainContent {
padding: 2.5rem 2rem;
gap: 2.5rem;

View File

@@ -18,7 +18,7 @@ const ShowImage = () => {
try {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/0f4420a8-8517-49ba-8ec5-75adde117813/ktp/img/${nik}`,
`https://bot.kediritechnopark.com/webhook/ed467164-05c0-4692-bb81-a8f13116bb1b/ktp/img/ikasapta/:nik/${nik}`,
{
method: "GET",
headers: {