Add pages

This commit is contained in:
2025-09-17 12:16:50 +07:00
parent bc60728369
commit 291cbd5f94
12 changed files with 2012 additions and 0 deletions

View 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;

View 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;

View 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;

View 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;