Commit f9a9c329 authored by ardiansyah's avatar ardiansyah

Merge branch 'ENV-STAGING' into 'ENV-PROD'

Env staging

See merge request !2394
parents 2ec2a53c 489898e5
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tia-dev.iml" filepath="$PROJECT_DIR$/.idea/tia-dev.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
\ No newline at end of file
...@@ -29,6 +29,24 @@ const create = (type = "") => { ...@@ -29,6 +29,24 @@ const create = (type = "") => {
timeout: 300000 timeout: 300000
}) })
break; break;
case 'DOWNLOAD':
api = apisauce.create({
// base URL is read from the "constructor"
baseURL,
// here are some default headers
headers: {
'Cache-Control': 'no-cache',
Accept: 'application/json',
'Content-Type': 'application/json',
},
responseType: 'blob',
// 3 mins timeout...
// timeout: 180000
// 5 mins timeout...
timeout: 300000
})
break;
case 'UPLOAD': case 'UPLOAD':
api = apisauce.create({ api = apisauce.create({
// base URL is read from the "constructor" // base URL is read from the "constructor"
...@@ -69,6 +87,16 @@ const create = (type = "") => { ...@@ -69,6 +87,16 @@ const create = (type = "") => {
// console.tron.log(url) // console.tron.log(url)
}) })
api.addResponseTransform(response => {
const msg = response.data?.message || ""
if (msg.includes("Someone Logged In") || msg.includes("Token Expired")) {
localStorage.removeItem(Constant.TOKEN)
window.location.reload()
return
}
})
// ------ // ------
// STEP 2 // STEP 2
// ------ // ------
...@@ -561,6 +589,9 @@ const create = (type = "") => { ...@@ -561,6 +589,9 @@ const create = (type = "") => {
const validateSaveLOCF = (body) => api.post('transaction/locf/monthly_report/validate_save', body) const validateSaveLOCF = (body) => api.post('transaction/locf/monthly_report/validate_save', body)
const createMonthlyReportLOV = (body) => api.post('transaction/lov/monthly_report/save_monthly_report', body) const createMonthlyReportLOV = (body) => api.post('transaction/lov/monthly_report/save_monthly_report', body)
// Historical
const getHistoricalReport = () => api.get('transaction/historical/get/report')
const exportReportHistorial = (body) => api.get(`transaction/historical/export_report?report_id=${body.report_id}&company_id=${body.company_id}&year=${body.year}&month=${body.month}&performance_period=${body.performance_period}`)
// Superadmin Approve // Superadmin Approve
const getListApprover = (report, monthlyReportId) => api.get(`transaction/${report}/get_approver/${monthlyReportId}`) const getListApprover = (report, monthlyReportId) => api.get(`transaction/${report}/get_approver/${monthlyReportId}`)
...@@ -972,8 +1003,9 @@ const create = (type = "") => { ...@@ -972,8 +1003,9 @@ const create = (type = "") => {
getTypeOfInvestment, getTypeOfInvestment,
validateSaveLOCF, validateSaveLOCF,
validateSaveLOV, validateSaveLOV,
createMonthlyReportLOV createMonthlyReportLOV,
getHistoricalReport,
exportReportHistorial
} }
} }
......
import React, { Component } from 'react'
import { Paper, } from '@material-ui/core';
import AlertSnackbar from '../../library/AlertSnackbar'
import SectionHeader from '../../library/SectionHeader';
import AutocompleteField from '../../library/AutocompleteField';
import CustomButton from '../../library/CustomButton';
import api from '../../api';
import DDLYear from '../../library/Dropdown/DDLYear';
import DDLMonth from '../../library/Dropdown/DDLMonth';
import DDLCompany from '../../library/Dropdown/DDLCompany';
import ContentContainer from '../../library/ContentContainer';
import Constant from '../../library/Constant';
import { downloadFileBlob } from '../../library/Utils';
class ReportHistorical extends Component {
constructor(props) {
super(props)
this.state = {
showAlert: false,
alertMessage: '',
alertSeverity: Constant.ALERT_SEVIRITY.SUCCESS,
selectedValue: null,
data: {},
isLoading: false,
isLoadingReportType: false,
listReportType: [],
listPeriodType: [
{ id: 'MTD', name: 'MTD', },
{ id: 'YTD', name: 'YTD', }
],
buttonError: false,
}
}
componentDidMount() {
this.getData()
}
setLoading = (isLoading) => {
this.setState({ isLoading })
}
setLoadingReportType = (isLoadingReportType) => {
this.setState({ isLoadingReportType })
}
getData() {
this.setLoadingReportType(true)
api.create().getHistoricalReport().then((res) => {
const list = res.data?.data || []
const arr = []
list.forEach(item => {
arr.push({ id: item.report_type_id, name: item.report_name })
})
this.setState({ listReportType: arr }, () => {
this.setDefaultData()
this.setLoadingReportType(false)
})
})
}
setDefaultData = () => {
const defaultReportType = this.state.listReportType[0] || null
const defaultPeriodType = this.state.listPeriodType[0] || null
this.setState({
data: {
...this.state.data,
report_id: defaultReportType,
performance_period: defaultPeriodType,
}
}, () => {
this.setLoading(false)
})
}
handleChangeDropdown = (newValue, name) => {
this.setState(prevState => ({
data: {
...prevState.data,
[name]: newValue,
}
}))
}
handleChangeMultiDropdown = (newValues, name) => {
this.setState(prevState => ({
data: {
...prevState.data,
[name]: newValues,
}
}))
}
showAlert = (message, severity = Constant.ALERT_SEVIRITY.SUCCESS) => {
this.setState({
showAlert: true,
alertMessage: message,
alertSeverity: severity,
});
};
closeAlert = () => {
this.setState({ showAlert: false });
};
handleDownload = async () => {
try {
const { data } = this.state
if (!data?.company_id) {
this.showAlert('Data is not complete !', Constant.ALERT_SEVIRITY.WARNING);
return
}
this.setLoading(true)
const payload = {
report_id: data.report_id?.id,
company_id: data.company_id?.map(c => c.id).join(','),
performance_period: data.performance_period?.id,
year: data.year?.id,
month: data.month?.id,
}
api.create('DOWNLOAD').exportReportHistorial(payload).then(async (res) => {
this.setLoading(false)
const blob = res.data
if (blob && blob.size > 0) {
const fileName = `Report_${Constant.PERIOD_TYPE[data.performance_period?.id]}_${data.report_id?.name}_${data.month?.name}_${data.year?.name}.xlsx`
downloadFileBlob(fileName, blob)
}
this.showAlert('Download Berhasil');
})
} catch (error) {
// Show error alert
this.showAlert(`Gagal menyimpan: ${error.message}`, Constant.ALERT_SEVIRITY.ERROR);
}
};
render() {
const { data, showAlert, alertMessage, alertSeverity, listReportType, listPeriodType, isLoading, isLoadingReportType } = this.state;
const contentStyle = { display: 'flex', marginTop: 10, gap: '20px' };
return (
<ContentContainer isLoading={isLoading} title="Report Historical">
<div style={{ padding: 20 }}>
<Paper style={{ paddingTop: 10, paddingBottom: 20 }}>
<SectionHeader
title="Report Historical"
/>
<div style={{ padding: '20px 20px 0px 20px' }}>
<div style={contentStyle}>
<AutocompleteField
options={listReportType}
value={data?.report_id}
onChange={(event, newValue) => this.handleChangeDropdown(newValue, 'report_id')}
label="Report Type"
isLoading={isLoadingReportType}
/>
<AutocompleteField
options={listPeriodType}
value={data?.performance_period}
onChange={(event, newValue) => this.handleChangeDropdown(newValue, 'performance_period')}
label="Period Type"
/>
</div>
<div style={contentStyle}>
<DDLMonth
value={data?.month}
name="month"
useCurrentMonthAsDefault={true}
onChange={(event, newValue, name) => this.handleChangeDropdown(newValue, name)}
/>
<DDLYear
value={data?.year}
name="year"
useCurrentYearAsDefault={true}
onChange={(event, newValue, name) => this.handleChangeDropdown(newValue, name)}
/>
</div>
<DDLCompany
multiple
value={data?.company_id}
name={"company_id"}
onChange={(event, newValue, name) => this.handleChangeDropdown(newValue, name)}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', paddingLeft: 20, paddingRight: 20, marginTop: 20 }}>
<CustomButton
disabled={this.state.buttonError}
onClick={this.handleDownload}
>
Download
</CustomButton>
</div>
</Paper>
</div>
<AlertSnackbar
open={showAlert}
message={alertMessage}
severity={alertSeverity}
onClose={this.closeAlert}
/>
</ContentContainer>
)
}
}
export default ReportHistorical
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import MuiAlert from '@material-ui/lab/Alert';
const Alert = withStyles({})((props) => (
<MuiAlert elevation={6} variant="filled" {...props} />
));
export default Alert;
\ No newline at end of file
import React, { Component } from 'react';
import { Snackbar } from '@material-ui/core';
import Alert from './Alert';
import Constant from './Constant';
class AlertSnackbar extends Component {
state = {
open: false,
message: '',
severity: Constant.ALERT_SEVIRITY.SUCCESS,
};
componentDidMount() {
const { open, message, severity } = this.props;
if (open !== undefined) {
this.setState({ open });
}
if (message) {
this.setState({ message });
}
if (severity) {
this.setState({ severity });
}
}
componentDidUpdate(prevProps) {
const { open, message, severity } = this.props;
if (prevProps.open !== open && open !== undefined) {
this.setState({ open });
}
if (prevProps.message !== message && message) {
this.setState({ message, open: true });
}
if (prevProps.severity !== severity) {
this.setState({ severity });
}
}
handleClose = (event, reason) => {
const { onClose, disableClickawayClose = false } = this.props;
if (disableClickawayClose && reason === 'clickaway') {
return;
}
this.setState({ open: false });
if (onClose) {
onClose(event, reason);
}
};
render() {
const {
// Props untuk custom content
children,
// Props untuk Snackbar
autoHideDuration = 2000,
anchorOrigin = { vertical: 'bottom', horizontal: 'center' },
// Props untuk Alert
action,
icon,
onClose: propOnClose,
// Lainnya
...snackbarProps
} = this.props;
const { open, message, severity } = this.state;
return (
<Snackbar
open={open}
autoHideDuration={autoHideDuration}
onClose={this.handleClose}
anchorOrigin={anchorOrigin}
{...snackbarProps}
>
<Alert
onClose={this.handleClose}
severity={severity}
action={action}
icon={icon}
>
{children || message}
</Alert>
</Snackbar>
);
}
}
export default AlertSnackbar;
\ No newline at end of file
import React from 'react';
import { TextField, Checkbox } from '@material-ui/core';
import { Autocomplete } from '@material-ui/lab';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CircularProgress from '@material-ui/core/CircularProgress';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const AutocompleteField = ({
options = [],
value,
onChange,
label,
id = "autocomplete-field",
name,
style = { width: 250 },
margin = 'normal',
disabled = false,
required = false,
error = false,
helperText,
disableClearable = true,
renderInput,
noOptionsText = "No options available",
multiple = false,
showCheckbox = false,
isLoading = false,
...props
}) => {
const defaultRenderInput = (params) => (
<TextField
{...params}
label={label}
margin={margin}
style={{ marginTop: 7 }}
disabled={disabled}
required={required}
error={error}
helperText={helperText}
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
);
// Custom renderOption untuk checkbox
const renderOptionWithCheckbox = (option, state) => {
// Manually check if option is selected
const isSelected = multiple && Array.isArray(value)
? value.some(v => v.id === option.id)
: false;
return (
<div
style={{
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '6px 10px',
cursor: 'pointer',
}}
>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={isSelected}
style={{ marginRight: 8 }}
color="primary"
/>
{option.name}
</div>
);
};
return (
<Autocomplete
multiple={multiple}
options={options}
onChange={onChange}
value={multiple ? (value || []) : (value || null)}
id={id}
disableClearable={disableClearable}
disableCloseOnSelect={multiple}
style={style}
disabled={disabled}
renderInput={renderInput || defaultRenderInput}
getOptionLabel={(option) => option?.name || ""}
noOptionsText={noOptionsText}
isOptionEqualToValue={(option, value) => {
if (!option || !value) return false;
return option.id === value.id;
}}
renderOption={showCheckbox && multiple ? renderOptionWithCheckbox : undefined}
loadingText="Loading..."
{...props}
/>
);
};
export default AutocompleteField;
\ No newline at end of file
...@@ -12,9 +12,23 @@ const Constant = { ...@@ -12,9 +12,23 @@ const Constant = {
URL_FE_STAGING: 'https://tia.eksad.com/tia-web-staging', URL_FE_STAGING: 'https://tia.eksad.com/tia-web-staging',
URL_FE_DEMO: 'https://tia.eksad.com/tia-web-demo', URL_FE_DEMO: 'https://tia.eksad.com/tia-web-demo',
URL_FE_PROD: 'https://dashboard.triputra-group.com/tia-web', URL_FE_PROD: 'https://dashboard.triputra-group.com/tia-web',
DATACAT: 'datacat' DATACAT: 'datacat',
// URL_BE_MAIN : Constant.URL_BE_DEV, // URL_BE_MAIN : Constant.URL_BE_DEV,
// URL_FE_MAIN : Constant.URL_FE_DEV, // URL_FE_MAIN : Constant.URL_FE_DEV,
ALERT_SEVIRITY : {
SUCCESS: 'success',
ERROR: 'error',
WARNING: 'warning',
},
PERIOD_TYPE : {
MTD: 'Monthly Historical',
YTD: 'Annual Historical',
},
COLORS_TEXT_COLUMNS: {
active: '#5198ea',
disabled: '#000000de',
}
} }
......
import React, { Component } from 'react';
import OverlayLoader from './OverlayLoader';
import Header from './Header';
class ContentContainer extends Component {
render() {
const {
children,
isLoading = false,
backgroundColor = '#f8f8f8',
flex = 1,
padding = 0,
style = {},
loaderProps = {},
title = '',
...restProps
} = this.props;
const containerStyle = {
flex: flex,
backgroundColor: backgroundColor,
padding: padding,
minHeight: '100px', // minimal height agar loader visible
...style,
};
return (
<div style={containerStyle} {...restProps}>
{isLoading && (
<OverlayLoader isLoading={isLoading} {...loaderProps} />
)}
{title && <Header title={title} />}
{children}
</div>
);
}
}
export default ContentContainer;
\ No newline at end of file
import React from 'react';
import { Typography } from '@material-ui/core';
const CustomButton = ({
children,
onClick,
disabled = false,
type = 'button',
width = 100,
height = 25,
backgroundColor = '#354960',
textColor = '#fff',
fontSize = '11px',
borderRadius = 3,
// Styling props
buttonStyle = {},
textStyle = {},
wrapperStyle = {},
// Icon props
startIcon,
endIcon,
// Variants
variant = 'contained', // 'contained' | 'outlined' | 'text'
...props
}) => {
// Variant styling
const getVariantStyles = () => {
switch (variant) {
case 'outlined':
return {
backgroundColor: 'transparent',
border: `1px solid ${backgroundColor}`,
};
case 'text':
return {
backgroundColor: 'transparent',
borderColor: 'transparent',
};
default: // contained
return {
backgroundColor,
borderColor: backgroundColor,
};
}
};
const getTextColor = () => {
if (variant === 'outlined' || variant === 'text') {
return backgroundColor;
}
return textColor;
};
return (
<button
type={type}
disabled={disabled}
onClick={onClick}
style={{
backgroundColor: 'transparent',
borderColor: 'transparent',
outline: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
padding: 0,
...buttonStyle,
}}
{...props}
>
<div
style={{
backgroundColor: getVariantStyles().backgroundColor,
border: getVariantStyles().border || 'none',
width,
height,
borderRadius,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
opacity: disabled ? 0.5 : 1,
...wrapperStyle,
}}
>
{startIcon && (
<span style={{ marginRight: 4, display: 'flex', alignItems: 'center' }}>
{startIcon}
</span>
)}
<Typography
style={{
fontSize,
color: getTextColor(),
textAlign: 'center',
...textStyle,
}}
>
{children}
</Typography>
{endIcon && (
<span style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
{endIcon}
</span>
)}
</div>
</button>
);
};
export default CustomButton;
\ No newline at end of file
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import AutocompleteField from '../AutocompleteField';
import api from '../../api';
class DDLCompany extends Component {
state = {
companies: [],
selectedValue: this.props.multiple ? [] : null,
isLoading: false,
};
componentDidMount() {
this.getCompanyActive();
}
componentDidUpdate(prevProps, prevState) {
// 🔁 value dari parent berubah ATAU companies selesai load
if (
prevProps.value !== this.props.value ||
prevState.companies !== this.state.companies
) {
this.syncValueWithCompanies(this.props.value);
}
}
setLoading = (isLoading) => {
this.setState({ isLoading });
};
getCompanyActive = async () => {
try {
this.setLoading(true);
const res = await api.create().getPerusahaanActive();
const data = res?.data?.data || [];
const companies = data.map(item => ({
id: String(item.company_id),
name: item.company_name,
}));
this.setState({ companies }, () => {
// ⭐ optional auto select index 0
if (
this.props.autoSelectFirst &&
!this.props.value &&
companies.length > 0
) {
this.handleChange(null, this.props.multiple ? [companies[0]] : companies[0]);
}
});
} catch (e) {
console.error(e);
this.setState({ companies: [] });
} finally {
this.setLoading(false);
}
};
// 🔑 SINKRON VALUE ↔ OPTIONS (INI KUNCI UTAMA)
syncValueWithCompanies = (value) => {
const { companies } = this.state;
const { multiple } = this.props;
if (!value || companies.length === 0) return;
if (multiple) {
const synced = value
.map(v => companies.find(c => c.id === String(v.id)))
.filter(Boolean);
this.setState({ selectedValue: synced });
} else {
const matched = companies.find(c => c.id === String(value.id));
this.setState({ selectedValue: matched || null });
}
};
// 🔁 SELALU KIRIM BALIK KE PARENT
handleChange = (event, newValue) => {
const { onChange, onCompanyChange, name, multiple } = this.props;
this.setState({ selectedValue: newValue });
if (onChange) {
onChange(event, newValue, name);
}
if (onCompanyChange) {
if (multiple) {
onCompanyChange(newValue.map(v => v.id));
} else {
onCompanyChange(newValue ? newValue.id : null);
}
}
};
render() {
const {
label,
placeholder,
disabled,
required,
error,
helperText,
style,
margin,
multiple,
} = this.props;
const { companies, selectedValue, isLoading } = this.state;
return (
<AutocompleteField
options={companies}
value={selectedValue}
onChange={this.handleChange}
label={label}
placeholder={placeholder}
disabled={disabled}
required={required}
error={error}
helperText={helperText}
style={style}
margin={margin}
multiple={multiple}
showCheckbox={multiple}
loading={isLoading}
/>
);
}
}
DDLCompany.propTypes = {
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onChange: PropTypes.func,
onCompanyChange: PropTypes.func,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
style: PropTypes.object,
margin: PropTypes.string,
disabled: PropTypes.bool,
required: PropTypes.bool,
error: PropTypes.bool,
helperText: PropTypes.string,
multiple: PropTypes.bool,
autoSelectFirst: PropTypes.bool,
};
DDLCompany.defaultProps = {
label: 'Company',
placeholder: 'Select Company',
style: { width: 250 },
margin: 'normal',
multiple: false,
autoSelectFirst: false,
};
export default DDLCompany;
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import AutocompleteField from '../AutocompleteField';
class DDLMonth extends Component {
constructor(props) {
super(props);
this.state = {
selectedValue: props.value || null,
months: [],
};
}
componentDidMount() {
this.generateMonthOptions();
// Set default value if enabled and no value provided
if (this.props.useCurrentMonthAsDefault && !this.props.value) {
setTimeout(() => this.setDefaultMonth(), 0);
} else if (this.props.value) {
setTimeout(() => this.syncValueWithMonths(this.props.value), 0);
}
}
componentDidUpdate(prevProps, prevState) {
// Update if value prop changes
if (prevProps.value !== this.props.value) {
this.syncValueWithMonths(this.props.value);
}
// Update if month format or locale changes
if (prevProps.monthFormat !== this.props.monthFormat ||
prevProps.locale !== this.props.locale ||
prevProps.disabledMonths !== this.props.disabledMonths) {
this.generateMonthOptions();
}
// Sync value after months regenerated
if (prevState.months !== this.state.months) {
if (this.props.value) {
this.syncValueWithMonths(this.props.value);
} else if (this.props.useCurrentMonthAsDefault) {
this.setDefaultMonth();
}
}
}
// Set default month (current month)
setDefaultMonth = () => {
const { months } = this.state;
const { onChange, name } = this.props;
if (months.length === 0) return;
const currentMonth = new Date().getMonth() + 1; // 1-12
const defaultMonth = months.find(m => m.id === currentMonth);
if (defaultMonth) {
this.setState({ selectedValue: defaultMonth });
// Notify parent component
if (onChange) {
onChange(null, defaultMonth, name);
}
}
};
// Sync value dengan months array untuk dapat referensi yang sama
syncValueWithMonths = (value) => {
if (!value) {
this.setState({ selectedValue: null });
return;
}
const { months } = this.state;
// Cari objek yang id-nya sama dari months array
const matchedMonth = months.find(m => m.id === value.id);
if (matchedMonth) {
// Set dengan objek dari months array (referensi yang sama)
this.setState({ selectedValue: matchedMonth });
} else {
// Fallback: set value as-is
this.setState({ selectedValue: value });
}
};
generateMonthOptions = () => {
const {
monthFormat = 'short',
locale = 'en-US',
startMonth = 1,
showAllMonths = true,
filterMonths,
} = this.props;
const months = [];
const date = new Date();
const totalMonths = showAllMonths ? 12 : (12 - startMonth + 1);
for (let i = 0; i < totalMonths; i++) {
const monthNumber = startMonth + i;
if (monthNumber > 12) break;
date.setMonth(monthNumber - 1);
let monthName;
switch (monthFormat) {
case 'full':
monthName = date.toLocaleString(locale, { month: 'long' });
break;
case 'numeric':
monthName = String(monthNumber).padStart(2, '0');
break;
case 'short':
default:
monthName = date.toLocaleString(locale, { month: 'short' });
}
months.push({
id: monthNumber,
name: monthName,
});
}
const filteredMonths = filterMonths ? months.filter(filterMonths) : months;
this.setState({ months: filteredMonths });
};
handleChange = (event, newValue) => {
const { onChange, name, onMonthChange } = this.props;
this.setState({ selectedValue: newValue });
if (onChange) {
onChange(event, newValue, name);
}
if (onMonthChange) {
onMonthChange(newValue ? newValue.id : null);
}
};
getMonthName = (monthNumber, format = 'short') => {
const date = new Date();
date.setMonth(monthNumber - 1);
switch (format) {
case 'full':
return date.toLocaleString(this.props.locale, { month: 'long' });
case 'numeric':
return String(monthNumber).padStart(2, '0');
default:
return date.toLocaleString(this.props.locale, { month: 'short' });
}
};
getCurrentMonth = () => {
return new Date().getMonth() + 1;
};
getSelectedMonthValue = () => {
const { selectedValue } = this.state;
return selectedValue ? selectedValue.id : null;
};
getSelectedMonthName = () => {
const { selectedValue } = this.state;
return selectedValue ? selectedValue.name : null;
};
render() {
const {
label = 'Month',
placeholder = 'Select month',
disabled = false,
required = false,
error = false,
helperText,
style = { width: 250 },
margin = 'normal',
} = this.props;
const { selectedValue, months } = this.state;
return (
<AutocompleteField
options={months}
value={selectedValue}
onChange={this.handleChange}
label={label}
placeholder={placeholder}
disabled={disabled}
required={required}
error={error}
helperText={helperText}
style={style}
margin={margin}
/>
);
}
}
DDLMonth.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
onMonthChange: PropTypes.func,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
style: PropTypes.object,
margin: PropTypes.string,
disabled: PropTypes.bool,
required: PropTypes.bool,
error: PropTypes.bool,
helperText: PropTypes.string,
monthFormat: PropTypes.oneOf(['full', 'short', 'numeric']),
locale: PropTypes.string,
startMonth: PropTypes.number,
showAllMonths: PropTypes.bool,
disabledMonths: PropTypes.arrayOf(PropTypes.number),
filterMonths: PropTypes.func,
useCurrentMonthAsDefault: PropTypes.bool, // NEW: Enable default current month
};
DDLMonth.defaultProps = {
label: 'Month',
placeholder: 'Select month',
monthFormat: 'short',
locale: 'en-US',
startMonth: 1,
showAllMonths: true,
style: { width: 250 },
useCurrentMonthAsDefault: false, // NEW: Default is false (opt-in)
};
export default DDLMonth;
\ No newline at end of file
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import AutocompleteField from '../AutocompleteField';
class DDLYear extends Component {
constructor(props) {
super(props);
this.state = {
selectedValue: props.value || null,
years: [],
};
}
componentDidMount() {
this.generateYearOptions();
// Set default value if enabled and no value provided
if (this.props.useCurrentYearAsDefault && !this.props.value) {
setTimeout(() => this.setDefaultYear(), 0);
} else if (this.props.value) {
setTimeout(() => this.syncValueWithYears(this.props.value), 0);
}
}
componentDidUpdate(prevProps, prevState) {
// Update if value prop changes
if (prevProps.value !== this.props.value) {
this.syncValueWithYears(this.props.value);
}
// Update if year range props change
if (prevProps.startYear !== this.props.startYear ||
prevProps.endYear !== this.props.endYear ||
prevProps.showDescending !== this.props.showDescending ||
prevProps.filterYears !== this.props.filterYears) {
this.generateYearOptions();
}
// Sync value after years regenerated
if (prevState.years !== this.state.years) {
if (this.props.value) {
this.syncValueWithYears(this.props.value);
} else if (this.props.useCurrentYearAsDefault) {
this.setDefaultYear();
}
}
}
// Set default year (current year)
setDefaultYear = () => {
const { years } = this.state;
const { onChange, name } = this.props;
if (years.length === 0) return;
const currentYear = new Date().getFullYear();
const defaultYear = years.find(y => Number(y.id) === currentYear);
if (defaultYear) {
this.setState({ selectedValue: defaultYear });
// Notify parent component
if (onChange) {
onChange(null, defaultYear, name);
}
}
};
// Sync value dengan years array untuk dapat referensi yang sama
syncValueWithYears = (value) => {
if (!value) {
this.setState({ selectedValue: null });
return;
}
const { years } = this.state;
// Cari objek yang id-nya sama dari years array
const matchedYear = years.find(y => y.id === value.id || String(y.id) === String(value.id));
if (matchedYear) {
// Set dengan objek dari years array (referensi yang sama)
this.setState({ selectedValue: matchedYear });
} else {
// Fallback: set value as-is
this.setState({ selectedValue: value });
}
};
generateYearOptions = () => {
const {
startYear = 2000,
endYear,
showDescending = false,
filterYears,
} = this.props;
const currentYear = new Date().getFullYear();
const actualEndYear = endYear !== undefined ? endYear : currentYear;
// Validate year range
if (startYear > actualEndYear) {
console.warn(`startYear (${startYear}) cannot be greater than endYear (${actualEndYear})`);
this.setState({ years: [] });
return;
}
// Generate years array
let years = [];
for (let year = startYear; year <= actualEndYear; year++) {
years.push({
id: String(year),
name: String(year),
});
}
// Apply filter if provided
if (filterYears && typeof filterYears === 'function') {
years = years.filter(filterYears);
}
// Sort based on preference
years.sort((a, b) => showDescending ? Number(b.id) - Number(a.id) : Number(a.id) - Number(b.id));
this.setState({ years });
};
handleChange = (event, newValue) => {
const { onChange, name, onYearChange } = this.props;
this.setState({ selectedValue: newValue });
if (onChange) {
onChange(event, newValue, name);
}
if (onYearChange) {
onYearChange(newValue ? Number(newValue.id) : null);
}
};
getCurrentYear = () => {
return new Date().getFullYear();
};
getYearsCount = () => {
const { years } = this.state;
return years.length;
};
getSelectedYearValue = () => {
const { selectedValue } = this.state;
return selectedValue ? Number(selectedValue.id) : null;
};
render() {
const {
label = 'Year',
placeholder = 'Select year',
disabled = false,
required = false,
error = false,
helperText,
style = { width: 250 },
margin = 'normal',
} = this.props;
const { selectedValue, years } = this.state;
return (
<AutocompleteField
options={years}
value={selectedValue}
onChange={this.handleChange}
label={label}
placeholder={placeholder}
disabled={disabled}
required={required}
error={error}
helperText={helperText}
style={style}
margin={margin}
/>
);
}
}
DDLYear.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
onYearChange: PropTypes.func,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
style: PropTypes.object,
margin: PropTypes.string,
disabled: PropTypes.bool,
required: PropTypes.bool,
error: PropTypes.bool,
helperText: PropTypes.string,
startYear: PropTypes.number,
endYear: PropTypes.number,
showDescending: PropTypes.bool,
filterYears: PropTypes.func,
useCurrentYearAsDefault: PropTypes.bool, // NEW: Enable default current year
};
DDLYear.defaultProps = {
label: 'Year',
placeholder: 'Select year',
startYear: 2000,
showDescending: false,
style: { width: 250 },
useCurrentYearAsDefault: false, // NEW: Default is false (opt-in)
};
export default DDLYear;
\ No newline at end of file
import React from 'react';
import { Typography } from '@material-ui/core';
const Header = ({
title = 'Dashboard Financial',
bgColor = 'main-color',
height = 78,
style = {},
}) => {
return (
<>
<div
className={bgColor}
style={{
height,
display: 'flex',
alignItems: 'center',
paddingLeft: 20,
...style.header,
}}
>
<Typography style={{ fontSize: '16px', color: 'white' }}>
{title}
</Typography>
</div>
</>
);
};
export default Header;
\ No newline at end of file
import React, { Component } from 'react';
import PropagateLoader from 'react-spinners/PropagateLoader';
class OverlayLoader extends Component {
render() {
const {
isLoading,
loaderSize = 20,
loaderColor = '#274B80',
overlayColor = 'rgba(255,255,255,0.8)',
customStyles = {},
} = this.props;
if (!isLoading) return null;
const defaultStyle = {
position: 'absolute',
zIndex: 110,
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: overlayColor,
};
const finalStyle = { ...defaultStyle, ...customStyles };
return (
<div style={finalStyle}>
<PropagateLoader
size={loaderSize}
color={loaderColor}
loading={isLoading}
/>
</div>
);
}
}
export default OverlayLoader;
\ No newline at end of file
import React from 'react';
import { Typography } from '@material-ui/core';
const SectionHeader = ({
title,
subtitle,
// Styling props
border = true,
borderColor = '#c4c4c4',
borderWidth = '1px',
borderStyle = 'solid',
// Typography styling
titleFontSize = '12px',
titleColor = '#4b4b4b',
titleMargin = '10px',
titleFontWeight = 'normal',
subtitleFontSize = '10px',
subtitleColor = '#666',
subtitleMargin = '5px 10px',
// Container styling
containerStyle = {},
titleStyle = {},
subtitleStyle = {},
// Children
children,
}) => {
borderStyle = border
? {
borderBottom: `${borderWidth} ${borderStyle} ${borderColor}`,
}
: {};
return (
<div style={{ ...borderStyle, ...containerStyle }}>
{/* Title */}
{title && (
<Typography
style={{
fontSize: titleFontSize,
color: titleColor,
margin: titleMargin,
fontWeight: titleFontWeight,
...titleStyle,
}}
>
{title}
</Typography>
)}
{/* Subtitle */}
{subtitle && (
<Typography
style={{
fontSize: subtitleFontSize,
color: subtitleColor,
margin: subtitleMargin,
...subtitleStyle,
}}
>
{subtitle}
</Typography>
)}
{/* Custom children */}
{children}
</div>
);
};
export default SectionHeader;
\ No newline at end of file
export function titleCase(text) { export function titleCase(text) {
var value = String(text).replace(/\./g, ' ') var value = String(text).replace(/\./g, ' ')
.replace(/\s/g, ' ') .replace(/\s/g, ' ')
.replace(/^(.)/, function($1) { return $1.toUpperCase(); }) .replace(/^(.)/, function ($1) { return $1.toUpperCase(); })
// .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) // .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
return value return value
...@@ -9,10 +9,67 @@ export function titleCase(text) { ...@@ -9,10 +9,67 @@ export function titleCase(text) {
export function roundMath(number, decimalPlaces) { export function roundMath(number, decimalPlaces) {
const factorOfTen = Math.pow(10, decimalPlaces) const factorOfTen = Math.pow(10, decimalPlaces)
return Math.round(number * factorOfTen) / factorOfTen return Math.round(number * factorOfTen) / factorOfTen
} }
export function fixNumber(num, decimalCount = 2) { export function fixNumber(num, decimalCount = 2) {
const output = Math.round((num + Number.EPSILON) * (Math.pow(10,decimalCount))) / (Math.pow(10,decimalCount)) const output = Math.round((num + Number.EPSILON) * (Math.pow(10, decimalCount))) / (Math.pow(10, decimalCount))
return output return output
} }
\ No newline at end of file
export function downloadFileBlob(fileName, blobData) {
const url = window.URL.createObjectURL(blobData)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
}
/**
* @param {Object} location - Objek location dari props (this.props.location)
* @param {string} paramName - Nama key yang ingin diambil (misal: 'month')
* @param {any} defaultValue - Nilai kembalian jika data null/undefined
*/
export const getStateParam = (location, paramName = null, defaultValue = null) => {
if (!paramName) {
return location?.state ?? defaultValue;
}
return location?.state?.[paramName] ?? defaultValue;
};
export const formatters = {
// Untuk format Infinity
infinity: {
month: (val) => String(val === undefined || val === 'Infinity' || val === '-Infinity'
? "0.0"
: Number(val).toFixed(2)),
totalCY: (val) => String(val === undefined || val === 'Infinity' || val === '-Infinity'
? "0.0"
: Number(val).toFixed(2)),
totalOther: (val) => String(val !== '' && val !== 'Infinity' && val !== '-Infinity'
? Number(val).toFixed(2)
: val)
},
// Untuk format .value property
value: {
month: (val) => String(val?.value === undefined ? val : Number(val.value).toFixed(1)),
total: (val) => String(val !== '' ? Number(val).toFixed(1) : val)
}
};
// ===== HELPER FUNCTIONS =====
export const createMonthData = (item, startIdx, formatFn) => {
const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december'];
const months = {};
monthNames.forEach((month, idx) => {
months[month] = formatFn(item[startIdx + idx]);
});
return months;
};
\ No newline at end of file
...@@ -26,6 +26,7 @@ import ReportCafrm from "../container/ReportCarfm/RepotrCafrm"; ...@@ -26,6 +26,7 @@ import ReportCafrm from "../container/ReportCarfm/RepotrCafrm";
import Maintenance from "../container/Auth/Maintenance"; import Maintenance from "../container/Auth/Maintenance";
import DownloadReport from "../container/DownloadReport/DownloadReport" import DownloadReport from "../container/DownloadReport/DownloadReport"
import ChcmDocument from '../container/CHCM/ChcmDocument'; import ChcmDocument from '../container/CHCM/ChcmDocument';
import ReportHistorical from '../container/Reports/ReportHistorical';
const routes = [ const routes = [
{ {
...@@ -148,6 +149,10 @@ const routes = [ ...@@ -148,6 +149,10 @@ const routes = [
path: "/home/report-trec", path: "/home/report-trec",
main: ChcmDocument main: ChcmDocument
}, },
{
path: "/home/historical",
main: ReportHistorical
},
{ {
path: "*", path: "*",
main: screen404 main: screen404
......
import api from "../api";
import Constant from "../library/Constant";
const handleResponse = (response) => {
// 1. Handling Sukses
if (response.ok && response.data?.status === "success") {
return response.data.data;
}
// 2. Ambil pesan asli
const serverMessage = response.data?.message || response.data?.error || response.problem || "Gagal";
// 3. Masking bahasa teknis backend
let finalMessage = serverMessage;
if (typeof serverMessage === 'string' && (
serverMessage.includes("java.lang") ||
serverMessage.includes("FormatException") ||
serverMessage.includes("sql")
)) {
finalMessage = "Terjadi kesalahan format data pada sistem.";
}
const error = new Error(finalMessage);
error.tipe = (response.status >= 400 && response.status < 500) ? 'warning' : 'error';
error.isApiError = true;
error.originalMessage = serverMessage;
throw error;
};
/**
* Wrapper agar komponen bisa memilih mau pakai data saja atau errornya juga.
* Menghilangkan kebutuhan try-catch di komponen.
*/
const wrapService = (promise) => {
return promise
.then(res => {
return { data: handleResponse(res), error: null };
})
.catch((err) => {
console.error("API_LOG:", err.originalMessage || err.message);
return {
data: null,
error: { message: err.message, tipe: err.tipe }
};
});
};
// --- EXPORTED SERVICES ---
export const fetchMenuPermission = (menuName) => wrapService(api.create().getPermission({ menu: menuName }));
export const fetchDetailRole = (roleId) => wrapService(api.create().getDetailRole(roleId));
export const fetchDetailUser = () => {
const userId = localStorage.getItem(Constant.USER);
return wrapService(api.create().getDetailUser(userId));
};
export const fetchApprover = () => wrapService(api.create().checkApprover());
export const fetchLastPeriod = (companyId) => wrapService(api.create().getLastPeriod(companyId));
export const fetchRevision = (payload) => wrapService(api.create().getRevision(payload));
export const fetchSubmission = (payload) => wrapService(api.create().getSubmission(payload));
export const fetchListApprover = (report, monthlyReportId) => {
return wrapService(api.create().getListApprover(report, monthlyReportId));
}
export const fetchDetailReportCF = (payload) => wrapService(api.create().getDetailReportCF(payload));
export const fetchPLID = (payload) => wrapService(api.create().getPLID(payload));
export const fetchHierarkiCreateReportPLMB = (payload) => wrapService(api.create().getHierarkiCreateReportPLMB(payload));
export const fetchFRID = (payload) => wrapService(api.create().getFRID(payload));
export const fetchDownloadFile = (payload) => wrapService(api.create().createDownloadFile(payload));
export const fetchZipReport = (downloadedFileReportId) => wrapService(api.create().createZipReport(downloadedFileReportId));
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment