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

+ +
+ } + > +
+ {/* Informasi Perusahaan */} + + Informasi Perusahaan + + + } + placeholder="Masukkan Nama Perusahaan" + size="large" + /> + + + + + + + + + beforeUpload(file, 'path_kontrak')} + fileList={fileListKontrak} + onChange={handleChangeKontrak} + maxCount={1} + > + + + + + beforeUpload(file, 'path_hse_plant')} + fileList={fileListHsse} + onChange={handleChangeHsse} + maxCount={1} + > + + + + + + + + + + + {/* Informasi Penanggung Jawab */} + + Informasi Penanggung Jawab + + + } + placeholder="Masukkan Nama Penanggung Jawab" + size="large" + /> + + + } + placeholder="Masukkan No Handphone (+62)" + size="large" + /> + + + } + placeholder="Masukkan No Identitas" + size="large" + /> + + + {/* Akun Pengguna */} + + Akun Pengguna (digunakan sebagai user login SYPIU) + + + } + placeholder="Masukkan Email" + size="large" + /> + + + } + placeholder="Masukkan Password" + size="large" + /> + + + {/* Tombol */} + + + + + + +
+ + + ); +}; + +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 ( + <> + + + + signin + +
+
+ + + + + + + +
+ {/* {message} */} + setUserInput(e.target.value)} + /> + + + + + + + + + + + + ); +}; + +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 ( + <> + + + + + signin + +
+
+ + + + + + + + +
+ + + + + + {/* setUserInput(e.target.value)} + /> */} + + + + + + + + + + + + + ) +} + +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. + + + 404 Not Found + + + + + + + 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 ( +
+ Loading + + + + 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 ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > + {FormData && ( +
+ {props.permitDefault && ( + <> +
+
+ Aktif +
+
+
+ +
+
+ + {FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'} + +
+
+
+ + + )} + +
+ Device Name + * + +
+
+ Tipe Input + * +
+ +
+
+ {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 ( + + + + + , + ]} + > + {pdfUrl && ( +