ok
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
123
src/KTPPDF.js
Normal 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;
|
||||
@@ -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%;
|
||||
|
||||
1494
src/KTPScanner.js
1494
src/KTPScanner.js
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user