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