ok
This commit is contained in:
93
src/api.js
Normal file
93
src/api.js
Normal 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();
|
||||
};
|
||||
@@ -1,17 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchDocumentTypes } from '../api';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 }) => (
|
||||
<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>
|
||||
@@ -19,17 +10,40 @@ const StatCard = ({ title, value }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DashboardPage() {
|
||||
function DashboardPage() {
|
||||
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 (
|
||||
<div className="bg-gray-50 min-h-screen">
|
||||
<main className="p-4 sm:p-6 lg:p-8">
|
||||
{/* Statistik */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<StatCard title="HARI INI" value="10" />
|
||||
<StatCard title="BULAN INI" value="10" />
|
||||
<StatCard title="TOTAL KESELURUHAN" value="10" />
|
||||
<StatCard title="HARI INI" value="-" />
|
||||
<StatCard title="BULAN INI" value="-" />
|
||||
<StatCard title="TOTAL KESELURUHAN" value="-" />
|
||||
</div>
|
||||
|
||||
{/* Daftar Jenis Dokumen */}
|
||||
@@ -44,16 +58,32 @@ export default function DashboardPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
|
||||
{documentTypes.map((doc, index) => (
|
||||
<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="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.name}</p>
|
||||
<p className="text-sm text-gray-500">{doc.count} data tersedia</p>
|
||||
</div>
|
||||
{/* Arrow Icon */}
|
||||
</Link>
|
||||
))}
|
||||
{loading ? (
|
||||
<div className="p-4 text-gray-500">Memuat data...</div>
|
||||
) : error ? (
|
||||
<div className="p-4 text-red-500">{error}</div>
|
||||
) : documentTypes.length === 0 ? (
|
||||
<div className="p-4 text-gray-500">Belum ada jenis dokumen.</div>
|
||||
) : (
|
||||
documentTypes.map((doc, index) => (
|
||||
<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>
|
||||
</main>
|
||||
@@ -62,3 +92,5 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useParams, Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// 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>);
|
||||
@@ -10,6 +10,9 @@ const TrashIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5
|
||||
|
||||
|
||||
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 [filesToUpload, setFilesToUpload] = useState([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
@@ -50,12 +53,14 @@ export default function InputDataPage() {
|
||||
setUploadProgress(i + 1); // Update progress sebelum upload
|
||||
|
||||
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 {
|
||||
console.log(`Mengupload file ${i + 1}/${totalFiles}: ${file.name}`);
|
||||
const response = await fetch('https://api.kedaimaster.com/scan-documents', {
|
||||
method: 'POST',
|
||||
const response = await fetch("https://bot.kediritechnopark.com/webhook/solid-data/scan", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
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">
|
||||
@@ -7,6 +7,25 @@ const DocumentIcon = () => (
|
||||
);
|
||||
|
||||
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 (
|
||||
<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">
|
||||
@@ -17,12 +36,12 @@ export default function LoginPage() {
|
||||
</div>
|
||||
<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>
|
||||
<Link
|
||||
to="/select-organization"
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Masuk
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 = () => (
|
||||
<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>
|
||||
);
|
||||
|
||||
const organizations = [
|
||||
{ name: 'psi', id: 'BLWR-XDU-QRUV' },
|
||||
{ name: 'managemen', id: 'NFTJ-POX-ZYOB' },
|
||||
{ name: 'solid', id: 'TCKQ-ZNF-UFTW' },
|
||||
];
|
||||
function OrganizationPage() {
|
||||
const [organizations, setOrganizations] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 (
|
||||
<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">
|
||||
<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>
|
||||
<div className="space-y-4 text-left">
|
||||
{organizations.map((org) => (
|
||||
<Link
|
||||
key={org.id}
|
||||
to="/dashboard"
|
||||
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.name}</p>
|
||||
<p className="text-sm text-gray-400">ID: {org.id}</p>
|
||||
{loading ? (
|
||||
<div className="text-gray-500">Memuat organisasi...</div>
|
||||
) : error ? (
|
||||
<div className="text-red-500">{error}</div>
|
||||
) : organizations.length === 0 ? (
|
||||
<div className="text-gray-500">Tidak ada organisasi untuk akun ini.</div>
|
||||
) : (
|
||||
<div className="space-y-4 text-left">
|
||||
{organizations.map((org) => (
|
||||
<button
|
||||
key={org.organization_id}
|
||||
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>
|
||||
<ArrowIcon />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ArrowIcon />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizationPage;
|
||||
Reference in New Issue
Block a user