lavoce #1

Merged
yogiedigital merged 11 commits from lavoce into main 2025-09-17 08:39:36 +00:00
19 changed files with 2074 additions and 0 deletions
Showing only changes of commit 4a3ae20b84 - Show all commits

View File

@@ -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 (
<>
<ConfigProvider
theme={{
token: {
// colorBgContainer: '#23A55A',
colorText: 'white',
colorBgContainer: 'purple',
// Seed Token
// colorPrimary: '#23A55A',
// // colorPrimary: `${color}`,
// borderRadius: 8,
// // Alias Token
// // colorBgContainer: '#f6ffed',
// // colorBgContainer: {color},
},
components: {
Button: {
defaultBg: '#23A55A',
defaultColor: 'white',
}
},
}}
>
<Button
type=""
block={block}
size={size}
onClick={() => clickTheButton(block)}
// style={{
// background: "#23A55A",
// borderColor: "#23A55A",
// color: "white",
// }}
>
{text}
</Button>
</ConfigProvider>
</>
);
}
BasicButton.propTypes = {
// color: propTypes.string,
text: propTypes.string,
size: propTypes.string,
block: propTypes.bool,
clickTheButton: propTypes.any
}
export default BasicButton;

View File

@@ -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 (
<div
data-testid="versionContainer"
className="cursor-default fixed top-1 right-1 z-20 m-1"
style={{ color: colorPrimary }}
>
</div>
);
};
export default BasicInput;

View File

@@ -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 (
<Header
data-testid="navbarContainer"
className="fixed z-10 flex w-full drop-shadow-md"
style={{
backgroundColor: colorPrimary,
paddingInline: 0,
}}
>
<div className="flex h-full w-full items-center gap-4 pl-[5%]">
<a href="/">
<div
data-testid="navbarLogo"
className="h-12 w-48 bg-white text-center"
>
LOGO
</div>
</a>
<div
data-testid="navbarTitle"
className={fullSize ? styles.navbarTitleFullSize : styles.navbarTitle}
>
<div className="text-[2.9vw] font-bold leading-8 sm:text-h5">
{import.meta.env.VITE_PROJECT_NAME}
</div>
<div className="pb-1 text-[2vw] font-bold uppercase leading-3 sm:text-small">
{import.meta.env.VITE_PROJECT_DESCRIPTION}
</div>
</div>
</div>
</Header>
);
};
NavBar.propTypes = {
fullSize: propTypes.any,
}
export default NavBar;

View File

@@ -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 (
<div
data-testid="versionContainer"
className="cursor-default fixed top-1 right-1 z-20 m-1"
style={{ color: colorPrimary }}
>
v{packageJson.version}
</div>
);
};
export default Version;

View File

@@ -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 };

View File

@@ -0,0 +1,7 @@
let filterData = [];
export const setFilterData = (data) => {
filterData = data;
};
export const getFilterData = () => filterData;

View File

@@ -0,0 +1,24 @@
import { Empty, Col, Row, Typography } from "antd";
const {Text} = Typography;
const EmptyData = ({
titleButton,
titlePositionButton
})=>{
return(
<Empty
description={
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Row justify="center" align="middle" style={{ height: "100%" }}>
<Text style={{fontSize:'22px', fontWeight:'bold'}}>Data Kosong</Text>
</Row>
<Row justify="center" align="middle" style={{ height: "100%"}}>
<Text style={{width:'20%', fontSize:'20px'}}>Untuk menampilkan data silahkan klik tombol <Text strong style={{fontSize:'20px'}}>{titleButton}</Text> {titlePositionButton}</Text>
</Row>
</Col>
}
/>
);
};
export default EmptyData;

View File

@@ -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,
};

View File

@@ -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 };

View File

@@ -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,
};

View File

