This commit is contained in:
jaya
2025-09-15 14:49:17 +07:00
parent e0c7ffca2c
commit 3bf563bb82
5 changed files with 257 additions and 61 deletions

93
src/api.js Normal file
View File

@@ -0,0 +1,93 @@
// src/api.js
// API utama untuk flow auth dan solid data (upload, fetch dokumen, organisasi)
// Membuat header auth
export const authHeaders = (extra = {}) => {
const token = localStorage.getItem("token");
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...extra,
};
return headers;
};
// Ambil daftar organisasi user
export const getOrganizationsFromBackend = async () => {
const token = localStorage.getItem("token");
if (!token) throw new Error("Token tidak ditemukan. Silakan login.");
const res = await fetch("https://bot.kediritechnopark.com/webhook/soliddata/get-organization", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
return await res.json();
};
// Pilih organisasi aktif
export const pickOrganization = async (organization_id, nama_organization) => {
const token = localStorage.getItem("token");
const chosen = { organization_id, nama_organization };
await fetch("https://bot.kediritechnopark.com/webhook/soliddata/pick-organization", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(chosen),
});
localStorage.setItem("selected_organization", JSON.stringify(chosen));
};
// Ambil daftar tipe dokumen (jenis dokumen)
export const fetchDocumentTypes = async (organizationId) => {
const token = localStorage.getItem("token");
const res = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/files",
{
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ organization_id: organizationId }),
}
);
return await res.json();
};
// Ambil entry/data per tipe dokumen
export const fetchEntries = async (dataTypeId) => {
const token = localStorage.getItem("token");
const res = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/files/entry",
{
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ data_type_id: dataTypeId }),
}
);
return await res.json();
};
// Upload dokumen (gambar/file)
export const uploadDocument = async (organizationId, dataTypeId, file) => {
const token = localStorage.getItem("token");
const formData = new FormData();
formData.append("organization_id", organizationId);
formData.append("data_type_id", dataTypeId);
formData.append("file", file);
const res = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/upload", {
method: "POST",
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: formData,
});
return await res.json();
};

View File

