lavoce #1
64
src/components/Common/BasicButton.jsx
Normal file
64
src/components/Common/BasicButton.jsx
Normal 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;
|
||||||
21
src/components/Common/BasicInput.jsx
Normal file
21
src/components/Common/BasicInput.jsx
Normal 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;
|
||||||
58
src/components/Common/NavBar.jsx
Normal file
58
src/components/Common/NavBar.jsx
Normal 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;
|
||||||
20
src/components/Common/Version.jsx
Normal file
20
src/components/Common/Version.jsx
Normal 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;
|
||||||
127
src/components/Global/ApiRequest.jsx
Normal file
127
src/components/Global/ApiRequest.jsx
Normal 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 };
|
||||||
7
src/components/Global/DataFilter.jsx
Normal file
7
src/components/Global/DataFilter.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
let filterData = [];
|
||||||
|
|
||||||
|
export const setFilterData = (data) => {
|
||||||
|
filterData = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilterData = () => filterData;
|
||||||
24
src/components/Global/EmptyData.jsx
Normal file
24
src/components/Global/EmptyData.jsx
Normal 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;
|
||||||
192
src/components/Global/Formatter.jsx
Normal file
192
src/components/Global/Formatter.jsx
Normal 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,
|
||||||
|
};
|
||||||
23
src/components/Global/KopReport.jsx
Normal file
23
src/components/Global/KopReport.jsx
Normal 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 };
|
||||||
72
src/components/Global/MqttConnection.jsx
Normal file
72
src/components/Global/MqttConnection.jsx
Normal 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,
|
||||||
|
};
|
||||||
109
src/components/Global/QrPermit.jsx
Normal file
109
src/components/Global/QrPermit.jsx
Normal 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;
|
||||||
25
src/components/Global/RegisterRequest.jsx
Normal file
25
src/components/Global/RegisterRequest.jsx
Normal 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;
|
||||||
499
src/components/Global/StatusButton.jsx
Normal file
499
src/components/Global/StatusButton.jsx
Normal 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;
|
||||||
294
src/components/Global/StatusUserButton.jsx
Normal file
294
src/components/Global/StatusUserButton.jsx
Normal 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;
|
||||||
281
src/components/Global/TableList.jsx
Normal file
281
src/components/Global/TableList.jsx
Normal 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;
|
||||||
69
src/components/Global/ToastNotif.jsx
Normal file
69
src/components/Global/ToastNotif.jsx
Normal 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 };
|
||||||
81
src/components/Global/headerReport.jsx
Normal file
81
src/components/Global/headerReport.jsx
Normal 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;
|
||||||
28
src/components/loading/Loading.jsx
Normal file
28
src/components/loading/Loading.jsx
Normal 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
|
||||||
80
src/components/loading/loading.css
Normal file
80
src/components/loading/loading.css
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user