@@ -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 (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fafafa',
textAlign: 'center',
minHeight: '100vh',
padding: isMobile ? '1rem' : '3rem',
boxSizing: 'border-box',
}}
>
<img
src={ImgPIU}
alt="Logo PIU"
style={{
width: isMobile ? '100%' : '100%',
maxWidth: isMobile ? '300px' : '30%',
height: 'auto',
marginBottom: isMobile ? '1.5rem' : '2vh',
marginTop: isMobile ? '-1rem' : '-75px',
}}
/>
{filteredRoles.map((role, index) => (
<div key={index} style={{ width: '100%' }}>
<Title
strong
level={1}
style={{
marginTop: isMobile ? '-2rem' : '-75px',
fontWeight: 600,
color: '#004D80',
textAlign: 'center',
fontSize: isMobile ? '6vw' : '2.5rem',
}}
>
Validasi Dokumen <br /> Pupuk Indonesia Utilitas
</Title>
<Title
level={2}
style={{
marginTop: isMobile ? '1rem' : '-5px',
fontWeight: 600,
color: '#141414',
textAlign: 'center',
fontSize: isMobile ? '6vw' : '2.5rem',
}}
>
{role}
</Title>
<div
style={{
fontSize: isMobile ? '4.5vw' : '25px',
lineHeight: isMobile ? 1.5 : 1.8,
marginTop: isMobile ? '1rem' : '1.5rem',
}}
>
<Text strong style={{ fontSize: isMobile ? '4.5vw' : 25 }}>
Nama:
</Text>{' '}
{selectedName ?? '-'}
<br />
<Text strong style={{ fontSize: isMobile ? '4.5vw' : 25 }}>
{formattedTime}
</Text>
</div>
</div>
))}
</div>
);
}
export default QrPermit;

View File

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

View File

