From 4a3ae20b84dfd13308f2062efccb2b90894e5d8a Mon Sep 17 00:00:00 2001 From: bragaz_rexita Date: Wed, 17 Sep 2025 12:17:14 +0700 Subject: [PATCH] Add component --- src/components/Common/BasicButton.jsx | 64 +++ src/components/Common/BasicInput.jsx | 21 + src/components/Common/NavBar.jsx | 58 +++ src/components/Common/Version.jsx | 20 + src/components/Global/ApiRequest.jsx | 127 ++++++ src/components/Global/DataFilter.jsx | 7 + src/components/Global/EmptyData.jsx | 24 + src/components/Global/Formatter.jsx | 192 ++++++++ src/components/Global/KopReport.jsx | 23 + src/components/Global/MqttConnection.jsx | 72 +++ src/components/Global/QrPermit.jsx | 109 +++++ src/components/Global/RegisterRequest.jsx | 25 ++ src/components/Global/StatusButton.jsx | 499 +++++++++++++++++++++ src/components/Global/StatusUserButton.jsx | 294 ++++++++++++ src/components/Global/TableList.jsx | 281 ++++++++++++ src/components/Global/ToastNotif.jsx | 69 +++ src/components/Global/headerReport.jsx | 81 ++++ src/components/loading/Loading.jsx | 28 ++ src/components/loading/loading.css | 80 ++++ 19 files changed, 2074 insertions(+) create mode 100644 src/components/Common/BasicButton.jsx create mode 100644 src/components/Common/BasicInput.jsx create mode 100644 src/components/Common/NavBar.jsx create mode 100644 src/components/Common/Version.jsx create mode 100644 src/components/Global/ApiRequest.jsx create mode 100644 src/components/Global/DataFilter.jsx create mode 100644 src/components/Global/EmptyData.jsx create mode 100644 src/components/Global/Formatter.jsx create mode 100644 src/components/Global/KopReport.jsx create mode 100644 src/components/Global/MqttConnection.jsx create mode 100644 src/components/Global/QrPermit.jsx create mode 100644 src/components/Global/RegisterRequest.jsx create mode 100644 src/components/Global/StatusButton.jsx create mode 100644 src/components/Global/StatusUserButton.jsx create mode 100644 src/components/Global/TableList.jsx create mode 100644 src/components/Global/ToastNotif.jsx create mode 100644 src/components/Global/headerReport.jsx create mode 100644 src/components/loading/Loading.jsx create mode 100644 src/components/loading/loading.css diff --git a/src/components/Common/BasicButton.jsx b/src/components/Common/BasicButton.jsx new file mode 100644 index 0000000..398c945 --- /dev/null +++ b/src/components/Common/BasicButton.jsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Button, ConfigProvider } from 'antd'; +import propTypes from 'prop-types'; + +const BasicButton = ({ + // color, + text, + size, + block, + clickTheButton +}) => { + return ( + <> + + + + + ); +} + +BasicButton.propTypes = { + // color: propTypes.string, + text: propTypes.string, + size: propTypes.string, + block: propTypes.bool, + clickTheButton: propTypes.any +} + +export default BasicButton; \ No newline at end of file diff --git a/src/components/Common/BasicInput.jsx b/src/components/Common/BasicInput.jsx new file mode 100644 index 0000000..62b584d --- /dev/null +++ b/src/components/Common/BasicInput.jsx @@ -0,0 +1,21 @@ +import { theme } from "antd"; +import React from "react"; +// import packageJson from "../../../package.json"; + +const BasicInput = () => { + const { + token: { colorPrimary }, + } = theme.useToken(); + + return ( +
+ +
+ ); +}; + +export default BasicInput; diff --git a/src/components/Common/NavBar.jsx b/src/components/Common/NavBar.jsx new file mode 100644 index 0000000..34d2c71 --- /dev/null +++ b/src/components/Common/NavBar.jsx @@ -0,0 +1,58 @@ +import { Layout, theme } from "antd"; +import React from 'react' +import propTypes from 'prop-types'; + +const { Header } = Layout; + +const navbarClass = + "text-white text-h5 flex flex-col whitespace-nowrap overflow-hidden text-ellipsis"; +const styles = { + navbarTitleFullSize: navbarClass, + navbarTitle: navbarClass + " max-w-[calc(100vw-200px)]", +}; +const NavBar = ({ fullSize }) => { + const { + token: { colorPrimary }, + } = theme.useToken(); + + console.log("import.meta", import.meta) + + return ( +
+
+ +
+ LOGO +
+
+
+
+ {import.meta.env.VITE_PROJECT_NAME} +
+
+ {import.meta.env.VITE_PROJECT_DESCRIPTION} +
+
+
+
+ ); +}; + +NavBar.propTypes = { + fullSize: propTypes.any, +} + +export default NavBar; diff --git a/src/components/Common/Version.jsx b/src/components/Common/Version.jsx new file mode 100644 index 0000000..4aea058 --- /dev/null +++ b/src/components/Common/Version.jsx @@ -0,0 +1,20 @@ +import { theme } from "antd"; +import React from "react"; +import packageJson from "../../../package.json"; + +const Version = () => { + const { + token: { colorPrimary }, + } = theme.useToken(); + return ( +
+ v{packageJson.version} +
+ ); +}; + +export default Version; diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx new file mode 100644 index 0000000..8811919 --- /dev/null +++ b/src/components/Global/ApiRequest.jsx @@ -0,0 +1,127 @@ +import axios from 'axios'; +import Swal from 'sweetalert2'; + +async function ApiRequest( + urlParams = { method: 'GET', params: {}, url: '', prefix: '/', token: true } +) { + const baseURLDef = `${import.meta.env.VITE_API_SERVER}`; + const instance = axios.create({ + baseURL: urlParams.url ?? baseURLDef, + }); + + const isFormData = urlParams.params instanceof FormData; + + const request = { + method: urlParams.method, + url: urlParams.prefix ?? '/', + data: urlParams.params, + // yang lama + // headers: { + // 'Content-Type': 'application/json', + // 'Accept-Language': 'en_US', + // }, + + // yang baru + headers: { + 'Accept-Language': 'en_US', + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + }, + }; + + if (urlParams.params === 'doc') { + request.responseType = 'arraybuffer'; + request.headers['Content-Type'] = 'blob'; + } + + // console.log(request); + + // console.log('prefix', urlParams.prefix); + + const tokenRedirect = sessionStorage.getItem('token_redirect'); + + let stringToken = ''; + + if (tokenRedirect !== null) { + stringToken = tokenRedirect; + // console.log(`sessionStorage: ${tokenRedirect}`); + } else { + stringToken = localStorage.getItem('token'); + // console.log(`localStorage: ${stringToken}`); + } + + if (urlParams.prefix !== 'auth/login') { + const tokenWithQuotes = stringToken; + const token = tokenWithQuotes.replace(/"/g, ''); + const AUTH_TOKEN = `Bearer ${token}`; + instance.defaults.headers.common['Authorization'] = AUTH_TOKEN; + } else if (urlParams.token == true) { + const tokenWithQuotes = stringToken; + const token = tokenWithQuotes.replace(/"/g, ''); + const AUTH_TOKEN = `Bearer ${token}`; + instance.defaults.headers.common['Authorization'] = AUTH_TOKEN; + } + + return await instance(request) + .then(function (response) { + const responseCustom = response; + responseCustom.error = false; + return responseCustom; + }) + .catch(function (error) { + console.log('error', error.toJSON()); + + const errorData = error.toJSON(); + + const respError = error.response ?? {}; + cekError( + errorData.status, + error?.response?.data?.message ?? errorData?.message ?? 'Something Wrong' + ); + respError.error = true; + return respError; + }); +} + +async function cekError(props, message = '') { + console.log('status code', props); + if (props === 401) { + Swal.fire({ + icon: 'warning', + title: 'Peringatan', + text: `${message}, Silahkan login`, + }).then((result) => { + if (result.isConfirmed) { + localStorage.clear(); + location.replace('/signin'); + } else if (result.isDenied) { + Swal.fire('Changes are not saved', '', 'info'); + } + }); + } else { + Swal.fire({ + icon: 'warning', + title: 'Peringatan', + text: message, + }).then((result) => {}); + } +} + +const SendRequest = async (queryParams) => { + try { + const response = await ApiRequest(queryParams); + return response || []; + } catch (error) { + console.log('error', error); + if (error.response) { + console.error('Error Status:', error.response.status); // Status error, misal: 401 + console.error('Error Data:', error.response.data); // Detail pesan error + console.error('Error Pesan:', error.response.data.message); //Pesan error + } else { + console.error('Error:', error.message); + } + Swal.fire({ icon: 'error', text: error }); + // return error; + } +}; + +export { ApiRequest, SendRequest }; diff --git a/src/components/Global/DataFilter.jsx b/src/components/Global/DataFilter.jsx new file mode 100644 index 0000000..7a96638 --- /dev/null +++ b/src/components/Global/DataFilter.jsx @@ -0,0 +1,7 @@ +let filterData = []; + +export const setFilterData = (data) => { + filterData = data; +}; + +export const getFilterData = () => filterData; \ No newline at end of file diff --git a/src/components/Global/EmptyData.jsx b/src/components/Global/EmptyData.jsx new file mode 100644 index 0000000..d1b3ab4 --- /dev/null +++ b/src/components/Global/EmptyData.jsx @@ -0,0 +1,24 @@ +import { Empty, Col, Row, Typography } from "antd"; +const {Text} = Typography; + +const EmptyData = ({ + titleButton, + titlePositionButton +})=>{ + return( + + + Data Kosong + + + Untuk menampilkan data silahkan klik tombol {titleButton} {titlePositionButton} + + + } + /> + ); +}; + +export default EmptyData; \ No newline at end of file diff --git a/src/components/Global/Formatter.jsx b/src/components/Global/Formatter.jsx new file mode 100644 index 0000000..9aef87c --- /dev/null +++ b/src/components/Global/Formatter.jsx @@ -0,0 +1,192 @@ +const formatIDR = (value) => { + if (!value) return ''; + return new Intl.NumberFormat('id-ID', { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +}; + +const formatCurrencyUSD = (value) => { + if (!value) return ''; + const numericValue = value.replace(/[^0-9.]/g, ''); // Hanya angka dan titik + const parts = numericValue.split('.'); // Pisahkan bagian desimal + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); // Tambahkan koma + return `${parts.join('.')}`; // Gabungkan kembali dengan simbol $ +}; + +const formatCurrencyIDR = (value) => { + if (!value) return ''; + const numericValue = value.replace(/[^0-9]/g, ''); + return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); +}; + +const toApiNumberFormatter = (value) => { + if (!value) return ''; + const formattedValue = value.replace(/[.,]/g, ''); // Hapus semua titik + return Number(formattedValue); +}; + +const toAppDateFormatter = (value) => { + const date = new Date(value); + + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + const day = date.getUTCDate().toString().padStart(2, '0'); + const month = months[date.getUTCMonth()]; + const year = date.getUTCFullYear(); + + const formattedDate = `${day} ${month} ${year}`; + return formattedDate; +}; + +const toAppDateFormatterTwoDigit = (value) => { + const date = new Date(value); + + // Pastikan validitas tanggal + if (isNaN(date.getTime())) { + return 'Invalid Date'; // Handle nilai tidak valid + } + + const day = date.getUTCDate().toString().padStart(2, '0'); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); // Tambahkan 1 ke bulan karena bulan dimulai dari 0 + const year = date.getUTCFullYear(); + + const formattedDate = `${day}-${month}-${year}`; + return formattedDate; +}; + +const toApiDateFormatter = (value) => { + const parts = value.split('-'); + if (parts.length === 3) { + const day = parts[0]; + const month = parts[1]; + const year = parts[2]; + return `${year}-${month}-${day}`; + } + + return ''; +}; + +const toAppDateTimezoneFormatter = (value) => { + const jakartaTimezone = 'Asia/Jakarta'; + const date = new Date(value); + + const formatterDay = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + day: '2-digit', + }); + const formatterMonth = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + month: 'short', + }); + const formatterYear = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + year: 'numeric', + }); + + const day = formatterDay.format(date); + const month = formatterMonth.format(date); + const year = formatterYear.format(date); + + return `${day} ${month} ${year}`; +}; + +const toApiDateTimezoneFormatter = (value) => { + const jakartaTimezone = 'Asia/Jakarta'; + const date = new Date(value); + + const formatterDay = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + day: '2-digit', + }); + const formatterMonth = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + month: '2-digit', + }); + const formatterYear = new Intl.DateTimeFormat('en-US', { + timeZone: jakartaTimezone, + year: 'numeric', + }); + + const day = formatterDay.format(date); + const month = formatterMonth.format(date); + const year = formatterYear.format(date); + + return `${year}-${month}-${day}`; +}; + +import { message } from 'antd'; +// cryptoHelper.js +import CryptoJS from 'crypto-js'; + +const secretKey = `${import.meta.env.VITE_KEY_SESSION}`; // Ganti dengan kunci rahasia kamu + +// Fungsi untuk mengenkripsi data +const encryptData = (data) => { + try { + const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), secretKey).toString(); + return ciphertext; + } catch (error) { + console.error('Encrypt Error:', error); + return null; + } +}; + +// Fungsi untuk mendekripsi data +const decryptData = (ciphertext) => { + try { + const bytes = CryptoJS.AES.decrypt(ciphertext, secretKey); + const decrypted = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + if (decrypted?.error) { + decrypted.error = false; + } + return decrypted; + } catch (error) { + // console.error('Decrypt Error:', error); + return { error: true, message: `Decrypt Error: ${error}` }; + } +}; + +const getSessionData = () => { + try { + const ciphertext = localStorage.getItem('session'); + + if (!ciphertext) { + return { + error: true, + }; + } + const result = decryptData(ciphertext); + return result; + } catch (error) { + // console.error('Decrypt Error:', error); + return { error: true, message: error }; + } +}; + +export { + formatIDR, + formatCurrencyUSD, + formatCurrencyIDR, + toApiNumberFormatter, + toAppDateFormatter, + toApiDateFormatter, + toAppDateTimezoneFormatter, + toAppDateFormatterTwoDigit, + toApiDateTimezoneFormatter, + encryptData, + getSessionData, + decryptData, +}; diff --git a/src/components/Global/KopReport.jsx b/src/components/Global/KopReport.jsx new file mode 100644 index 0000000..b01d1f3 --- /dev/null +++ b/src/components/Global/KopReport.jsx @@ -0,0 +1,23 @@ +const toBase64 = (url) => + fetch(url) + .then((res) => res.blob()) + .then( + (blob) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }) + ); + + // Fungsi utama +const kopReportPdf = async (logo, title) => { + const images = await toBase64(logo); + return { + images, + title, + }; +}; + +export { kopReportPdf }; \ No newline at end of file diff --git a/src/components/Global/MqttConnection.jsx b/src/components/Global/MqttConnection.jsx new file mode 100644 index 0000000..1a9551b --- /dev/null +++ b/src/components/Global/MqttConnection.jsx @@ -0,0 +1,72 @@ +// mqttService.js +import mqtt from 'mqtt'; + +const mqttUrl = 'ws://36.66.16.49:9001'; +const topics = ['SYPIU_GGCP', 'SYPIU_GGCP/list-permit/changed','SYPIU_GGCP/list-user/changed']; + +const options = { + keepalive: 30, + clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8), + protocolId: 'MQTT', + protocolVersion: 4, + clean: true, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + username: 'ide', // jika ada + password: 'aremania', // jika ada +}; + +const client = mqtt.connect(mqttUrl, options); + +// Track connection status +let isConnected = false; + +client.on('connect', () => { + console.log('MQTT Connected'); + isConnected = true; + + // Subscribe default topic + client.subscribe(topics, (err) => { + if (err) console.error('Subscribe error:', err); + else console.log(`Subscribed to topics: ${topics.join(', ')}`); + }); +}); + +client.on('error', (err) => { + console.error('Connection error: ', err); + client.end(); +}); + +client.on('close', () => { + console.log('MQTT Disconnected'); + isConnected = false; +}); + +/** + * Publish message to MQTT + * @param {string} topic + * @param {string} message + */ +const publishMessage = (topic, message) => { + if (client && isConnected && message.trim() !== '') { + client.publish(topic, message); + } else { + console.warn('MQTT not connected or message empty'); + } +}; + +/** + * Listen to incoming messages + * @param {function} callback - Function(topic, message) + */ +const listenMessage = (callback) => { + client.on('message', (topic, message) => { + callback(topic, message.toString()); + }); +}; + +export { + publishMessage, + listenMessage, + client, +}; diff --git a/src/components/Global/QrPermit.jsx b/src/components/Global/QrPermit.jsx new file mode 100644 index 0000000..cb42ad7 --- /dev/null +++ b/src/components/Global/QrPermit.jsx @@ -0,0 +1,109 @@ +import { Typography } from 'antd'; +import { useSearchParams } from 'react-router-dom'; +import ImgPIU from '../../assets/freepik/LOGOPIU.png'; + +const { Title, Text } = Typography; + +function QrPermit() { + const [searchParams] = useSearchParams(); + const selectedRole = searchParams.get('role'); + const selectedName = searchParams.get('name'); + const selectedTime = searchParams.get('time'); + + const formattedTime = selectedTime + ? new Date(selectedTime).toLocaleString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + : '-'; + + const userApproval = { + role: ['Performing Authority', 'Permit Control', 'Site Authority', 'Area Authority'], + roleCode: ['PA', 'PC', 'SA', 'AA'], + }; + + const filteredRoles = userApproval.role.filter((role, index) => { + const code = userApproval.roleCode[index]; + return !selectedRole || selectedRole === code; + }); + + const isMobile = window.innerWidth <= 768; + + return ( +
+ Logo PIU + {filteredRoles.map((role, index) => ( +
+ + Validasi Dokumen <br /> Pupuk Indonesia Utilitas + + + {role} + +
+ + Nama: + {' '} + {selectedName ?? '-'} +
+ + {formattedTime} + +
+
+ ))} +
+ ); +} + +export default QrPermit; diff --git a/src/components/Global/RegisterRequest.jsx b/src/components/Global/RegisterRequest.jsx new file mode 100644 index 0000000..4bd767c --- /dev/null +++ b/src/components/Global/RegisterRequest.jsx @@ -0,0 +1,25 @@ +import axios from 'axios'; + +const RegistrationRequest = async ({ method, prefix, params, headers = {} }) => { + const baseURL = `${import.meta.env.VITE_API_SERVER}`; + + try { + const response = await axios({ + method: method, + url: `${baseURL}/${prefix}`, + data: params, + headers: { + 'Accept-Language': 'en_US', + ...headers, + }, + withCredentials: true, + }); + + return response.data || {}; + } catch (error) { + console.error(`Error saat ${prefix}:`, error.response?.data || error.message); + throw error.response?.data || { message: 'Terjadi kesalahan pada server' }; + } +}; + +export default RegistrationRequest; \ No newline at end of file diff --git a/src/components/Global/StatusButton.jsx b/src/components/Global/StatusButton.jsx new file mode 100644 index 0000000..9c7b531 --- /dev/null +++ b/src/components/Global/StatusButton.jsx @@ -0,0 +1,499 @@ +import React, { useState } from 'react'; +import { + Button, + Modal, + Divider, + Card, + Tag, + ConfigProvider, + Typography, + message, + Input, + Radio, +} from 'antd'; +import { getStatusHistory, approvalPermit } from '../../api/status-history'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from './ToastNotif'; +import { getSessionData } from './Formatter'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +dayjs.extend(utc); + +const { Text } = Typography; +const { TextArea } = Input; + +const StatusButton = (props) => { + const { + color, + name, + style, + canApprove = true, + canReject = true, + refreshData = (e) => {}, + } = props; + + const session = getSessionData(); + const isVendor = session?.user?.role_id == `${import.meta.env.VITE_ROLE_VENDOR}`; + + const [isModalVisible, setIsModalVisible] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [historyData, setHistoryData] = useState([]); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [showPermitSelesai, setShowPermitSelesai] = useState(false); + const [actionType, setActionType] = useState(null); + const [deskripsi, setDeskripsi] = useState(''); + const [closeType, setCloseType] = useState('1'); + + const pengajuanPermitSelesai = 4; + const permitSelesai = 7; + + const fetchHistory = async () => { + const permitId = props.permitId; + const id = parseInt(permitId); + + if (!permitId || isNaN(id)) { + console.error('Permit ID tidak valid:', permitId); + message.error('Permit ID tidak valid atau kosong'); + return; + } + + try { + const res = await getStatusHistory(id); + + const mapped = + res?.data?.data?.map((item) => ({ + name: item.name, + color: item.status_permit_color, + text: item.status_permit_name, + deskripsi: item.deskripsi, + date: item.created_at, + closed: item.close_type !== null ? item.close_type_name : null, + })) ?? []; + + setHistoryData(mapped); + } catch (err) { + console.error('API ERROR:', err); + message.error('Gagal mengambil riwayat status dari server'); + } + }; + + const showModal = () => { + fetchHistory(); + setIsModalVisible(true); + }; + + const handleCancel = () => { + setIsModalVisible(false); + }; + + const handleSelesai = () => { + setShowPermitSelesai(true); + }; + + const handleApprove = () => { + setActionType('approve'); + setShowConfirmModal(true); + }; + + const handleReject = () => { + setActionType('reject'); + setShowConfirmModal(true); + }; + + const submitSelesai = async () => { + const payload = { + status_permit: true, + close_type: closeType, + }; + + try { + + setConfirmLoading(true); + const response = await approvalPermit(props.permitId, payload); + + if (response?.status === 200) { + NotifOk({ + icon: 'success', + title: 'Pengajuan Selesai', + message: `Permit berhasil diajukan sebagai ${ + closeType === '1' ? 'selesai' : 'belum selesai' + }.`, + }); + setIsModalVisible(false); + setShowPermitSelesai(false); + setCloseType(''); + setTimeout(() => { + refreshData(); + }, 500); + } else { + throw new Error(response?.data?.message || 'Proses gagal'); + } + } catch (err) { + console.error('Error saat mengajukan permit:', err); + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: err.message || 'Terjadi kesalahan saat memproses permit.', + }); + } finally { + setConfirmLoading(false); + setShowPermitSelesai(false); + setCloseType(''); + } + }; + + const submitApproval = async () => { + const payload = { + status_permit: actionType === 'approve' ? true : false, + deskripsi: deskripsi.trim(), + }; + + try { + + setConfirmLoading(true); + const response = await approvalPermit(props.permitId, payload); + + if (response?.status === 200) { + NotifOk({ + icon: 'success', + title: actionType === 'approve' ? 'Disetujui' : 'Ditolak', + message: + actionType === 'approve' + ? 'Permit berhasil disetujui.' + : 'Permit berhasil ditolak.', + }); + setIsModalVisible(false); + setShowConfirmModal(false); + setDeskripsi(''); + setTimeout(() => { + refreshData(); + }, 500); + } else { + throw new Error(response?.data?.message || 'Proses gagal'); + } + } catch (err) { + console.error('Error saat menyetujui permit:', err); + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: err.message || 'Terjadi kesalahan saat memproses permit.', + }); + } finally { + setConfirmLoading(false); + setShowConfirmModal(false); + setDeskripsi(''); + } + }; + + return ( + <> + + + + {name ?? 'Belum ada status'} + + } + open={isModalVisible} + onCancel={handleCancel} + footer={[ + <> + + + + {props.status_permit === pengajuanPermitSelesai && + historyData.length >= 0 && + isVendor && ( + <> + + + + + )} + {props.status_permit !== 0 && + props.status_permit !== pengajuanPermitSelesai && + props.status_permit !== permitSelesai && + historyData.length >= 0 && ( + <> + {canReject && ( + + + + )} + {canApprove && ( + + + + )} + + )} + , + ]} + > + + + {historyData.length > 0 ? ( + [...historyData] + .sort((a, b) => new Date(b.date) - new Date(a.date)) + .map((item, index) => ( + +
+ + {item.text} + + {item.date != null && ( + + {dayjs.utc(item.date).format('YYYY-MM-DD HH:mm:ss')} + + )} +
+
+
{item.name}
+ + {item.closed && ( + + Closed: {item.closed} + + )} +
+ + {item.deskripsi && ( +

+ {item.deskripsi} +

+ )} +
+ )) + ) : ( + Belum ada riwayat status. + )} +
+ + { + setShowConfirmModal(false); + setDeskripsi(''); + }} + confirmLoading={confirmLoading} + footer={[ + , + + + + , + ]} + > +

Silakan isi deskripsi:

+