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 (
+
+

+ {filteredRoles.map((role, index) => (
+
+
+ Validasi Dokumen
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:
+
+
+ {
+ setShowPermitSelesai(false);
+ setCloseType('');
+ }}
+ confirmLoading={confirmLoading}
+ footer={[
+ ,
+
+
+ ,
+ ]}
+ >
+ Status Permit saat ini :
+ setCloseType(e.target.value)} value={closeType}>
+ Belum Selesai
+ Selesai
+
+
+ >
+ );
+};
+
+export default StatusButton;
diff --git a/src/components/Global/StatusUserButton.jsx b/src/components/Global/StatusUserButton.jsx
new file mode 100644
index 0000000..2a3034f
--- /dev/null
+++ b/src/components/Global/StatusUserButton.jsx
@@ -0,0 +1,294 @@
+import React, { useState } from 'react';
+import { Button, Modal, Divider, Card, Tag, ConfigProvider, Typography } from 'antd';
+import { NotifAlert, NotifOk, NotifConfirmDialog } from './ToastNotif';
+import { approvalUser } from '../../api/user-admin';
+import { toAppDateFormatter } from './Formatter';
+
+const { Text } = Typography;
+
+const StatusUserButton = ({ color, name, data, readOnly, style }) => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [confirmLoading, setConfirmLoading] = useState(false);
+
+ const showModal = () => {
+ setIsModalVisible(true);
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ const statusColor = data?.warna || color || '#999';
+ const statusName = data?.status_name || name || 'N/A';
+ const userCreated = data?.user_add_name || 'Pengguna tidak dikenal';
+ const userUpdated = data?.user_upd_name || 'Pengguna tidak dikenal';
+
+ const handleApprove = async () => {
+ setConfirmLoading(true);
+ try {
+ const payload = {
+ approve_user: true,
+ };
+ const response = await approvalUser(data?.id_register, payload);
+
+ if (response?.data?.statusCode === 200) {
+ NotifOk({
+ icon: 'success',
+ title: 'Berhasil',
+ message: 'User berhasil di-approve.',
+ });
+ setIsModalVisible(false);
+ } else {
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: response?.data?.message || 'Gagal approve user.',
+ });
+ }
+ } catch (err) {
+ console.error('Error saat approve user:', err);
+ NotifAlert({
+ icon: 'error',
+ title: 'Error',
+ message: 'Terjadi kesalahan pada server.',
+ });
+ } finally {
+ setConfirmLoading(false);
+ }
+ };
+
+ const handleReject = async () => {
+ setConfirmLoading(true);
+ NotifConfirmDialog({
+ icon: 'question',
+ title: 'Konfirmasi Penolakan',
+ message: 'Apakah kamu yakin ingin menolak permintaan ini?',
+ confirmButtonText: 'Reject',
+ onConfirm: async () => {
+ try {
+ const payload = {
+ approve_user: false,
+ };
+ const response = await approvalUser(data.id_register, payload);
+
+ if (response?.data?.statusCode === 200) {
+ NotifOk({
+ icon: 'success',
+ title: 'Ditolak',
+ message: 'User berhasil ditolak.',
+ });
+ setIsModalVisible(false);
+ } else {
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: response?.message || 'Gagal reject user.',
+ });
+ }
+ } catch (err) {
+ NotifAlert({
+ icon: 'error',
+ title: 'Error',
+ message: 'Terjadi kesalahan pada server.',
+ });
+ } finally {
+ setConfirmLoading(false);
+ }
+ },
+ onCancel: () => {
+ },
+ });
+ };
+
+ return (
+ <>
+
+
+
+ {statusName}
+
+ }
+ open={isModalVisible}
+ onCancel={handleCancel}
+ footer={[
+ <>
+
+
+
+ {data?.status_register === 1 && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ >,
+ ]}
+ >
+
+
+ {data ? (
+ <>
+ {data.updated_at !== data.created_at && (
+
+
+
+ Updated at
+
+
+ {toAppDateFormatter(data.updated_at)}
+
+
+
+ Diubah terakhir oleh {userUpdated}
+
+
+ )}
+
+
+
+ Created at
+
+
+ {toAppDateFormatter(data.created_at)}
+
+
+
+ Dibuat oleh {userCreated}
+
+
+ >
+ ) : (
+ Belum ada riwayat status.
+ )}
+
+ >
+ );
+};
+
+export default StatusUserButton;
diff --git a/src/components/Global/TableList.jsx b/src/components/Global/TableList.jsx
new file mode 100644
index 0000000..7e4d221
--- /dev/null
+++ b/src/components/Global/TableList.jsx
@@ -0,0 +1,281 @@
+import React, { memo, useState, useEffect, useRef } from 'react';
+import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag } from 'antd';
+import {
+ PlusOutlined,
+ FilterOutlined,
+ EditOutlined,
+ DeleteOutlined,
+ EyeOutlined,
+ SearchOutlined,
+ FilePdfOutlined,
+} from '@ant-design/icons';
+import { setFilterData } from './DataFilter';
+
+const { Text } = Typography;
+
+const defCard = {
+ r1: {
+ style: { fontWeight: 'bold', fontSize: 13 },
+ type: 'primary',
+ color: '',
+ text: 'Cold Work Permit',
+ name: '',
+ },
+ r2: {
+ style: { marginLeft: 8, fontSize: 13 },
+ type: 'primary',
+ color: 'success',
+ text: 'Pengajuan',
+ name: '',
+ },
+ r3: {
+ style: { fontSize: 12 },
+ type: 'secondary',
+ color: '',
+ text: 'No. IVR/20250203/XXV/III',
+ name: '',
+ },
+ r4: {
+ style: { fontSize: 12 },
+ type: 'primary',
+ color: '',
+ text: '3 Feb 2025',
+ name: '',
+ },
+ r5: {
+ style: { fontSize: 12 },
+ type: 'primary',
+ color: '',
+ text: 'Lokasi Gudang Robang',
+ name: '',
+ },
+ r6: {
+ style: { fontSize: 12 },
+ type: 'primary',
+ color: '',
+ text: 'maka tambahkan user tersebut dalam user_partner dengan partner baru yang ditambahkan diatas',
+ name: '',
+ },
+ action: (e) => {},
+};
+
+const TableList = memo(function TableList({
+ getData,
+ queryParams,
+ columns,
+ triger,
+ mobile,
+ rowSelection = null,
+}) {
+ const [gridLoading, setGridLoading] = useState(false);
+
+ const [data, setData] = useState([]);
+ const [pagingResponse, setPagingResponse] = useState({
+ totalData: '',
+ perPage: '',
+ totalPage: '',
+ });
+
+ const [pagination, setPagination] = useState({
+ current: 1,
+ limit: 10,
+ total: 0,
+ });
+
+ const { useBreakpoint } = Grid;
+
+ useEffect(() => {
+ filter(1, 10);
+ }, [triger]);
+
+ const filter = async (currentPage, pageSize) => {
+ setGridLoading(true);
+
+ const paging = {
+ page: currentPage,
+ limit: pageSize,
+ };
+
+ const param = new URLSearchParams({ ...paging, ...queryParams });
+
+ const resData = await getData(param);
+ if (resData) {
+ setTimeout(() => {
+ setGridLoading(false);
+ }, 900);
+ }
+
+ setData(resData.data.data ?? []);
+ setFilterData(resData.data.data ?? []);
+
+ if (resData.status == 200) {
+ setPagingResponse({
+ totalData: resData.data.total,
+ perPage: resData.data.paging.page_total,
+ totalPage: resData.data.paging.limit,
+ });
+
+ setPagination((prev) => ({
+ ...prev,
+ current: resData.data.paging.page,
+ limit: resData.data.paging.limit,
+ total: resData.data.paging.total,
+ }));
+ }
+ };
+
+ const handlePaginationChange = (page, pageSize) => {
+ setPagination((prev) => ({
+ ...prev,
+ current: page,
+ pageSize,
+ }));
+ filter(page, pageSize);
+ };
+
+ const screens = useBreakpoint();
+
+ const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
+
+ return (
+
+ {isMobile && mobile ? (
+
+
+ {data.map((item) => (
+
+ {mobile.r1 && (
+
+ {item[mobile.r1.name] ?? mobile.r1.text ?? ''}
+
+ )}
+ {mobile.r2 && (
+
+ {item[mobile.r2.name] ?? mobile.r2.text ?? ''}
+
+ )}
+
+ )
+ }
+ style={{ width: '100%' }}
+ >
+
+ {mobile.r3 && mobile.r4 && (
+
+
+ {item[mobile.r3 ? mobile.r3.name : ''] ?? ''}
+
+
+ {item[mobile.r4 ? mobile.r4.name : ''] ?? ''}
+
+
+ )}
+ {mobile.r5 && (
+
+ {item[mobile.r5 ? mobile.r5.name : ''] ?? ''}
+
+ )}
+
+ {mobile.r6 && (
+
+
+ {item[mobile.r6 ? mobile.r6.name : ''] ?? ''}
+
+
+ )}
+
+ }
+ onClick={(e) => {
+ mobile.action(item);
+ }}
+ >
+ Detail
+
+
+
+ ))}
+
+
+ ) : (
+
+ {/* TABLE */}
+ ({ ...item, key: index }))}
+ pagination={false}
+ loading={gridLoading}
+ scroll={{
+ y: 520,
+ x: 1300,
+ }}
+ />
+
+ {/* PAGINATION */}
+
+
+
+
+ Menampilkan {pagingResponse.totalData} Data dari{' '}
+ {pagingResponse.perPage} Halaman
+
+
+
+
+
+
+
+
+ )}
+
+ );
+});
+
+export default TableList;
diff --git a/src/components/Global/ToastNotif.jsx b/src/components/Global/ToastNotif.jsx
new file mode 100644
index 0000000..cf9d8ad
--- /dev/null
+++ b/src/components/Global/ToastNotif.jsx
@@ -0,0 +1,69 @@
+import Swal from 'sweetalert2';
+
+const NotifAlert = ({ icon, title, message }) => {
+ Swal.fire({
+ icon: icon,
+ title: title,
+ text: message,
+ showConfirmButton: false,
+ position: 'center',
+ timer: 2000,
+ });
+};
+
+const NotifOk = ({ icon, title, message }) => {
+ Swal.fire({
+ icon: icon,
+ title: title,
+ text: message,
+ });
+};
+
+const NotifConfirmDialog = ({
+ icon,
+ title,
+ message,
+ onConfirm,
+ onCancel,
+ confirmButtonText = 'Hapus',
+}) => {
+ Swal.fire({
+ icon: icon,
+ title: title,
+ text: message,
+ showCancelButton: true,
+ cancelButtonColor: '#23A55A',
+ cancelButtonText: 'Batal',
+ confirmButtonColor: '#d33000',
+ confirmButtonText: confirmButtonText,
+ reverseButtons: true,
+ }).then((result) => {
+ if (result.isConfirmed) {
+ onConfirm();
+ } else if (result.dismiss) {
+ onCancel();
+ }
+ });
+};
+
+const QuestionConfirmSubmit = ({ icon, title, message, onConfirm, onCancel }) => {
+ Swal.fire({
+ icon: icon,
+ title: title,
+ text: message,
+ showCancelButton: true,
+ cancelButtonColor: '#23A55A',
+ cancelButtonText: 'Batal',
+ confirmButtonColor: '#d33000',
+ confirmButtonText: 'Proses',
+ reverseButtons: true,
+ }).then((result) => {
+ if (result.isConfirmed) {
+ onConfirm();
+ } else if (result.dismiss) {
+ onCancel();
+ }
+ });
+};
+
+export { NotifAlert, NotifOk, NotifConfirmDialog, QuestionConfirmSubmit };
diff --git a/src/components/Global/headerReport.jsx b/src/components/Global/headerReport.jsx
new file mode 100644
index 0000000..47b8a30
--- /dev/null
+++ b/src/components/Global/headerReport.jsx
@@ -0,0 +1,81 @@
+import { Row, Col, Button, ConfigProvider, Divider, Typography } from "antd";
+import { ArrowLeftOutlined, SearchOutlined } from '@ant-design/icons';
+
+const {Text} = Typography;
+
+const HeaderReport = ({
+ title,
+ loadingPilihDataStep1,
+ handleSelectData,
+ step
+})=>{
+ return(
+ <>
+
+
+
+ {title}
+
+
+ {step}
+
+
+
+
+
+ }>Batal
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+export default HeaderReport;
\ No newline at end of file
diff --git a/src/components/loading/Loading.jsx b/src/components/loading/Loading.jsx
new file mode 100644
index 0000000..21fd37c
--- /dev/null
+++ b/src/components/loading/Loading.jsx
@@ -0,0 +1,28 @@
+import React, { memo } from 'react'
+import './loading.css'
+
+
+const Loading = memo(function Loading() {
+ return(
+
+
+
+
+ L
+ o
+ a
+ d
+ i
+ n
+ g
+ .
+ .
+ .
+
+
+
+
+ );
+})
+
+export default Loading
\ No newline at end of file
diff --git a/src/components/loading/loading.css b/src/components/loading/loading.css
new file mode 100644
index 0000000..763b9bb
--- /dev/null
+++ b/src/components/loading/loading.css
@@ -0,0 +1,80 @@
+.mask {
+ /* background-color: rgba(0, 0, 0, .6); */
+ background-color: rgba(255, 255, 255, .6);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 999;
+}
+
+.main {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+text {
+ font-weight: bolder;
+}
+
+@keyframes letter {
+ 0% {
+ font-size: 30px;
+ }
+
+ 50% {
+ font-size: 40px;
+ }
+
+ 100% {
+ font-size: 30px;
+ }
+}
+
+.letter {
+ animation: letter 1s infinite;
+ color: #fff;
+}
+
+.letter1 {
+ animation-delay: 0s;
+}
+
+.letter2 {
+ animation-delay: -0.9s;
+}
+
+.letter3 {
+ animation-delay: -0.8s;
+}
+
+.letter4 {
+ animation-delay: -0.7s;
+}
+
+.letter5 {
+ animation-delay: -0.6s;
+}
+
+.letter6 {
+ animation-delay: -0.5s;
+}
+
+.letter7 {
+ animation-delay: -0.4s;
+}
+
+.letter8 {
+ animation-delay: -0.3s;
+}
+
+.letter9 {
+ animation-delay: -0.2s;
+}
+
+.letter10 {
+ animation-delay: -0.1s;
+}
\ No newline at end of file