@@ -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 (
<>
<Button
size="middle"
onClick={showModal}
style={{
backgroundColor: '#fff',
color: color,
borderColor: color,
borderRadius: '8px',
padding: '5px 16px',
fontWeight: 500,
...style?.button,
}}
>
{name || 'N/A'}
</Button>
<Modal
title={
<Tag
style={{
backgroundColor: '#fff',
color: color || '#999',
border: `1px solid ${color || '#ddd'}`,
borderRadius: 8,
padding: '4px 12px',
fontSize: 16,
display: 'inline-block',
}}
>
{name ?? 'Belum ada status'}
</Tag>
}
open={isModalVisible}
onCancel={handleCancel}
footer={[
<>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
{props.status_permit === pengajuanPermitSelesai &&
historyData.length >= 0 &&
isVendor && (
<>
<ConfigProvider
theme={{
token: { colorBgContainer: '#23a55ade' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button loading={confirmLoading} onClick={handleSelesai}>
Selesai
</Button>
</ConfigProvider>
</>
)}
{props.status_permit !== 0 &&
props.status_permit !== pengajuanPermitSelesai &&
props.status_permit !== permitSelesai &&
historyData.length >= 0 && (
<>
{canReject && (
<ConfigProvider
theme={{
token: { colorBgContainer: '#FF4D4Fde' },
components: {
Button: {
defaultBg: '#FF4D4F',
defaultColor: '#FFFFFF',
defaultBorderColor: '#FF4D4F',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#FF4D4F',
},
},
}}
>
<Button onClick={handleReject}>Reject</Button>
</ConfigProvider>
)}
{canApprove && (
<ConfigProvider
theme={{
token: { colorBgContainer: '#23a55ade' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button
loading={confirmLoading}
onClick={handleApprove}
>
Approve
</Button>
</ConfigProvider>
)}
</>
)}
</>,
]}
>
<Divider style={{ margin: '16px 0', borderTop: '2px solid #D9D9D9' }} />
{historyData.length > 0 ? (
[...historyData]
.sort((a, b) => new Date(b.date) - new Date(a.date))
.map((item, index) => (
<Card
key={index}
style={{
marginBottom: 16,
color: item.color,
backgroundColor: '#FFFFFF',
border: `1.5px solid ${item.color}`,
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<Tag
style={{
backgroundColor: '#FFFFFF',
color: item.color,
border: `1.5px solid ${item.color}`,
fontSize: 13,
padding: '4px 10px',
borderRadius: 6,
fontWeight: '600',
}}
>
{item.text}
</Tag>
{item.date != null && (
<Text type="secondary" style={{ fontSize: 12 }}>
{dayjs.utc(item.date).format('YYYY-MM-DD HH:mm:ss')}
</Text>
)}
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
}}
>
<div style={{ fontSize: 13, color: '#333' }}>{item.name}</div>
{item.closed && (
<Tag
style={{
backgroundColor: '#f9f9f9',
color: '#000',
border: '1px dashed #999',
fontSize: 12,
fontStyle: 'italic',
marginRight: 0,
}}
>
Closed: {item.closed}
</Tag>
)}
</div>
{item.deskripsi && (
<p style={{ fontSize: 12, color: '#333', margin: 0 }}>
{item.deskripsi}
</p>
)}
</Card>
))
) : (
<Text type="secondary">Belum ada riwayat status.</Text>
)}
</Modal>
<Modal
open={showConfirmModal}
title={actionType === 'approve' ? 'Konfirmasi Persetujuan' : 'Konfirmasi Penolakan'}
onCancel={() => {
setShowConfirmModal(false);
setDeskripsi('');
}}
confirmLoading={confirmLoading}
footer={[
<Button
key="cancel"
onClick={() => {
setShowConfirmModal(false);
setDeskripsi('');
}}
>
Batal
</Button>,
<ConfigProvider
key="action"
theme={{
token: {
colorBgContainer:
actionType === 'approve' ? '#23a55ade' : '#FF4D4Fde',
},
components: {
Button: {
defaultBg: actionType === 'approve' ? '#23a55a' : '#FF4D4F',
defaultColor: '#FFFFFF',
defaultBorderColor:
actionType === 'approve' ? '#23a55a' : '#FF4D4F',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor:
actionType === 'approve' ? '#23a55a' : '#FF4D4F',
},
},
}}
>
<Button key="submit" loading={confirmLoading} onClick={submitApproval}>
{actionType === 'approve' ? 'Approve' : 'Reject'}
</Button>
</ConfigProvider>,
]}
>
<p>Silakan isi deskripsi:</p>
<TextArea
rows={4}
value={deskripsi}
onChange={(e) => setDeskripsi(e.target.value)}
placeholder="Contoh: Disetujui karena dokumen lengkap atau ditolak karena dokumen tidak lengkap."
/>
</Modal>
<Modal
open={showPermitSelesai}
title={'Konfirmasi Pengajuan Selesai'}
onCancel={() => {
setShowPermitSelesai(false);
setCloseType('');
}}
confirmLoading={confirmLoading}
footer={[
<Button
key="cancel"
onClick={() => {
setShowPermitSelesai(false);
setCloseType('');
}}
>
Batal
</Button>,
<ConfigProvider
key="action"
theme={{
token: { colorBgContainer: '#23a55ade' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button key="submit" loading={confirmLoading} onClick={submitSelesai}>
Ajukan Permit Selesai
</Button>
</ConfigProvider>,
]}
>
<p>Status Permit saat ini :</p>
<Radio.Group onChange={(e) => setCloseType(e.target.value)} value={closeType}>
<Radio value="0">Belum Selesai</Radio>
<Radio value="1">Selesai</Radio>
</Radio.Group>
</Modal>
</>
);
};
export default StatusButton;

View File

@@ -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 (
<>
<Button
size="middle"
onClick={showModal}
style={{
backgroundColor: '#fff',
color: statusColor,
borderColor: statusColor,
borderRadius: '8px',
padding: '4px 16px',
fontWeight: 500,
...style?.button,
}}
>
{statusName}
</Button>
<Modal
title={
<Tag
style={{
backgroundColor: '#fff',
color: statusColor,
border: `1px solid ${statusColor}`,
borderRadius: 8,
padding: '4px 12px',
fontSize: 16,
display: 'inline-block',
}}
>
{statusName}
</Tag>
}
open={isModalVisible}
onCancel={handleCancel}
footer={[
<>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
{data?.status_register === 1 && (
<>
<ConfigProvider
theme={{
token: {
colorBgContainer: '#FF4D4Fde',
},
components: {
Button: {
defaultBg: '#FF4D4F',
defaultColor: '#FFFFFF',
defaultBorderColor: '#FF4D4F',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#FF4D4F',
},
},
}}
>
<Button onClick={handleReject}>Reject</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: {
colorBgContainer: '#23a55ade',
},
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button loading={confirmLoading} onClick={handleApprove}>
Approve
</Button>
</ConfigProvider>
</>
)}
</>,
]}
>
<Divider style={{ margin: '16px 0', borderTop: '2px solid #D9D9D9' }} />
{data ? (
<>
{data.updated_at !== data.created_at && (
<Card
style={{
marginBottom: 16,
color: '#FF6E35',
backgroundColor: '#FFFFFF',
border: `1.5px solid #FF6E35`,
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<Tag
style={{
backgroundColor: '#FFFFFF',
color: '#FF6E35',
border: `1.5px solid #FF6E35`,
fontSize: 13,
padding: '4px 10px',
borderRadius: 6,
fontWeight: '500',
}}
>
Updated at
</Tag>
<Text
type="secondary"
style={{ fontSize: 12, fontWeight: '500' }}
>
{toAppDateFormatter(data.updated_at)}
</Text>
</div>
<div style={{ fontSize: 13, color: '#333' }}>
Diubah terakhir oleh <b>{userUpdated}</b>
</div>
</Card>
)}
<Card
style={{
marginBottom: 16,
color: '#3498DB',
backgroundColor: '#FFFFFF',
border: `1.5px solid #3498DB`,
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<Tag
style={{
backgroundColor: '#FFFFFF',
color: '#3498DB',
border: `1.5px solid #3498DB`,
fontSize: 13,
padding: '4px 10px',
borderRadius: 6,
fontWeight: '500',
}}
>
Created at
</Tag>
<Text type="secondary" style={{ fontSize: 12, fontWeight: '500' }}>
{toAppDateFormatter(data.created_at)}
</Text>
</div>
<div style={{ fontSize: 13, color: '#333' }}>
Dibuat oleh <b>{userCreated}</b>
</div>
</Card>
</>
) : (
<Text type="secondary">Belum ada riwayat status.</Text>
)}
</Modal>
</>
);
};
export default StatusUserButton;

View File

@@ -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 (
<div>
{isMobile && mobile ? (
<Row gutter={24}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{data.map((item) => (
<Card
key={item.id}
title={
(mobile.r1 || mobile.r2) && (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{mobile.r1 && (
<span style={mobile.r1.style ?? {}}>
{item[mobile.r1.name] ?? mobile.r1.text ?? ''}
</span>
)}
{mobile.r2 && (
<Tag
color={mobile.r2.color ?? ''}
style={mobile.r2.style ?? {}}
>
{item[mobile.r2.name] ?? mobile.r2.text ?? ''}
</Tag>
)}
</div>
)
}
style={{ width: '100%' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{mobile.r3 && mobile.r4 && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
justifyContent: 'space-between',
}}
>
<Text
type={mobile.r3 ? mobile.r3.type ?? 'primary' : ''}
style={mobile.r3 ? mobile.r3.style ?? {} : {}}
>
{item[mobile.r3 ? mobile.r3.name : ''] ?? ''}
</Text>
<Text style={mobile.r4 ? mobile.r4.style ?? {} : {}}>
{item[mobile.r4 ? mobile.r4.name : ''] ?? ''}
</Text>
</div>
)}
{mobile.r5 && (
<Text
type={mobile.r5 ? mobile.r5.type ?? 'secondary' : ''}
style={mobile.r5 ? mobile.r5.style ?? {} : {}}
>
{item[mobile.r5 ? mobile.r5.name : ''] ?? ''}
</Text>
)}
</div>
{mobile.r6 && (
<div>
<Text
type={mobile.r6 ? mobile.r6.type ?? 'primary' : ''}
style={mobile.r6 ? mobile.r6.style ?? {} : {}}
>
{item[mobile.r6 ? mobile.r6.name : ''] ?? ''}
</Text>
</div>
)}
<div
style={{
marginTop: 16,
borderTop: '1px solid #f0f0f0',
paddingTop: 8,
textAlign: 'right',
}}
>
<Button
type="primary"
size="small"
shape="round"
icon={<EyeOutlined />}
onClick={(e) => {
mobile.action(item);
}}
>
Detail
</Button>
</div>
</Card>
))}
</div>
</Row>
) : (
<Row gutter={24}>
{/* TABLE */}
<Table
rowSelection={rowSelection || null}
columns={columns}
dataSource={data.map((item, index) => ({ ...item, key: index }))}
pagination={false}
loading={gridLoading}
scroll={{
y: 520,
x: 1300,
}}
/>
{/* PAGINATION */}
<Col xs={24} style={{ marginTop: '16px' }}>
<Row justify="space-between" align="middle">
<Col>
<div>
Menampilkan {pagingResponse.totalData} Data dari{' '}
{pagingResponse.perPage} Halaman
</div>
</Col>
<Col>
<Pagination
showSizeChanger
onChange={handlePaginationChange}
onShowSizeChange={handlePaginationChange}
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
/>
</Col>
</Row>
</Col>
</Row>
)}
</div>
);
});
export default TableList;

View File

@@ -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 };

View File

@@ -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(
<>
<Row style={{ justifyContent: "space-between" }}>
<Col>
<Row>
<Text style={{fontSize:'18px'}} strong>{title}</Text>
<div style={{width:'20px'}}></div>
<div style={{
width:'95px',
border:'1px solid #e9f6ef',
borderRadius:'5px',
backgroundColor:'#e9f6ef',
color:'#23a55a',
padding:'3px',
fontSize:'16px',
fontWeight:'bold'
}}>
{step}
</div>
</Row>
</Col>
<Col>
<ConfigProvider
theme={{
token: {
colorBgContainer: "#eff0f5",
},
components: {
Button: {
defaultBg: "white",
defaultColor: "#000000",
defaultBorderColor: "#000000",
defaultHoverColor: "#000000",
defaultHoverBorderColor: "#000000",
defaultHoverColor: "#000000",
},
},
}}
>
<Button icon={<ArrowLeftOutlined />}>Batal</Button>
</ConfigProvider>
</Col>
</Row>
<Divider/>
<Row style={{ justifyContent: "space-between" }}>
<Col>
<ConfigProvider
theme={{
token: {
colorBgContainer: "#209652",
},
components: {
Button: {
defaultBg: "#23a55a",
defaultColor: "#FFFFFF",
defaultBorderColor: "#23a55a",
defaultHoverColor: "#FFFFFF",
defaultHoverBorderColor: "#209652",
},
},
}}
>
<Button loading={loadingPilihDataStep1} onClick={handleSelectData}><SearchOutlined /> Pilih Data</Button>
</ConfigProvider>
</Col>
</Row>
</>
);
};
export default HeaderReport;

View File

@@ -0,0 +1,28 @@
import React, { memo } from 'react'
import './loading.css'
const Loading = memo(function Loading() {
return(
<div className='mask'>
<div className="main">
<div className="loader">
<p className="text">
<span className="letter letter1">L</span>
<span className="letter letter2">o</span>
<span className="letter letter3">a</span>
<span className="letter letter4">d</span>
<span className="letter letter5">i</span>
<span className="letter letter6">n</span>
<span className="letter letter7">g</span>
<span className="letter letter8">.</span>
<span className="letter letter9">.</span>
<span className="letter letter10">.</span>
</p>
</div>
</div>
</div>
);
})
export default Loading

View File

@@ -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;
}