diff --git a/src/pages/auth/Registration.jsx b/src/pages/auth/Registration.jsx
new file mode 100644
index 0000000..093c2b1
--- /dev/null
+++ b/src/pages/auth/Registration.jsx
@@ -0,0 +1,461 @@
+import React, { useState } from 'react';
+import {
+ Flex,
+ Input,
+ InputNumber,
+ Form,
+ Button,
+ Card,
+ Space,
+ Upload,
+ Divider,
+ Tooltip,
+ message,
+ Select,
+} from 'antd';
+import {
+ UploadOutlined,
+ UserOutlined,
+ IdcardOutlined,
+ PhoneOutlined,
+ LockOutlined,
+ InfoCircleOutlined,
+ MailOutlined,
+} from '@ant-design/icons';
+const { Item } = Form;
+const { Option } = Select;
+import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
+import { useNavigate } from 'react-router-dom';
+import { register, uploadFile, checkUsername } from '../../api/auth';
+import { NotifAlert } from '../../components/Global/ToastNotif';
+
+const Registration = () => {
+ const [form] = Form.useForm();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [fileListKontrak, setFileListKontrak] = useState([]);
+ const [fileListHsse, setFileListHsse] = useState([]);
+ const [fileListIcon, setFileListIcon] = useState([]);
+
+ // Daftar jenis vendor
+ const vendorTypes = [
+ { vendor_type: 1, vendor_type_name: 'One-Time' },
+ { vendor_type: 2, vendor_type_name: 'Rutin' },
+ ];
+
+ const onFinish = async (values) => {
+ setLoading(true);
+ try {
+ if (!fileListKontrak.length || !fileListHsse.length) {
+ message.error('Harap unggah Lampiran Kontrak Kerja dan HSSE Plan!');
+ setLoading(false);
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('path_kontrak', fileListKontrak[0].originFileObj);
+ formData.append('path_hse_plant', fileListHsse[0].originFileObj);
+ if (fileListIcon.length) {
+ formData.append('path_icon', fileListIcon[0].originFileObj);
+ }
+
+ const uploadResponse = await uploadFile(formData);
+
+ if (!uploadResponse.data?.pathKontrak && !uploadResponse.data?.pathHsePlant) {
+ message.error(uploadResponse.message || 'Gagal mengunggah file.');
+ setLoading(false);
+ return;
+ }
+
+ const params = new URLSearchParams({ username: values.username });
+ const usernameCheck = await checkUsername(params);
+ if (usernameCheck.data.data && usernameCheck.data.data.available === false) {
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: usernameCheck.data.message || 'Terjadi kesalahan, silakan coba lagi',
+ });
+ setLoading(false);
+ return;
+ }
+
+ const registerData = {
+ nama_perusahaan: values.namaPerusahaan,
+ no_kontak_wo: values.noKontakWo,
+ path_kontrak: uploadResponse.data.pathKontrak || '',
+ durasi: values.durasiPekerjaan,
+ nilai_csms: values.nilaiCsms.toString(),
+ vendor_type: values.jenisVendor, // Tambahkan jenis vendor ke registerData
+ path_hse_plant: uploadResponse.data.pathHsePlant || '',
+ nama_leader: values.penanggungJawab,
+ no_identitas: values.noIdentitas,
+ no_hp: values.noHandphone,
+ email_register: values.username,
+ password_register: values.password,
+ };
+
+ const response = await register(registerData);
+
+ if (response.data?.id_register) {
+ message.success('Data berhasil disimpan!');
+
+ try {
+ form.resetFields();
+ setFileListKontrak([]);
+ setFileListHsse([]);
+ setFileListIcon([]);
+
+ navigate('/registration-submitted');
+ } catch (postSuccessError) {
+ message.warning(
+ 'Registrasi berhasil, tetapi ada masalah setelahnya. Silakan ke halaman login secara manual.'
+ );
+ }
+ } else {
+ message.error(response.message || 'Pendaftaran gagal, silakan coba lagi.');
+ }
+ } catch (error) {
+ console.error('Error saat registrasi:', error);
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: error.message || 'Terjadi kesalahan, silakan coba lagi',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const onCancel = () => {
+ form.resetFields();
+ setFileListKontrak([]);
+ setFileListHsse([]);
+ setFileListIcon([]);
+ navigate('/signin');
+ };
+
+ const handleChangeKontrak = ({ fileList }) => {
+ setFileListKontrak(fileList);
+ };
+
+ const handleChangeHsse = ({ fileList }) => {
+ setFileListHsse(fileList);
+ };
+
+ const handleChangeIcon = ({ fileList }) => {
+ setFileListIcon(fileList);
+ };
+
+ const beforeUpload = (file, fieldname) => {
+ const isValidType = [
+ 'image/jpeg',
+ 'image/jpg',
+ 'image/png',
+ fieldname !== 'path_icon' ? 'application/pdf' : null,
+ ]
+ .filter(Boolean)
+ .includes(file.type);
+ const isNotEmpty = file.size > 0;
+ const isSizeValid = file.size / 1024 / 1024 < 10;
+
+ if (!isValidType) {
+ message.error(
+ `Hanya file ${
+ fieldname === 'path_icon' ? 'JPG/PNG' : 'PDF/JPG/PNG'
+ } yang diperbolehkan!`
+ );
+ return false;
+ }
+ if (!isNotEmpty) {
+ message.error('File tidak boleh kosong!');
+ return false;
+ }
+ if (!isSizeValid) {
+ message.error('Ukuran file maksimal 10MB!');
+ return false;
+ }
+ return true;
+ };
+
+ return (
+
+
+ Formulir Pendaftaran
+ }
+ onClick={() => navigate('/signin')}
+ >
+ Kembali
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default Registration;
diff --git a/src/pages/auth/SignIn.jsx b/src/pages/auth/SignIn.jsx
new file mode 100644
index 0000000..d2f0d33
--- /dev/null
+++ b/src/pages/auth/SignIn.jsx
@@ -0,0 +1,187 @@
+import { Flex, Input, Form, Button, Card, Space, Image } from 'antd';
+import React from 'react';
+import handleSignIn from '../../Utils/Auth/SignIn';
+import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
+import logo from 'assets/freepik/LOGOPIU.png';
+import { useNavigate } from 'react-router-dom';
+import { NotifAlert } from '../../components/Global/ToastNotif';
+import { decryptData } from '../../components/Global/Formatter';
+
+const SignIn = () => {
+ const [captchaSvg, setCaptchaSvg] = React.useState('');
+ const [userInput, setUserInput] = React.useState('');
+ const [message, setMessage] = React.useState('');
+ const [captchaText, setcaptchaText] = React.useState('');
+
+ const navigate = useNavigate();
+ // let url = `${import.meta.env.VITE_API_SERVER}/users`;
+
+ React.useEffect(() => {
+ fetchCaptcha();
+ }, []);
+
+ // Fetch the CAPTCHA SVG from the backend
+ const fetchCaptcha = async () => {
+ try {
+ // let url = `${import.meta.env.VITE_API_SERVER}/operation`
+ // const response = await fetch('http://localhost:9528/generate-captcha');
+ const response = await fetch(
+ `${import.meta.env.VITE_API_SERVER}/auth/generate-captcha`,
+ {
+ credentials: 'include', // Wajib untuk mengirim cookie
+ }
+ );
+
+ // Ambil header
+ const captchaToken = response.headers.get('X-Captcha-Token');
+
+ // console.log('Captcha Token:', decryptData(captchaToken));
+
+ setcaptchaText(decryptData(captchaToken));
+
+ const data = await response.text();
+ setCaptchaSvg(data);
+ } catch (error) {
+ console.error('Error fetching CAPTCHA:', error);
+ }
+ };
+
+ const handleOnSubmit = async (e) => {
+ // console.log('Received values of form: ', e);
+ // e.preventDefault();
+
+ try {
+ // const response = await fetch(`${import.meta.env.VITE_API_SERVER}/auth/verify-captcha`, {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // credentials: 'include', // WAJIB: Agar cookie CAPTCHA dikirim
+ // body: JSON.stringify({ captcha: userInput }),
+ // });
+
+ // const data = await response.json();
+ // console.log(data);
+
+ const data = {
+ success: captchaText === userInput ? true : false,
+ };
+
+ if (data.success) {
+ setMessage('CAPTCHA verified successfully!');
+ await handleSignIn(e);
+ } else {
+ setMessage('CAPTCHA verification failed. Try again.');
+ fetchCaptcha(); // Refresh CAPTCHA on failure
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: data.message || 'CAPTCHA verification failed. Try again.',
+ });
+ }
+ } catch (error) {
+ console.error('Error verifying CAPTCHA:', error);
+ setMessage('An error occurred. Please try again.');
+ }
+
+ setUserInput(''); // Clear the input field
+ };
+
+ const moveToRegistration = (e) => {
+ // e.preventDefault();
+ // navigate("/signup")
+ navigate('/registration');
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* {message} */}
+ setUserInput(e.target.value)}
+ />
+
+
+
+
+ Sign In
+
+
+
+
+ moveToRegistration()}
+ >
+ Registration
+
+
+
+ >
+ );
+};
+
+export default SignIn;
diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx
new file mode 100644
index 0000000..8bd1d14
--- /dev/null
+++ b/src/pages/auth/Signup.jsx
@@ -0,0 +1,158 @@
+import { Flex, Input, Form, Button, Card, Space, Image } from 'antd'
+import React from 'react'
+// import handleSignIn from '../../Utils/Auth/SignIn';
+import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
+import {useNavigate} from "react-router-dom";
+
+const SignUp = () => {
+
+ const [captchaSvg, setCaptchaSvg] = React.useState('');
+ const [userInput, setUserInput] = React.useState('');
+ const [message, setMessage] = React.useState('');
+
+ const navigate = useNavigate();
+ // let url = `${import.meta.env.VITE_API_SERVER}/users`;
+
+ React.useEffect(() => {
+ fetchCaptcha();
+ }, []);
+
+ // Fetch the CAPTCHA SVG from the backend
+ const fetchCaptcha = async () => {
+ try {
+ const response = await fetch('http://localhost:9528/generate-captcha');
+ const data = await response.text();
+ setCaptchaSvg(data);
+ } catch (error) {
+ console.error('Error fetching CAPTCHA:', error);
+ }
+ };
+
+ const handleOnSubmt = async (e) => {
+ // console.log('Received values of form: ', e);
+ // await handleSignIn(e);
+ e.preventDefault();
+
+ try {
+ const response = await fetch('http://localhost:5000/verify-captcha', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userInput }),
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ setMessage('CAPTCHA verified successfully!');
+ } else {
+ setMessage('CAPTCHA verification failed. Try again.');
+ fetchCaptcha(); // Refresh CAPTCHA on failure
+ }
+ } catch (error) {
+ console.error('Error verifying CAPTCHA:', error);
+ setMessage('An error occurred. Please try again.');
+ }
+
+ setUserInput(''); // Clear the input field
+ }
+
+ const moveToSignin = (e) => {
+ // e.preventDefault();
+ navigate("/signin")
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* setUserInput(e.target.value)}
+ /> */}
+
+
+
+
+ Registrasi
+
+
+
+
+
+ moveToSignin()}
+ >
+ Sign In
+
+
+
+ >
+ )
+}
+
+export default SignUp
diff --git a/src/pages/blank/Blank.jsx b/src/pages/blank/Blank.jsx
new file mode 100644
index 0000000..54f06cf
--- /dev/null
+++ b/src/pages/blank/Blank.jsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Blank = () => {
+ return (
+
+ Hi From BlankPage
+
+ )
+}
+
+export default Blank
diff --git a/src/pages/blank/NotFound.jsx b/src/pages/blank/NotFound.jsx
new file mode 100644
index 0000000..3cda2b0
--- /dev/null
+++ b/src/pages/blank/NotFound.jsx
@@ -0,0 +1,85 @@
+import { Button, Typography } from 'antd';
+import { Link } from 'react-router-dom';
+import ImgRobot from '../../assets/freepik/404.png';
+
+const { Title, Paragraph, Text } = Typography;
+
+const NotFound = () => {
+ return (
+
+
+ Oops... You seem lost.
+
+
+
+ We couldn't find the page you were looking for. Let us take you back to the main
+ page.
+
+
+
+
+
+
+ Go back
+
+
+
+
+ Illustration by{' '}
+
+ Freepik
+
+
+
+ );
+};
+
+export default NotFound;
diff --git a/src/pages/blank/Waiting.jsx b/src/pages/blank/Waiting.jsx
new file mode 100644
index 0000000..d327574
--- /dev/null
+++ b/src/pages/blank/Waiting.jsx
@@ -0,0 +1,58 @@
+import { Button, Spin, Typography } from 'antd';
+import { Link } from 'react-router-dom';
+import ImgPIU from '../../assets/freepik/LOGOPIU.png';
+
+const { Title, Paragraph, Text } = Typography;
+
+const Waiting = () => {
+ return (
+
+
+
+
+
+ Please wait...
+
+
+
+ We are loading your content. This won’t take long.
+
+
+ );
+};
+
+export default Waiting;
diff --git a/src/pages/home/Home.jsx b/src/pages/home/Home.jsx
new file mode 100644
index 0000000..38bd5ab
--- /dev/null
+++ b/src/pages/home/Home.jsx
@@ -0,0 +1,52 @@
+import { useEffect, useState } from 'react';
+import { Card, Typography, Flex } from 'antd';
+import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
+
+const { Text } = Typography;
+
+const Home = () => {
+ const { setBreadcrumbItems } = useBreadcrumb();
+ const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsMobile(window.innerWidth <= 768);
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ setBreadcrumbItems([
+ {
+ title: (
+
+ • Dashboard
+
+ ),
+ },
+ {
+ title: (
+
+ Home
+
+ ),
+ },
+ ]);
+ } else {
+ navigate('/signin');
+ }
+ }, []);
+
+ return (
+
+
+ Wellcome Call Of Duty App
+
+
+ );
+};
+
+export default Home;
diff --git a/src/pages/home/component/StatCard.jsx b/src/pages/home/component/StatCard.jsx
new file mode 100644
index 0000000..bfbf068
--- /dev/null
+++ b/src/pages/home/component/StatCard.jsx
@@ -0,0 +1,71 @@
+import { Card } from 'antd';
+
+const StatCard = ({ title, data }) => {
+ const totalCount = data.reduce((sum, item) => sum + item.count, 0);
+
+ return (
+
+
+ {data.map((item, idx) => (
+
+
+ {item.label}
+
+
+ {item.percent}%
+
+
+ {item.count.toLocaleString()}
+
+
+ ))}
+
+
+
+
+ {data.map((item, idx) => (
+
+ ))}
+
+
+
+
+ Total: {totalCount.toLocaleString()}
+
+
+ );
+};
+
+export default StatCard;
diff --git a/src/pages/master/device/IndexDevice.jsx b/src/pages/master/device/IndexDevice.jsx
new file mode 100644
index 0000000..0ea060c
--- /dev/null
+++ b/src/pages/master/device/IndexDevice.jsx
@@ -0,0 +1,86 @@
+import React, { memo, useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import ListDevice from './component/ListDevice';
+import DetailDevice from './component/DetailDevice';
+import GeneratePdf from './component/GeneratePdf';
+import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
+import { Typography } from 'antd';
+
+const { Text } = Typography;
+
+const IndexDevice = memo(function IndexDevice() {
+ const navigate = useNavigate();
+ const { setBreadcrumbItems } = useBreadcrumb();
+
+ const [actionMode, setActionMode] = useState('list');
+ const [selectedData, setSelectedData] = useState(null);
+ const [readOnly, setReadOnly] = useState(false);
+ const [showModal, setShowmodal] = useState(false);
+
+ const setMode = (param) => {
+ setShowmodal(true);
+ switch (param) {
+ case 'add':
+ setReadOnly(false);
+ break;
+
+ case 'edit':
+ setReadOnly(false);
+ break;
+
+ case 'preview':
+ setReadOnly(true);
+ break;
+
+ default:
+ setShowmodal(false);
+ break;
+ }
+ setActionMode(param);
+ };
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ setBreadcrumbItems([
+ { title: • Master },
+ { title: Device }
+ ]);
+ } else {
+ navigate('/signin');
+ }
+ }, []);
+
+ return (
+
+
+
+ {actionMode == 'generatepdf' && (
+
+ )}
+
+ );
+});
+
+export default IndexDevice;
diff --git a/src/pages/master/device/component/DetailDevice.jsx b/src/pages/master/device/component/DetailDevice.jsx
new file mode 100644
index 0000000..77ab15f
--- /dev/null
+++ b/src/pages/master/device/component/DetailDevice.jsx
@@ -0,0 +1,308 @@
+import React, { useEffect, useState } from 'react';
+import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Radio } from 'antd';
+import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
+import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd';
+import { Checkbox } from 'antd';
+const CheckboxGroup = Checkbox.Group;
+
+const { Text } = Typography;
+
+const DetailDevice = (props) => {
+ const [confirmLoading, setConfirmLoading] = useState(false);
+
+ const defaultData = {
+ id_apd: '',
+ nama_apd: '',
+ type_input: 1,
+ is_active: true,
+ jenis_permit_default: [],
+ };
+
+ const [FormData, setFormData] = useState(defaultData);
+
+ const [jenisPermit, setJenisPermit] = useState([]);
+ const [checkedList, setCheckedList] = useState([]);
+
+ const onChange = (list) => {
+ setCheckedList(list);
+ };
+
+ const onChangeRadio = (e) => {
+ setFormData({
+ ...FormData,
+ type_input: e.target.value,
+ });
+ };
+
+ const getDataJenisPermit = async () => {
+ setCheckedList([]);
+ const result = await getJenisPermit();
+ const data = result.data ?? [];
+ const names = data.map((item) => ({
+ value: item.id_jenis_permit,
+ label: item.nama_jenis_permit,
+ }));
+ setJenisPermit(names);
+ };
+
+ const handleCancel = () => {
+ props.setSelectedData(null);
+ props.setActionMode('list');
+ };
+
+ const handleSave = async () => {
+ setConfirmLoading(true);
+
+ if (!FormData.nama_apd) {
+ NotifOk({
+ icon: 'warning',
+ title: 'Peringatan',
+ message: 'Kolom Nama APD Tidak Boleh Kosong',
+ });
+
+ setConfirmLoading(false);
+ return;
+ }
+
+ if (props.permitDefault && checkedList.length === 0) {
+ NotifOk({
+ icon: 'warning',
+ title: 'Peringatan',
+ message: 'Kolom Jenis Permit Tidak Boleh Kosong',
+ });
+
+ setConfirmLoading(false);
+ return;
+ }
+
+ const payload = {
+ nama_apd: FormData.nama_apd,
+ is_active: FormData.is_active,
+ type_input: FormData.type_input,
+ jenis_permit_default: checkedList,
+ };
+
+ if (props.permitDefault) {
+ try {
+ let response;
+ if (!FormData.id_apd) {
+ response = await createApd(payload);
+ } else {
+ response = await updateApd(FormData.id_apd, payload);
+ }
+
+ if (response.statusCode === 200) {
+ NotifOk({
+ icon: 'success',
+ title: 'Berhasil',
+ message: `Data "${response.data.nama_apd}" berhasil ${
+ FormData.id_apd ? 'diubah' : 'ditambahkan'
+ }.`,
+ });
+
+ props.setActionMode('list');
+ } else {
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: response.message || 'Terjadi kesalahan saat menyimpan data.',
+ });
+ }
+ } catch (error) {
+ NotifAlert({
+ icon: 'error',
+ title: 'Error',
+ message: 'Terjadi kesalahan pada server. Coba lagi nanti.',
+ });
+ }
+ } else {
+ props.setData((prevData) => [
+ ...prevData,
+ { nama_apd: payload.nama_apd, type_input: payload.type_input },
+ ]);
+
+ props.setActionMode('list');
+ }
+
+ setConfirmLoading(false);
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData({
+ ...FormData,
+ [name]: value,
+ });
+ };
+
+ const handleStatusToggle = (event) => {
+ const isChecked = event;
+ setFormData({
+ ...FormData,
+ is_active: isChecked ? true : false,
+ });
+ };
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ getDataJenisPermit();
+ if (props.selectedData != null) {
+ setFormData(props.selectedData);
+ setCheckedList(props.selectedData.jenis_permit_default_arr);
+ } else {
+ setFormData(defaultData);
+ }
+ } else {
+ navigate('/signin');
+ }
+ }, [props.showModal]);
+
+ return (
+
+
+ Batal
+
+
+ {!props.readOnly && (
+
+ Simpan
+
+ )}
+
+ ,
+ ]}
+ >
+ {FormData && (
+
+ {props.permitDefault && (
+ <>
+
+
+ Aktif
+
+
+
+
+
+
+
+ {FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'}
+
+
+
+
+
+ >
+ )}
+
+ Device ID
+
+
+
+ Device Name
+ *
+
+
+
+ {props.permitDefault && (
+
+ Jenis Permit
+ *
+
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default DetailDevice;
\ No newline at end of file
diff --git a/src/pages/master/device/component/GeneratePdf.jsx b/src/pages/master/device/component/GeneratePdf.jsx
new file mode 100644
index 0000000..02ea2a0
--- /dev/null
+++ b/src/pages/master/device/component/GeneratePdf.jsx
@@ -0,0 +1,127 @@
+import React, {useEffect, useState } from 'react';
+import { Modal, Button, ConfigProvider } from 'antd';
+import { jsPDF } from 'jspdf';
+import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
+import { kopReportPdf } from '../../../../components/Global/KopReport';
+
+const GeneratePdf = (props) => {
+ const [pdfUrl, setPdfUrl] = useState(null);
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ generatePdf();
+ } else {
+ navigate('/signin');
+ }
+ }, []);
+
+ const handleCancel = () => {
+ props.setSelectedData(null);
+ props.setActionMode('list');
+ };
+
+ const generatePdf = async () => {
+ const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
+
+ const doc = new jsPDF({
+ orientation: "portrait",
+ unit: "mm",
+ format: "a4"
+ });
+
+ const width = 45;
+ const height = 23;
+ const marginTop = 6;
+ const marginLeft = 10;
+ doc.addImage(images, 'PNG', marginLeft, marginTop, width, height);
+
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(25);
+ doc.setTextColor(35, 165, 90);
+ doc.setTextColor('#00b0f0');
+ doc.text(title, 100, 25);
+ doc.setTextColor('#000000');
+ doc.setFontSize(11);
+ doc.setFont('helvetica', 'normal');
+
+ doc.setLineWidth(0.2);
+ doc.line(10, 32, 200, 32);
+ doc.setLineWidth(0.6);
+ doc.line(10, 32.8, 200, 32.8);
+
+ doc.text("Tanggal Pengajuan", 10, 42);
+ doc.text(":", 59, 42);
+
+ doc.text("Deskripsi Pekerjaan", 10, 48);
+ doc.text(":", 59, 48);
+
+ doc.text("No. Permit", 10, 54);
+ doc.text(":", 59, 54);
+ doc.text("Spesifik Lokasi", 120, 54);
+ doc.text(":", 160, 54);
+
+ doc.text("No. Order", 10, 60);
+ doc.text(":", 59, 60);
+ doc.text("Jum. Personil Terlihat", 120, 60);
+ doc.text(":", 160, 60);
+
+ doc.text("Peralatan yang digunakan", 10, 66);
+ doc.text(":", 59, 66);
+
+ doc.text("Jenis APD yang digunakan", 10, 72);
+ doc.text(":", 59, 72);
+
+ const blob = doc.output('blob');
+ const url = URL.createObjectURL(blob);
+
+ setPdfUrl(url);
+
+ setTimeout(() => {
+ URL.revokeObjectURL(url);
+ }, 1000);
+ };
+
+ return (
+
+
+ Batal
+
+ >,
+ ]}
+ >
+ {pdfUrl && (
+
+ )}
+
+ );
+};
+
+export default GeneratePdf;
diff --git a/src/pages/master/device/component/ListDevice.jsx b/src/pages/master/device/component/ListDevice.jsx
new file mode 100644
index 0000000..e18f30d
--- /dev/null
+++ b/src/pages/master/device/component/ListDevice.jsx
@@ -0,0 +1,408 @@
+import React, { memo, useState, useEffect } from 'react';
+import {
+ Space,
+ Tag,
+ ConfigProvider,
+ Button, Row, Col, Card, Divider, Form, Input, Dropdown } from 'antd';
+import {
+ PlusOutlined,
+ FilterOutlined,
+ EditOutlined,
+ DeleteOutlined,
+ EyeOutlined,
+ SearchOutlined,
+ FilePdfOutlined,
+ FileExcelOutlined,
+ EllipsisOutlined
+} from '@ant-design/icons';
+import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
+import { useNavigate } from 'react-router-dom';
+import { deleteApd, getAllApd } from '../../../../api/master-apd';
+import TableList from '../../../../components/Global/TableList';
+import { getFilterData } from '../../../../components/Global/DataFilter';
+import ExcelJS from 'exceljs';
+import { saveAs } from 'file-saver';
+import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
+
+const columns = (items, handleClickMenu) => [
+ {
+ title: 'ID',
+ dataIndex: 'id_apd',
+ key: 'id_apd',
+ width: '5%',
+ hidden: 'true',
+ },
+ {
+ title: 'Device Name',
+ dataIndex: 'nama_apd',
+ key: 'nama_apd',
+ width: '55%',
+ },
+ {
+ title: 'Aktif',
+ dataIndex: 'is_active',
+ key: 'is_active',
+ width: '10%',
+ align: 'center',
+ render: (_, { is_active }) => (
+ <>
+ {is_active === true ? (
+
+ Aktif
+
+ ) : (
+
+ Non-Aktif
+
+ )}
+ >
+ ),
+ },
+ {
+ title: 'Aksi',
+ key: 'aksi',
+ align: 'center',
+ width: '5%',
+ render: (_, record) => (
+ handleClickMenu(key, record)
+ }}
+ trigger={['click']}
+ placement="bottomRight"
+ >
+ }
+ />
+
+ ),
+ },
+];
+
+const ListDevice = memo(function ListDevice(props) {
+ const [showFilter, setShowFilter] = useState(false);
+ const [trigerFilter, setTrigerFilter] = useState(false);
+
+ const defaultFilter = { nama_apd: '' };
+ const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ if (props.actionMode == 'list') {
+ setFormDataFilter(defaultFilter);
+ doFilter();
+ }
+ } else {
+ navigate('/signin');
+ }
+ }, [props.actionMode]);
+
+ const toggleFilter = () => {
+ setFormDataFilter(defaultFilter);
+ setShowFilter((prev) => !prev);
+ };
+
+ const doFilter = () => {
+ setTrigerFilter((prev) => !prev);
+ };
+
+ const handleInputChangeFilter = (e) => {
+ const { name, value } = e.target;
+ setFormDataFilter((prevData) => ({
+ ...prevData,
+ [name]: value,
+ }));
+ };
+
+ const showPreviewModal = (param) => {
+ props.setSelectedData(param);
+ props.setActionMode('preview');
+ };
+
+ const showEditModal = (param = null) => {
+ props.setSelectedData(param);
+ props.setActionMode('edit');
+ };
+
+ const showAddModal = (param = null) => {
+ props.setSelectedData(param);
+ props.setActionMode('add');
+ };
+
+ const showDeleteDialog = (param) => {
+ NotifConfirmDialog({
+ icon: 'question',
+ title: 'Konfirmasi',
+ message: 'Apakah anda yakin hapus data "' + param.nama_apd + '" ?',
+ onConfirm: () => handleDelete(param.id_apd),
+ onCancel: () => props.setSelectedData(null),
+ });
+ };
+
+ const handleDelete = async (id_apd) => {
+ const response = await deleteApd(id_apd);
+
+ if (response.statusCode == 200) {
+ NotifAlert({
+ icon: 'success',
+ title: 'Berhasil',
+ message: 'Data Data "' + response.data[0].nama_apd + '" berhasil dihapus.',
+ });
+ doFilter();
+ } else {
+ NotifOk({
+ icon: 'error',
+ title: 'Gagal',
+ message: 'Gagal Menghapus Data Data "' + response.data[0].nama_apd + '"',
+ });
+ }
+ };
+
+ const generatePdf = () => {
+ props.setActionMode('generatepdf');
+ };
+
+ const exportExcel = async()=>{
+ const data = getFilterData();
+
+ const workbook = new ExcelJS.Workbook();
+ const sheet = workbook.addWorksheet('Data APD');
+ let rowCursor = 1;
+ // Kop Logo PIE
+ if (logoPiEnergi) {
+ const response = await fetch(logoPiEnergi);
+ const blob = await response.blob();
+ const buffer = await blob.arrayBuffer();
+
+ const imageId = workbook.addImage({
+ buffer,
+ extension: 'png',
+ });
+
+ // Tempatkan gambar di pojok atas
+ sheet.addImage(imageId, {
+ tl: { col: 0.2, row: 0.8 },
+ ext: { width: 163, height: 80 },
+ });
+
+ sheet.getRow(5).height = 15; // biar ada jarak ke tabel
+ rowCursor = 3;
+ }
+
+ // Tambah Judul
+ const titleCell = sheet.getCell(`C${rowCursor}`);
+ titleCell.value = 'Data APD';
+ titleCell.font = { size: 20, bold: true, color: { argb: 'FF00AEEF' } };
+ titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
+ sheet.mergeCells(`C${rowCursor}:F${rowCursor}`);
+
+ // Header tabel
+ const headers = [
+ 'ID APD',
+ 'Nama APD',
+ 'Deskripsi',
+ 'Jenis Permit Default',
+ 'Aktif',
+ 'Dibuat',
+ 'Diubah',
+ ];
+ sheet.addRow(headers);
+ const headerRow = sheet.getRow(6);
+ headerRow.font = { bold: true, size: 12 };
+ headerRow.eachCell((cell) => {
+ cell.alignment = {
+ horizontal: 'center', // rata tengah kiri-kanan
+ vertical: 'middle', // rata tengah atas-bawah
+ };
+ });
+
+ // Tambahkan data
+ data.forEach((item) => {
+ sheet.addRow([
+ item.id_apd,
+ item.nama_apd,
+ item.deskripsi_apd ?? '',
+ item.jenis_permit_default ?? '',
+ item.is_active ? 'Ya' : 'Tidak',
+ new Date(item.created_at).toLocaleString(),
+ new Date(item.updated_at).toLocaleString(),
+ ]);
+ });
+
+ // Auto width
+ sheet.columns.forEach((col) => {
+ let maxLength = 10;
+ col.eachCell({ includeEmpty: true }, (cell) => {
+ const len = cell.value?.toString().length || 0;
+ if (len > maxLength) maxLength = len;
+ });
+ col.width = maxLength + 2;
+ });
+
+ // Export
+ const buffer = await workbook.xlsx.writeBuffer();
+ saveAs(new Blob([buffer]), 'Data_APD.xlsx');
+ };
+
+ const handleClickMenu = (key, record) => {
+ switch (key) {
+ case 'preview':
+ showPreviewModal(record);
+ break;
+
+ case 'edit':
+ showEditModal(record);
+ break;
+
+ case 'delete':
+ showDeleteDialog(record);
+ break;
+
+ default:
+ break;
+ }
+ };
+
+ const menu = [
+ {
+ key: 'preview',
+ label: Preview ,
+ icon: ,
+ },
+ {
+ key: 'edit',
+ label: Edit ,
+ icon: ,
+ },
+ {
+ key: 'delete',
+ label: Delete ,
+ icon: ,
+ danger: true,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ }>
+ Filter
+
+
+
+
+
+
+ }
+ onClick={() => showAddModal()}
+ >
+ Tambah Data
+
+
+
+
+
+
+
+ {/* filter */}
+
+ {showFilter && (
+ <>
+
+
+
+
+
+ }
+ onClick={doFilter}
+ style={{ height: '40px' }}
+ >
+ Cari
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+});
+
+export default ListDevice;
\ No newline at end of file