Add pages
This commit is contained in:
461
src/pages/auth/Registration.jsx
Normal file
461
src/pages/auth/Registration.jsx
Normal file
@@ -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 (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
backgroundImage: `url(${sypiu_ggcp})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
|
||||
padding: '24px',
|
||||
}}
|
||||
title={
|
||||
<Flex align="center" justify="space-between">
|
||||
<h2 style={{ margin: 0, color: '#1a3c34' }}>Formulir Pendaftaran</h2>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => navigate('/signin')}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
layout="horizontal"
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
style={{ maxWidth: 800 }}
|
||||
>
|
||||
{/* Informasi Perusahaan */}
|
||||
<Divider
|
||||
orientation="left"
|
||||
orientationMargin={0}
|
||||
style={{
|
||||
color: '#23A55A',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 0,
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
Informasi Perusahaan
|
||||
</Divider>
|
||||
<Item
|
||||
label="Nama Perusahaan"
|
||||
name="namaPerusahaan"
|
||||
rules={[{ required: true, message: 'Masukkan Nama Perusahaan!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="Masukkan Nama Perusahaan"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="Durasi Pekerjaan (Hari)"
|
||||
name="durasiPekerjaan"
|
||||
rules={[{ required: true, message: 'Masukkan Durasi Pekerjaan!' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Masukkan Durasi Pekerjaan"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="No Kontrak Kerja / Agreement"
|
||||
name="noKontakWo"
|
||||
rules={[
|
||||
{ required: true, message: 'Masukkan No Kontrak Kerja / Agreement!' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
placeholder="Masukkan No Kontrak Kerja / Agreement"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="Lampiran Kontrak Kerja"
|
||||
name="lampiranKontrak"
|
||||
rules={[{ required: true, message: 'Unggah Lampiran Kontrak Kerja!' }]}
|
||||
>
|
||||
<Upload
|
||||
beforeUpload={(file) => beforeUpload(file, 'path_kontrak')}
|
||||
fileList={fileListKontrak}
|
||||
onChange={handleChangeKontrak}
|
||||
maxCount={1}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} size="large">
|
||||
Unggah PDF/JPG
|
||||
</Button>
|
||||
</Upload>
|
||||
</Item>
|
||||
<Item
|
||||
label="HSSE Plan"
|
||||
name="hssePlan"
|
||||
rules={[{ required: true, message: 'Unggah HSSE Plan!' }]}
|
||||
>
|
||||
<Upload
|
||||
beforeUpload={(file) => beforeUpload(file, 'path_hse_plant')}
|
||||
fileList={fileListHsse}
|
||||
onChange={handleChangeHsse}
|
||||
maxCount={1}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} size="large">
|
||||
Unggah PDF/JPG
|
||||
</Button>
|
||||
</Upload>
|
||||
</Item>
|
||||
<Item
|
||||
label="Nilai CSMS"
|
||||
name="nilaiCsms"
|
||||
rules={[{ required: true, message: 'Masukkan Nilai CSMS!' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Masukkan Nilai CSMS"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="Jenis Vendor"
|
||||
name="jenisVendor"
|
||||
rules={[{ required: true, message: 'Pilih Jenis Vendor!' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Pilih Jenis Vendor"
|
||||
size="large"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{vendorTypes.map((vendor) => (
|
||||
<Option key={vendor.vendor_type} value={vendor.vendor_type}>
|
||||
{vendor.vendor_type_name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Item>
|
||||
|
||||
{/* Informasi Penanggung Jawab */}
|
||||
<Divider
|
||||
orientation="left"
|
||||
orientationMargin={0}
|
||||
style={{
|
||||
color: '#23A55A',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 0,
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
Informasi Penanggung Jawab
|
||||
</Divider>
|
||||
<Item
|
||||
label="Nama Penanggung Jawab"
|
||||
name="penanggungJawab"
|
||||
rules={[{ required: true, message: 'Masukkan Nama Penanggung Jawab!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="Masukkan Nama Penanggung Jawab"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="No Handphone"
|
||||
name="noHandphone"
|
||||
rules={[
|
||||
{ required: true, message: 'Masukkan No Handphone!' },
|
||||
{
|
||||
pattern: /^(\+62|0)[0-9]{9,12}$/,
|
||||
message:
|
||||
'Format nomor telepon tidak valid! (Contoh: +62.... atau 0....)',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<PhoneOutlined />}
|
||||
placeholder="Masukkan No Handphone (+62)"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="No Identitas"
|
||||
name="noIdentitas"
|
||||
rules={[{ required: true, message: 'Masukkan No Identitas!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<IdcardOutlined />}
|
||||
placeholder="Masukkan No Identitas"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
|
||||
{/* Akun Pengguna */}
|
||||
<Divider
|
||||
orientation="left"
|
||||
orientationMargin={0}
|
||||
style={{
|
||||
color: '#23A55A',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 0,
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
Akun Pengguna (digunakan sebagai user login SYPIU)
|
||||
</Divider>
|
||||
<Item
|
||||
label="Email"
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: 'Masukkan Email!' },
|
||||
{ type: 'email', message: 'Format email tidak valid!' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="Masukkan Email"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: 'Masukkan Password!' },
|
||||
{ min: 6, message: 'Password minimal 6 karakter!' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="Masukkan Password"
|
||||
size="large"
|
||||
/>
|
||||
</Item>
|
||||
|
||||
{/* Tombol */}
|
||||
<Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Space style={{ marginTop: '24px', width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
loading={loading}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
width: 120,
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
<Button onClick={onCancel} size="large" style={{ width: 120 }}>
|
||||
Batal
|
||||
</Button>
|
||||
</Space>
|
||||
</Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Registration;
|
||||
187
src/pages/auth/SignIn.jsx
Normal file
187
src/pages/auth/SignIn.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
// vertical
|
||||
style={{
|
||||
height: '100vh',
|
||||
// marginTop: '10vh',
|
||||
// backgroundImage: `url('https://via.placeholder.com/300')`,
|
||||
backgroundImage: `url(${sypiu_ggcp})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="center">
|
||||
<Image
|
||||
// src="/src/assets/freepik/LOGOPIU.png"
|
||||
src={logo}
|
||||
height={150}
|
||||
width={220}
|
||||
preview={false}
|
||||
alt="signin"
|
||||
/>
|
||||
</Flex>
|
||||
<br />
|
||||
<Form onFinish={handleOnSubmit} layout="vertical" style={{ width: '250px' }}>
|
||||
<Form.Item
|
||||
label="Username"
|
||||
name="username"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// type: "email",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="username" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input your password!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="password" size="large" />
|
||||
</Form.Item>
|
||||
<div
|
||||
style={{ marginLeft: 45 }}
|
||||
dangerouslySetInnerHTML={{ __html: captchaSvg }}
|
||||
/>
|
||||
{/* {message} */}
|
||||
<Input
|
||||
style={{ marginTop: 10, marginBottom: 15 }}
|
||||
type="text"
|
||||
placeholder="Enter CAPTCHA text"
|
||||
size="large"
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
/>
|
||||
|
||||
<Form.Item>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Button type="primary" htmlType="submit" style={{ width: '100%' }}>
|
||||
Sign In
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => moveToRegistration()}
|
||||
>
|
||||
Registration
|
||||
</Button>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
158
src/pages/auth/Signup.jsx
Normal file
158
src/pages/auth/Signup.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
|
||||
<Flex
|
||||
align='center'
|
||||
justify='center'
|
||||
// vertical
|
||||
style={{
|
||||
height: '100vh',
|
||||
// marginTop: '10vh',
|
||||
// backgroundImage: `url('https://via.placeholder.com/300')`,
|
||||
backgroundImage: `url(${sypiu_ggcp})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
>
|
||||
<Flex align='center' justify='center'>
|
||||
<Image src='/vite.svg' height={150} width={150} preview={false} alt='signin' />
|
||||
</Flex>
|
||||
<br />
|
||||
<Form
|
||||
onFinish={handleOnSubmt}
|
||||
layout='vertical'
|
||||
style={{ width: '250px' }}
|
||||
>
|
||||
|
||||
<Form.Item label="Email" name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'email'
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder='email' size='large' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Password" name="password"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input your password!'
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder='password' size='large' />
|
||||
</Form.Item>
|
||||
<div dangerouslySetInnerHTML={{ __html: captchaSvg }} />
|
||||
|
||||
<Form.Item label="Captcha" name="captcha"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'text'
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder='Enter CAPTCHA text' size='large' />
|
||||
</Form.Item>
|
||||
|
||||
{/* <input
|
||||
type="text"
|
||||
placeholder="Enter CAPTCHA text"
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
/> */}
|
||||
|
||||
<Form.Item>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Button type="primary" htmlType="submit" style={{ width: '100%' }}>
|
||||
Registrasi
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
</Form>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => moveToSignin()}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignUp
|
||||
11
src/pages/blank/Blank.jsx
Normal file
11
src/pages/blank/Blank.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Blank = () => {
|
||||
return (
|
||||
<div>
|
||||
Hi From BlankPage
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blank
|
||||
85
src/pages/blank/NotFound.jsx
Normal file
85
src/pages/blank/NotFound.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fafafa',
|
||||
padding: '5vh 16px',
|
||||
textAlign: 'center',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
level={2}
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
marginBottom: 12,
|
||||
fontSize: 'clamp(28px, 5vw, 42px)',
|
||||
color: '#1f1f1f',
|
||||
}}
|
||||
>
|
||||
Oops... You seem lost.
|
||||
</Title>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: 'clamp(14px, 2vw, 18px)',
|
||||
maxWidth: '90%',
|
||||
color: '#595959',
|
||||
marginBottom: '3vh',
|
||||
}}
|
||||
>
|
||||
We couldn't find the page you were looking for. Let us take you back to the main
|
||||
page.
|
||||
</Paragraph>
|
||||
|
||||
<img
|
||||
src={ImgRobot}
|
||||
alt="404 Not Found"
|
||||
style={{
|
||||
maxWidth: '90%',
|
||||
width: '480px',
|
||||
marginBottom: '4vh',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Link to="/">
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
style={{
|
||||
backgroundColor: '#2f2f2f',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
padding: '10px 20px',
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Text type="secondary" style={{ fontSize: '12px', marginTop: '5vh' }}>
|
||||
Illustration by{' '}
|
||||
<a
|
||||
href="https://www.freepik.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#1890ff' }}
|
||||
>
|
||||
Freepik
|
||||
</a>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
58
src/pages/blank/Waiting.jsx
Normal file
58
src/pages/blank/Waiting.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fafafa',
|
||||
padding: '5vh 16px',
|
||||
textAlign: 'center',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={ImgPIU}
|
||||
alt="Loading"
|
||||
style={{
|
||||
maxWidth: '30%',
|
||||
// width: '400px',
|
||||
marginBottom: '4vh',
|
||||
}}
|
||||
/>
|
||||
<Spin size="large" style={{ marginBottom: '4vh' }} />
|
||||
|
||||
<Title
|
||||
level={2}
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
marginBottom: 12,
|
||||
fontSize: 'clamp(28px, 5vw, 42px)',
|
||||
color: '#1f1f1f',
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
</Title>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: 'clamp(14px, 2vw, 18px)',
|
||||
maxWidth: '90%',
|
||||
color: '#595959',
|
||||
marginBottom: '3vh',
|
||||
}}
|
||||
>
|
||||
We are loading your content. This won’t take long.
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Waiting;
|
||||
52
src/pages/home/Home.jsx
Normal file
52
src/pages/home/Home.jsx
Normal file
@@ -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: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• Dashboard
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
Home
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Flex align="center" justify="center">
|
||||
<Text strong style={{fontSize:'30px'}}>Wellcome Call Of Duty App</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
71
src/pages/home/component/StatCard.jsx
Normal file
71
src/pages/home/component/StatCard.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Card } from 'antd';
|
||||
|
||||
const StatCard = ({ title, data }) => {
|
||||
const totalCount = data.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
return (
|
||||
<Card title={title} style={{ borderRadius: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
{data.map((item, idx) => (
|
||||
<div key={idx} style={{ textAlign: 'center', flex: 1 }}>
|
||||
<div style={{ fontSize: 'clamp(12px, 1.5vw, 16px)', fontWeight: 500 }}>
|
||||
{item.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 'clamp(20px, 3vw, 28px)',
|
||||
fontWeight: 700,
|
||||
color: item.color,
|
||||
}}
|
||||
>
|
||||
{item.percent}%
|
||||
</div>
|
||||
<div style={{ fontSize: 'clamp(12px, 1.5vw, 16px)', color: item.color }}>
|
||||
{item.count.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', height: 28, marginTop: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: 20,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{data.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
width: `${item.percent}%`,
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
textAlign: 'center',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Total: {totalCount.toLocaleString()}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatCard;
|
||||
86
src/pages/master/device/IndexDevice.jsx
Normal file
86
src/pages/master/device/IndexDevice.jsx
Normal file
@@ -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: <Text strong style={{ fontSize: '14px' }}>• Master</Text> },
|
||||
{ title: <Text strong style={{ fontSize: '14px' }}>Device</Text> }
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListDevice
|
||||
actionMode={actionMode}
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<DetailDevice
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
showModal={showModal}
|
||||
permitDefault={true}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
{actionMode == 'generatepdf' && (
|
||||
<GeneratePdf
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
showPdf={true}
|
||||
permitDefault={true}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexDevice;
|
||||
308
src/pages/master/device/component/DetailDevice.jsx
Normal file
308
src/pages/master/device/component/DetailDevice.jsx
Normal file
@@ -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 (
|
||||
<Modal
|
||||
// title={`${FormData.id_apd === '' ? 'Tambah' : 'Edit'} APD`}
|
||||
title={`${
|
||||
props.actionMode === 'add'
|
||||
? 'Tambah'
|
||||
: props.actionMode === 'preview'
|
||||
? 'Preview'
|
||||
: 'Edit'
|
||||
} Device`}
|
||||
open={props.showModal}
|
||||
// open={true}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorBgContainer: '#209652',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
defaultColor: '#FFFFFF',
|
||||
defaultBorderColor: '#23a55a',
|
||||
defaultHoverColor: '#FFFFFF',
|
||||
defaultHoverBorderColor: '#23a55a',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!props.readOnly && (
|
||||
<Button loading={confirmLoading} onClick={handleSave}>
|
||||
Simpan
|
||||
</Button>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
</React.Fragment>,
|
||||
]}
|
||||
>
|
||||
{FormData && (
|
||||
<div>
|
||||
{props.permitDefault && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<Text strong>Aktif</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: '8px' }}>
|
||||
<Switch
|
||||
disabled={props.readOnly}
|
||||
style={{
|
||||
backgroundColor:
|
||||
FormData.is_active == 1 ? '#23A55A' : '#bfbfbf',
|
||||
}}
|
||||
checked={FormData.is_active == 1 ? true : false}
|
||||
onChange={handleStatusToggle}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
{FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
</>
|
||||
)}
|
||||
<div hidden>
|
||||
<Text strong>Device ID</Text>
|
||||
<Input
|
||||
name="id_apd"
|
||||
value={FormData.id_apd}
|
||||
onChange={handleInputChange}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Device Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="nama_apd"
|
||||
value={FormData.nama_apd}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 4, marginBottom: 4 }}>
|
||||
<Text strong>Tipe Input</Text>
|
||||
<Text style={{ color: 'red', marginLeft: 4 }}>*</Text>
|
||||
<div>
|
||||
<Radio.Group
|
||||
value={FormData.type_input}
|
||||
options={[
|
||||
{ value: 1, label: 'Check' },
|
||||
{ value: 2, label: 'Text' },
|
||||
{ value: 3, label: 'Number' },
|
||||
]}
|
||||
onChange={onChangeRadio}
|
||||
disabled={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{props.permitDefault && (
|
||||
<div>
|
||||
<Text strong>Jenis Permit</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
|
||||
<CheckboxGroup
|
||||
options={jenisPermit}
|
||||
value={checkedList}
|
||||
onChange={onChange}
|
||||
disabled={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailDevice;
|
||||
127
src/pages/master/device/component/GeneratePdf.jsx
Normal file
127
src/pages/master/device/component/GeneratePdf.jsx
Normal file
@@ -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 (
|
||||
<Modal
|
||||
width='60%'
|
||||
title="Preview PDF"
|
||||
open={props.showPdf}
|
||||
// open={true}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
</ConfigProvider>
|
||||
</>,
|
||||
]}
|
||||
>
|
||||
{pdfUrl && (
|
||||
<iframe
|
||||
src={`${pdfUrl}#zoom=100`}
|
||||
title="PDF Viewer"
|
||||
width="100%"
|
||||
height="600px"
|
||||
style={{ marginTop: '20px', border: '1px solid #ccc' }}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneratePdf;
|
||||
408
src/pages/master/device/component/ListDevice.jsx
Normal file
408
src/pages/master/device/component/ListDevice.jsx
Normal file
@@ -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 ? (
|
||||
<Tag color={'green'} key={'aaa'}>
|
||||
Aktif
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color={'red'} key={'aaa'}>
|
||||
Non-Aktif
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: (_, record) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items,
|
||||
onClick: ({key})=>handleClickMenu(key, record)
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button
|
||||
shape="default"
|
||||
icon={<EllipsisOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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: <span style={{fontSize:'17px'}}>Preview</span>,
|
||||
icon: <EyeOutlined style={{fontSize:'17px', marginTop:'5px'}} />,
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: <span style={{fontSize:'17px'}}>Edit</span>,
|
||||
icon: <EditOutlined style={{fontSize:'17px'}} />,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: <span style={{fontSize:'17px'}}>Delete</span>,
|
||||
icon: <DeleteOutlined style={{fontSize:'17px'}} />,
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={toggleFilter} icon={<FilterOutlined />}>
|
||||
Filter
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space wrap size="small">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => showAddModal()}
|
||||
>
|
||||
Tambah Data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
{/* filter */}
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
{showFilter && (
|
||||
<>
|
||||
<Divider />
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Nama APD">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
maxWidth: '500px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
name="nama_apd"
|
||||
value={formDataFilter.nama_apd}
|
||||
onChange={handleInputChangeFilter}
|
||||
placeholder="Enter Nama APD"
|
||||
style={{ height: '40px' }}
|
||||
/>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorText: 'white',
|
||||
colorBgContainer: 'purple',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
onClick={doFilter}
|
||||
style={{ height: '40px' }}
|
||||
>
|
||||
Cari
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
getData={getAllApd}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns(menu, handleClickMenu)}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListDevice;
|
||||
Reference in New Issue
Block a user