@@ -1,17 +1,8 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { fetchDocumentTypes } from '../api';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AddDocumentModal from '../components/AddDocumentModal'; // Kita akan buat ini import AddDocumentModal from '../components/AddDocumentModal'; // Kita akan buat ini
// Data dummy
const documentTypes = [
{ name: 'Akta Kelahiran', count: 0 },
{ name: 'Ijazah', count: 0 },
{ name: 'KK', count: 0 },
{ name: 'KTP', count: 6 },
{ name: 'Polinema', count: 3 },
{ name: 'Sampul Buku', count: 1 },
];
const StatCard = ({ title, value }) => ( const StatCard = ({ title, value }) => (
<div className="bg-gradient-to-br from-blue-500 to-indigo-600 text-white p-6 rounded-2xl shadow-lg"> <div className="bg-gradient-to-br from-blue-500 to-indigo-600 text-white p-6 rounded-2xl shadow-lg">
<p className="text-sm font-medium opacity-80">{title}</p> <p className="text-sm font-medium opacity-80">{title}</p>
@@ -19,17 +10,40 @@ const StatCard = ({ title, value }) => (
</div> </div>
); );
export default function DashboardPage() { function DashboardPage() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [documentTypes, setDocumentTypes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const org = localStorage.getItem('selected_organization');
if (!org) {
setError('Organisasi belum dipilih.');
setLoading(false);
return;
}
const { organization_id } = JSON.parse(org);
setLoading(true);
fetchDocumentTypes(organization_id)
.then((data) => {
setDocumentTypes(Array.isArray(data) ? data : data.data || []);
setError('');
})
.catch((err) => {
setError(err.message || 'Gagal memuat dokumen');
})
.finally(() => setLoading(false));
}, []);
return ( return (
<div className="bg-gray-50 min-h-screen"> <div className="bg-gray-50 min-h-screen">
<main className="p-4 sm:p-6 lg:p-8"> <main className="p-4 sm:p-6 lg:p-8">
{/* Statistik */} {/* Statistik */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<StatCard title="HARI INI" value="10" /> <StatCard title="HARI INI" value="-" />
<StatCard title="BULAN INI" value="10" /> <StatCard title="BULAN INI" value="-" />
<StatCard title="TOTAL KESELURUHAN" value="10" /> <StatCard title="TOTAL KESELURUHAN" value="-" />
</div> </div>
{/* Daftar Jenis Dokumen */} {/* Daftar Jenis Dokumen */}
@@ -44,16 +58,32 @@ export default function DashboardPage() {
</button> </button>
</div> </div>
<div className="bg-white rounded-2xl shadow-sm divide-y divide-gray-100"> <div className="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
{documentTypes.map((doc, index) => ( {loading ? (
<Link key={doc.name} to={`/input-data/${doc.name.toLowerCase().replace(' ', '-')}`} className="flex items-center p-4 hover:bg-gray-50 transition-colors"> <div className="p-4 text-gray-500">Memuat data...</div>
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">{index + 1}</div> ) : error ? (
<div className="ml-4 flex-grow"> <div className="p-4 text-red-500">{error}</div>
<p className="font-semibold text-gray-800">{doc.name}</p> ) : documentTypes.length === 0 ? (
<p className="text-sm text-gray-500">{doc.count} data tersedia</p> <div className="p-4 text-gray-500">Belum ada jenis dokumen.</div>
</div> ) : (
{/* Arrow Icon */} documentTypes.map((doc, index) => (
</Link> <Link
))} key={doc.data_type_id || doc.name}
to={`/input-data/${(doc.nama_tipe || doc.name)?.toLowerCase().replace(' ', '-')}`}
state={{
data_type_id: doc.data_type_id,
expectation: doc.expectation,
nama_tipe: doc.nama_tipe || doc.name
}}
className="flex items-center p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">{index + 1}</div>
<div className="ml-4 flex-grow">
<p className="font-semibold text-gray-800">{doc.nama_tipe || doc.name}</p>
<p className="text-sm text-gray-500">{doc.total_entries || doc.count || 0} data tersedia</p>
</div>
</Link>
))
)}
</div> </div>
</div> </div>
</main> </main>
@@ -61,4 +91,6 @@ export default function DashboardPage() {
<AddDocumentModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} /> <AddDocumentModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div> </div>
); );
} }
export default DashboardPage;

View File

@@ -1,5 +1,5 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link, useLocation } from 'react-router-dom';
// Ikon (tidak ada perubahan) // Ikon (tidak ada perubahan)
const BackIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>); const BackIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /></svg>);
@@ -10,6 +10,9 @@ const TrashIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5
export default function InputDataPage() { export default function InputDataPage() {
const location = useLocation();
const expectation = location.state?.expectation || {};
const data_type_id = location.state?.data_type_id || '';
const { docType } = useParams(); const { docType } = useParams();
const [filesToUpload, setFilesToUpload] = useState([]); const [filesToUpload, setFilesToUpload] = useState([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
@@ -37,7 +40,7 @@ export default function InputDataPage() {
// --- PERUBAHAN: Fungsi handleUpload diubah total --- // --- PERUBAHAN: Fungsi handleUpload diubah total ---
const handleUpload = async () => { const handleUpload = async () => {
if (filesToUpload.length === 0) return; if (filesToUpload.length === 0) return;
setIsUploading(true); setIsUploading(true);
setUploadProgress(0); setUploadProgress(0);
@@ -50,12 +53,14 @@ export default function InputDataPage() {
setUploadProgress(i + 1); // Update progress sebelum upload setUploadProgress(i + 1); // Update progress sebelum upload
const formData = new FormData(); const formData = new FormData();
formData.append('document', file); // Kirim satu file formData.append("image", file);
formData.append("expectation", JSON.stringify(expectation));
formData.append("data_type_id", data_type_id);
try { try {
console.log(`Mengupload file ${i + 1}/${totalFiles}: ${file.name}`); console.log(`Mengupload file ${i + 1}/${totalFiles}: ${file.name}`);
const response = await fetch('https://api.kedaimaster.com/scan-documents', { const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/scan", {
method: 'POST', method: "POST",
body: formData, body: formData,
}); });

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
const DocumentIcon = () => ( const DocumentIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"> <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
@@ -7,6 +7,25 @@ const DocumentIcon = () => (
); );
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate();
const handleLogin = () => {
const baseUrl = "https://kediritechnopark.com/";
const modal = "product";
const productId = 9;
// Ganti authorizedUri sesuai domain produksi Anda jika sudah deploy
const authorizedUri = window.location.origin + "/select-organization?token=";
const unauthorizedUri = `${baseUrl}?modal=${modal}&product_id=${productId}`;
const url =
`${baseUrl}?modal=${modal}&product_id=${productId}` +
`&authorized_uri=${encodeURIComponent(authorizedUri)}` +
`&unauthorized_uri=${encodeURIComponent(unauthorizedUri)}`;
window.location.href = url;
};
return ( return (
<div className="bg-gray-100 flex items-center justify-center h-screen"> <div className="bg-gray-100 flex items-center justify-center h-screen">
<div className="w-full max-w-sm p-8 bg-white rounded-2xl shadow-lg text-center"> <div className="w-full max-w-sm p-8 bg-white rounded-2xl shadow-lg text-center">
@@ -17,12 +36,12 @@ export default function LoginPage() {
</div> </div>
<h1 className="text-3xl font-bold text-gray-800">SOLID DATA</h1> <h1 className="text-3xl font-bold text-gray-800">SOLID DATA</h1>
<p className="text-gray-500 mt-2 mb-8">Kelola data dokumen Anda dengan mudah</p> <p className="text-gray-500 mt-2 mb-8">Kelola data dokumen Anda dengan mudah</p>
<Link <button
to="/select-organization" onClick={handleLogin}
className="block w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition-opacity duration-300" className="block w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition-opacity duration-300"
> >
Masuk Masuk
</Link> </button>
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,6 @@
import { Link } from 'react-router-dom'; import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { getOrganizationsFromBackend, pickOrganization } from '../api';
const OrgIcon = () => ( const OrgIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 4h5m-5 4h5" /></svg> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 4h5m-5 4h5" /></svg>
@@ -8,37 +10,82 @@ const ArrowIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>
); );
const organizations = [ function OrganizationPage() {
{ name: 'psi', id: 'BLWR-XDU-QRUV' }, const [organizations, setOrganizations] = useState([]);
{ name: 'managemen', id: 'NFTJ-POX-ZYOB' }, const [loading, setLoading] = useState(true);
{ name: 'solid', id: 'TCKQ-ZNF-UFTW' }, const [error, setError] = useState('');
]; const navigate = useNavigate();
const location = useLocation();
// Simpan token dari URL ke localStorage jika ada
useEffect(() => {
const params = new URLSearchParams(location.search);
const token = params.get("token");
if (token) {
localStorage.setItem("token", token);
// Hapus token dari URL agar tidak dikirim ulang saat reload
navigate("/select-organization", { replace: true });
}
// eslint-disable-next-line
}, []);
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
navigate("/login", { replace: true });
return;
}
setLoading(true);
getOrganizationsFromBackend()
.then((data) => {
setOrganizations(data);
setError('');
})
.catch((err) => {
setError(err.message || 'Gagal memuat organisasi');
})
.finally(() => setLoading(false));
}, [navigate]);
const handleSelect = async (org) => {
await pickOrganization(org.organization_id, org.nama_organization);
navigate('/dashboard');
};
export default function OrganizationPage() {
return ( return (
<div className="bg-gray-100 flex items-center justify-center min-h-screen py-10"> <div className="bg-gray-100 flex items-center justify-center min-h-screen py-10">
<div className="w-full max-w-md p-8 text-center"> <div className="w-full max-w-md p-8 text-center">
<h1 className="text-3xl font-bold text-gray-800">Pilih Organisasi</h1> <h1 className="text-3xl font-bold text-gray-800">Pilih Organisasi</h1>
<p className="text-gray-500 mt-2 mb-10">Silakan pilih organisasi yang ingin Anda kelola.</p> <p className="text-gray-500 mt-2 mb-10">Silakan pilih organisasi yang ingin Anda kelola.</p>
<div className="space-y-4 text-left"> {loading ? (
{organizations.map((org) => ( <div className="text-gray-500">Memuat organisasi...</div>
<Link ) : error ? (
key={org.id} <div className="text-red-500">{error}</div>
to="/dashboard" ) : organizations.length === 0 ? (
className="flex items-center justify-between w-full p-5 bg-white rounded-xl shadow-sm hover:shadow-md hover:bg-gray-50 transition-all duration-300 cursor-pointer" <div className="text-gray-500">Tidak ada organisasi untuk akun ini.</div>
> ) : (
<div className="flex items-center"> <div className="space-y-4 text-left">
<div className="p-2 bg-gray-100 rounded-lg mr-4"><OrgIcon /></div> {organizations.map((org) => (
<div> <button
<p className="font-bold text-gray-800">{org.name}</p> key={org.organization_id}
<p className="text-sm text-gray-400">ID: {org.id}</p> onClick={() => handleSelect(org)}
className="flex items-center justify-between w-full p-5 bg-white rounded-xl shadow-sm hover:shadow-md hover:bg-gray-50 transition-all duration-300 cursor-pointer"
>
<div className="flex items-center">
<div className="p-2 bg-gray-100 rounded-lg mr-4"><OrgIcon /></div>
<div>
<p className="font-bold text-gray-800">{org.nama_organization}</p>
<p className="text-sm text-gray-400">ID: {org.organization_id}</p>
</div>
</div> </div>
</div> <ArrowIcon />
<ArrowIcon /> </button>
</Link> ))}
))} </div>
</div> )}
</div> </div>
</div> </div>
); );
} }
export default OrganizationPage;