Commit 0b56200e authored by Rais Aryaguna's avatar Rais Aryaguna

Merge branch 'develop' of ssh://gitlab.ortax.org:2222/fachri/ctas-box into develop

parents e1874185 0c51fca2
...@@ -2,7 +2,17 @@ import { forwardRef, useState } from 'react'; ...@@ -2,7 +2,17 @@ import { forwardRef, useState } from 'react';
import { NumericFormat } from 'react-number-format'; import { NumericFormat } from 'react-number-format';
const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) { const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) {
const { onChange, maxValue, minValue, maxLength, minLength, allowDecimalValue, ...other } = props; const {
onChange,
maxValue,
minValue,
maxLength,
minLength,
allowDecimalValue,
allowNegativeValue, // ambil custom prop di sini
...other // other sekarang TIDAK mengandung allowNegativeValue
} = props;
const [newValue, setNewValue] = useState(props.value); const [newValue, setNewValue] = useState(props.value);
const key = (e2) => { const key = (e2) => {
...@@ -19,14 +29,16 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) { ...@@ -19,14 +29,16 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) {
return ( return (
<NumericFormat <NumericFormat
{...other} {...other} // aman: other tidak punya allowNegativeValue
isNumericString // gunakan prop NumericFormat yang valid
valueIsNumericString
onKeyPress={key} onKeyPress={key}
thousandSeparator="." thousandSeparator="."
decimalSeparator="," decimalSeparator=","
decimalScale={allowDecimalValue ? 2 : 0} decimalScale={allowDecimalValue ? 2 : 0}
getInputRef={ref} getInputRef={ref}
allowNegative={false} // gunakan allowNegativeValue untuk mengatur allowNegative MUI prop
allowNegative={Boolean(allowNegativeValue)}
isAllowed={(values) => { isAllowed={(values) => {
const { floatValue, value } = values; const { floatValue, value } = values;
...@@ -50,7 +62,7 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) { ...@@ -50,7 +62,7 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) {
return true; return true;
}} }}
onValueChange={(values) => { onValueChange={(values) => {
onChange({ onChange?.({
target: { target: {
name: props.name, name: props.name,
value: values.value, value: values.value,
...@@ -64,6 +76,9 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) { ...@@ -64,6 +76,9 @@ const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) {
NumberFormatRupiah.defaultProps = { NumberFormatRupiah.defaultProps = {
maxValue: undefined, maxValue: undefined,
minValue: undefined, minValue: undefined,
// default agar backward-compatible
allowNegativeValue: false,
allowDecimalValue: false,
}; };
export default NumberFormatRupiah; export default NumberFormatRupiah;
...@@ -60,6 +60,8 @@ export function RHFSelect({ ...@@ -60,6 +60,8 @@ export function RHFSelect({
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<TextField <TextField
{...field} {...field}
value={field.value ?? ''}
onChange={(event) => field.onChange(event.target.value)}
select select
fullWidth fullWidth
error={!!error} error={!!error}
......
...@@ -37,8 +37,6 @@ export type AccountDrawerProps = IconButtonProps & { ...@@ -37,8 +37,6 @@ export type AccountDrawerProps = IconButtonProps & {
export function AccountDrawer({ data = [], sx, ...other }: AccountDrawerProps) { export function AccountDrawer({ data = [], sx, ...other }: AccountDrawerProps) {
const user = useSelector((state: RootState) => state.user); const user = useSelector((state: RootState) => state.user);
console.log(user);
const pathname = usePathname(); const pathname = usePathname();
const { value: open, onFalse: onClose, onTrue: onOpen } = useBoolean(); const { value: open, onFalse: onClose, onTrue: onOpen } = useBoolean();
......
...@@ -78,7 +78,7 @@ export const navData: NavSectionProps['data'] = [ ...@@ -78,7 +78,7 @@ export const navData: NavSectionProps['data'] = [
{ {
title: 'e-Bupot Unifikasi', title: 'e-Bupot Unifikasi',
path: paths.unifikasi.dn, path: paths.unifikasi.dn,
icon: ICONS.banking, icon: ICONS.blank,
children: [ children: [
{ title: 'Bupot Unifikasi', path: paths.unifikasi.dn }, { title: 'Bupot Unifikasi', path: paths.unifikasi.dn },
{ title: 'Bupot Non Residen', path: paths.unifikasi.nr }, { title: 'Bupot Non Residen', path: paths.unifikasi.nr },
......
...@@ -96,6 +96,7 @@ export const paths = { ...@@ -96,6 +96,7 @@ export const paths = {
nr: `${ROOTS.UNIFIKASI}/nr`, nr: `${ROOTS.UNIFIKASI}/nr`,
ssp: `${ROOTS.UNIFIKASI}/ssp`, ssp: `${ROOTS.UNIFIKASI}/ssp`,
digunggung: `${ROOTS.UNIFIKASI}/digunggung`, digunggung: `${ROOTS.UNIFIKASI}/digunggung`,
digunggungNew: `${ROOTS.UNIFIKASI}/digunggung/new`,
dipersamakan: `${ROOTS.UNIFIKASI}/dokumen-dipersamakan`, dipersamakan: `${ROOTS.UNIFIKASI}/dokumen-dipersamakan`,
sptPosting: `${ROOTS.UNIFIKASI}/spt/posting`, sptPosting: `${ROOTS.UNIFIKASI}/spt/posting`,
sptRekam: `${ROOTS.UNIFIKASI}/spt/rekam`, sptRekam: `${ROOTS.UNIFIKASI}/spt/rekam`,
......
...@@ -131,6 +131,7 @@ export const dashboardRoutes: RouteObject[] = [ ...@@ -131,6 +131,7 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'ssp/:id/:type', element: <OverviewUnifikasiRekamSspPage /> }, { path: 'ssp/:id/:type', element: <OverviewUnifikasiRekamSspPage /> },
{ path: 'digunggung', element: <OverviewUnifikasiDigunggungPage /> }, { path: 'digunggung', element: <OverviewUnifikasiDigunggungPage /> },
{ path: 'digunggung/new', element: <OverviewUnifikasiRekamDigunggungPage /> }, { path: 'digunggung/new', element: <OverviewUnifikasiRekamDigunggungPage /> },
{ path: 'digunggung/:id/:type', element: <OverviewUnifikasiRekamDigunggungPage /> },
{ path: 'dokumen-dipersamakan', element: <OverviewUnifikasiDokumenDipersamakanPage /> }, { path: 'dokumen-dipersamakan', element: <OverviewUnifikasiDokumenDipersamakanPage /> },
{ {
path: 'dokumen-dipersamakan/new', path: 'dokumen-dipersamakan/new',
......
import React from 'react';
import type { GridPreferencePanelsValue } from '@mui/x-data-grid-premium';
import { useGridApiContext } from '@mui/x-data-grid-premium';
import { IconButton, Tooltip } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
// ✅ React.memo: cegah render ulang tanpa alasan
const CustomColumnsButton: React.FC = React.memo(() => {
const apiRef = useGridApiContext();
// ✅ useCallback biar referensi handleClick stabil di setiap render
const handleClick = React.useCallback(() => {
if (!apiRef.current) return;
apiRef.current.showPreferences('columns' as GridPreferencePanelsValue);
}, [apiRef]);
return (
<Tooltip title="Kolom">
<IconButton
size="small"
onClick={handleClick}
sx={{
color: '#123375',
'&:hover': { backgroundColor: 'rgba(18, 51, 117, 0.08)' },
}}
>
<ViewColumnIcon fontSize="small" />
</IconButton>
</Tooltip>
);
});
export default CustomColumnsButton;
import * as React from 'react';
import type { GridToolbarProps } from '@mui/x-data-grid-premium';
import { GridToolbarContainer } from '@mui/x-data-grid-premium';
import { Stack, Divider, IconButton, Tooltip } from '@mui/material';
import type { ActionItem } from '../types/types';
import { CustomFilterButton } from './CustomFilterButton';
import CustomColumnsButton from './CustomColumnsButton';
interface CustomToolbarProps extends GridToolbarProps {
actions?: ActionItem[][];
columns: any[]; // GridColDef[]
filterModel: any;
setFilterModel: (m: any) => void;
statusOptions?: { value: string; label: string }[];
}
// ✅ React.memo mencegah render ulang kalau props sama
export const CustomToolbar = React.memo(function CustomToolbar({
actions = [],
columns,
filterModel,
setFilterModel,
statusOptions = [],
...gridToolbarProps
}: CustomToolbarProps) {
return (
<GridToolbarContainer
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 1.5,
}}
{...gridToolbarProps}
>
<Stack direction="row" alignItems="center" gap={1}>
{actions.map((group, groupIdx) => (
<Stack key={groupIdx} direction="row" gap={0.5} alignItems="center">
{group.map((action, idx) => (
<Tooltip key={idx} title={action.title}>
<span>
<IconButton
sx={{ color: action.disabled ? 'action.disabled' : '#123375' }}
size="small"
onClick={action.func}
disabled={action.disabled}
>
{action.icon}
</IconButton>
</span>
</Tooltip>
))}
{groupIdx < actions.length - 1 && <Divider orientation="vertical" flexItem />}
</Stack>
))}
</Stack>
<Stack direction="row" alignItems="center" gap={0.5}>
<CustomColumnsButton />
<CustomFilterButton
columns={columns}
filterModel={filterModel}
setFilterModel={setFilterModel}
statusOptions={statusOptions}
/>
</Stack>
</GridToolbarContainer>
);
});
import * as React from 'react';
import { Grid, Typography, Divider, Box } from '@mui/material';
interface ListDetailItem {
label: React.ReactNode;
value: React.ReactNode;
}
interface ListDetailBuilderProps {
rows?: ListDetailItem[];
labelWidth?: number; // optional, default 4
spacingY?: number; // optional, default 2
}
export default function ListDetailBuilder({
rows = [],
labelWidth = 4,
spacingY = 2,
}: ListDetailBuilderProps) {
if (rows.length === 0) return null;
return (
<Box
sx={{
width: '100%',
bgcolor: 'background.paper',
borderRadius: 2,
overflow: 'hidden',
}}
>
{rows.map((row, index) => (
<React.Fragment key={index}>
<Grid container alignItems="flex-start" spacing={2} sx={{ px: 2, py: spacingY }}>
<Grid size={{ xs: 12, md: labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
fontWeight: 600,
color: 'text.secondary',
whiteSpace: 'pre-wrap',
}}
>
{row.label}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 12 - labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
color: 'text.primary',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{row.value ?? '-'}
</Typography>
</Grid>
</Grid>
{index < rows.length - 1 && <Divider sx={{ mx: 2 }} />}
</React.Fragment>
))}
</Box>
);
}
import React from 'react';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
type Props = { value?: string; revNo?: number };
const StatusChip: React.FC<Props> = ({ value, revNo }) => {
if (!value) return <Chip label="" size="small" />;
if (value === 'NORMAL-Done' && revNo !== 0) {
return (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
}}
>
<Chip
label="Normal Pengganti"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
paddingRight: '5px',
}}
/>
<Chip
label={revNo}
size="small"
variant="filled"
sx={{
position: 'absolute',
top: -6,
right: -6,
backgroundColor: '#1976d2',
color: '#fff',
borderRadius: '50%',
fontWeight: 500,
width: 18,
height: 18,
minWidth: 0,
border: '2px solid #fff',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
'& .MuiChip-label': {
padding: 0,
fontSize: '0.65rem',
lineHeight: 1,
},
}}
/>
</Box>
);
}
if (value === 'NORMAL-Done' && revNo === 0) {
return (
<Chip
label="Normal"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: '500',
}}
/>
);
}
if (value === 'AMENDED') {
return (
<Chip
label="Diganti"
size="small"
variant="outlined"
sx={{
color: '#fff',
backgroundColor: '#f38c28',
borderRadius: '8px',
fontWeight: 500,
border: 'none',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.15)',
}}
/>
);
}
if (value === 'CANCELLED') {
return (
<Chip
label="Dibatalkan"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: '500',
}}
/>
);
}
if (value === 'DRAFT') {
return (
<Chip
label="Draft"
size="small"
variant="outlined"
sx={{
borderColor: '#9e9e9e',
color: '#616161',
borderRadius: '8px',
}}
/>
);
}
return <Chip label={value} size="small" />;
};
export default React.memo(StatusChip);
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
} from '@mui/material';
interface CancelConfirmationDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
selectedCount: number;
}
const CancelConfirmationDialog: React.FC<CancelConfirmationDialogProps> = ({
open,
onClose,
onConfirm,
selectedCount,
}) => (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Konfirmasi Pembatalan</DialogTitle>
<DialogContent>
<Typography>
Apakah Anda yakin ingin membatalkan {selectedCount} data yang dipilih?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Batal</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Ya, Batalkan
</Button>
</DialogActions>
</Dialog>
);
export default CancelConfirmationDialog;
import React, { useEffect, useMemo, useState } from 'react';
import { Stack, Button, Typography } from '@mui/material';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import minMax from 'dayjs/plugin/minMax';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
// import useCancelDn from '../../hooks/useCancelSsp';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useCancelDigunggung from '../../hooks/useCancelDigunggung';
dayjs.extend(minMax);
// Helper format tanggal ke format API (DDMMYYYY)
const formatDateDDMMYYYY = (d: Date) => {
const dd = String(d.getDate()).padStart(2, '0');
const mm = String(d.getMonth() + 1).padStart(2, '0');
const yyyy = d.getFullYear();
return `${dd}${mm}${yyyy}`;
};
interface ModalCancelDigunggungProps {
dataSelected?: any[];
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
}
const ModalCancelDigunggung: React.FC<ModalCancelDigunggungProps> = ({
dataSelected = [],
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan',
onConfirmCancel,
}) => {
const queryClient = useQueryClient();
const [tglPembatalan, setTglPembatalan] = useState<Dayjs | null>(null);
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const { mutateAsync } = useCancelDigunggung({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// ✅ update jumlah data di progress bar
useEffect(() => {
setNumberOfData(dataSelected?.length ?? 0);
}, [dataSelected, setNumberOfData]);
// ✅ Ambil tanggal pemotongan paling awal (untuk minDate)
const minPembatalanDate = useMemo(() => {
if (!dataSelected.length) return null;
const dates = dataSelected
.map((d) => {
const tgl = d.tglPemotongan || d.tglpemotongan;
return tgl ? dayjs(tgl, ['YYYY-MM-DD', 'DD/MM/YYYY']) : null;
})
.filter((d): d is Dayjs => !!d && d.isValid());
return dates.length > 0 ? dayjs.min(dates) : null;
}, [dataSelected]);
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const handleSubmit = async () => {
if (!tglPembatalan) {
enqueueSnackbar('Tanggal pembatalan harus diisi', { variant: 'warning' });
return;
}
const formattedDate = formatDateDDMMYYYY(tglPembatalan.toDate());
const ids = dataSelected.map((item) => String(item.id ?? item.internal_id));
try {
setIsOpenDialogProgressBar(true);
const results = await Promise.allSettled(
ids.map((id) => mutateAsync({ id, tglPembatalan: formattedDate }))
);
const rejected = results.filter((r) => r.status === 'rejected');
const success = results.filter((r) => r.status === 'fulfilled');
// ✅ tampilkan pesan error detail
if (rejected.length > 0) {
const errorMessages = rejected
.map((r) => (r.status === 'rejected' ? r.reason?.message : ''))
.filter(Boolean)
.join('\n');
enqueueSnackbar(
<span style={{ whiteSpace: 'pre-line' }}>
{errorMessages || `${rejected.length} dari ${ids.length} data gagal dibatalkan.`}
</span>,
{ variant: 'error' }
);
processFail();
}
if (success.length > 0) {
enqueueSnackbar(successMessage, { variant: 'success' });
processSuccess();
await onConfirmCancel?.();
}
// ✅ update cache data lokal agar status langsung berubah
queryClient.setQueryData(['unifikasi', 'digunggung'], (old: any) => {
if (!old?.data) return old;
return {
...old,
data: old.data.map((row: any) =>
ids.includes(String(row.id)) ? { ...row, fgStatus: 'CANCELLED' } : row
),
};
});
// ✅ refetch data agar sinkron
await queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
// ⚠️ Tidak perlu clearSelection di sini — DnListView akan sync otomatis lewat rowsSet
handleCloseModal();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal membatalkan data', { variant: 'error' });
processFail();
} finally {
setIsOpenDialogProgressBar(false);
}
};
return (
<>
{/* ✅ Dialog reusable */}
<DialogUmum
isOpen={isOpenDialogCancel}
onClose={handleCloseModal}
title="Batal Bukti Pemotongan/Pemungutan PPh Unifikasi"
>
<Stack spacing={2}>
<Typography>
Silakan isi tanggal pembatalan. Tanggal tidak boleh sebelum tanggal pemotongan.
</Typography>
<DatePicker
label="Tanggal Pembatalan"
format="DD/MM/YYYY"
value={tglPembatalan}
maxDate={dayjs()}
minDate={minPembatalanDate || undefined}
onChange={(newValue) => setTglPembatalan(newValue)}
slotProps={{
textField: {
size: 'medium',
fullWidth: true,
helperText:
minPembatalanDate && `Tanggal minimal: ${minPembatalanDate.format('DD/MM/YYYY')}`,
InputLabelProps: { shrink: true },
sx: {
'& .MuiOutlinedInput-root': {
borderRadius: 1.5,
backgroundColor: '#fff',
'&:hover fieldset': {
borderColor: '#123375 !important',
},
'&.Mui-focused fieldset': {
borderColor: '#123375 !important',
borderWidth: '1px',
},
},
},
},
}}
/>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<Button variant="outlined" onClick={handleCloseModal}>
Batal
</Button>
<Button
variant="contained"
color="error"
onClick={handleSubmit}
disabled={!tglPembatalan}
>
Batalkan
</Button>
</Stack>
</Stack>
</DialogUmum>
{/* ✅ Dialog progress bar */}
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalCancelDigunggung;
import React, { useEffect, useState } from 'react';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent';
import ListDetailBuilder from '../ListDetailItem';
import StatusChip from '../StatusChip';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
interface ModalCetakPdfDigunggungProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
type Row = { label: string; value: React.ReactNode };
const ModalCetakDigunggung: React.FC<ModalCetakPdfDigunggungProps> = ({
payload,
isOpen,
onClose,
}) => {
const [rows, setRows] = useState<Row[]>([]);
useEffect(() => {
if (!payload) return;
const periode =
payload.msPajak && payload.thnPajak ? `${payload.msPajak} - ${payload.thnPajak}` : '-';
const formattedRows: Row[] = [
{ label: 'Periode', value: periode },
{ label: 'Kode Objek Pajak', value: payload.kdObjPjk || '-' },
{ label: 'Nomor Bukti Pemotongan', value: payload.noBupot || '-' },
{
label: 'Identitas Dipotong',
value: payload.nitkuPemotong,
},
{ label: 'Nama Dipotong', value: payload.namaPenandatangan || '-' },
{
label: 'Jumlah Bruto (Rp)',
value: payload.jmlBruto,
},
{
label: 'Tarif (%)',
value: `${Number(payload.tarif || 0)}`,
},
{
label: 'Jumlah Dipotong (Rp)',
value: payload.pphDipotong,
},
{
label: 'Perekam',
value: payload.userId || '-',
},
{
label: 'Created Date',
value: payload.created_at
? dayjs(payload.created_at).local().format('DD/MM/YYYY HH:mm:ss')
: '-',
},
{
label: 'Modified By',
value: payload.updated || '-',
},
{
label: 'Modified Date',
value: payload.updated_at
? dayjs(payload.updated_at).local().format('DD/MM/YYYY HH:mm:ss')
: '-',
},
{
label: 'Status',
value: <StatusChip value={payload.fgStatus} revNo={payload.revNo} />,
},
];
setRows(formattedRows);
}, [payload]);
return (
<DialogUmum maxWidth="lg" isOpen={isOpen} onClose={onClose} title="Detail Bupot Digunggung">
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
<ListDetailBuilder rows={rows} />
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakDigunggung;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useDeleteDn from '../../hooks/useDeleteDigunggung';
interface ModalDeleteDigunggungProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
onConfirmDelete?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalDeleteDigunggung: React.FC<ModalDeleteDigunggungProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useDeleteDn({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmDelete?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan menghapus data ini?"
description="Data yang sudah dihapus tidak dapat dikembalikan."
actionTitle="Hapus"
isOpen={isOpenDialogDelete}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalDeleteDigunggung;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useUpload from '../../hooks/useUpload';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import { Field } from 'src/components/hook-form';
import MenuItem from '@mui/material/MenuItem';
import { useSelector } from 'react-redux';
import type { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form';
import Button from '@mui/material/Button';
interface ModalUploadDigunggungProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalUploadDigunggung: React.FC<ModalUploadDigunggungProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
}) => {
const queryClient = useQueryClient();
const uploadDigunggung = useUpload();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useSelector((state: RootState) => state.user.data.signer);
const { mutateAsync } = useUpload({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
// fungsi multiple upload -- gunakan normalized array of ids
const handleMultipleUpload = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
await onConfirmUpload?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'nr'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogUpload, dataSelected, setNumberOfData]);
return (
<>
<FormProvider {...methods}>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleCloseModal}
title="Upload Bukti Potong"
>
<Stack spacing={2} sx={{ mt: 2 }}>
<Grid size={{ md: 12 }}>
<Field.Select name="signer" label="NPWP/NIK Penandatangan">
<MenuItem value={signer}>{signer}</MenuItem>
</Field.Select>
</Grid>
<Grid size={12}>
<Agreement
isCheckedAgreement={isCheckedAgreement}
setIsCheckedAgreement={setIsCheckedAgreement}
text="Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah saya isi dengan benar secara elektronik sesuai dengan"
/>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<Button
type="button"
disabled={!isCheckedAgreement}
onClick={onSubmit}
loading={uploadDigunggung.isPending}
variant="contained"
sx={{ background: '#143B88' }}
>
Save
</Button>
</Stack>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalUploadDigunggung;
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useEffect } from 'react';
import { Field } from 'src/components/hook-form'; import { Field } from 'src/components/hook-form';
import { JENIS_DOKUMEN } from 'src/sections/bupot-unifikasi/bupot-dn/constant'; import { JENIS_DOKUMEN } from '../../constant';
import dayjs from 'dayjs';
import { useAppSelector } from 'src/store';
import { useFormContext } from 'react-hook-form';
const DokumenReferensi = () => { const DokumenReferensi = () => {
const MockNitku = [ const { watch, setValue } = useFormContext<Record<string, any>>();
{ const nitku = useAppSelector((state) => state.user.data.nitku_trial);
nama: '1091031210912281000000', const nitkuValue = watch('idTku');
},
{ console.log(nitku);
nama: '1091031210912281000001',
}, useEffect(() => {
]; if (!nitkuValue && nitku) {
setValue('idTku', nitku);
}
}, [nitku, nitkuValue, setValue]);
return ( return (
<Grid sx={{ mb: 3 }} container rowSpacing={2} columnSpacing={2}> <Grid sx={{ mb: 3 }} container rowSpacing={2} columnSpacing={2}>
<Grid sx={{ mt: 3 }} size={{ md: 12 }}> <Grid sx={{ mt: 3 }} size={{ md: 12 }}>
...@@ -33,15 +41,16 @@ const DokumenReferensi = () => { ...@@ -33,15 +41,16 @@ const DokumenReferensi = () => {
<Field.Text name="nomorDok" label="Nomor Dokumen" /> <Field.Text name="nomorDok" label="Nomor Dokumen" />
</Grid> </Grid>
<Grid size={{ md: 6 }}> <Grid size={{ md: 6 }}>
<Field.DatePicker name="tglDok" label="Tanggal Dokumen" /> <Field.DatePicker
name="tglDok"
label="Tanggal Dokumen"
maxDate={dayjs()}
minDate={dayjs('2025-01-01')}
/>
</Grid> </Grid>
<Grid size={{ md: 6 }}> <Grid size={{ md: 6 }}>
<Field.Select name="idTku" label="NITKU Pemotong"> <Field.Select name="idTku" label="NITKU Pemotong">
{MockNitku.map((item, index) => ( <MenuItem value={nitku}>{nitku}</MenuItem>
<MenuItem key={index} value={item.nama}>
{item.nama}
</MenuItem>
))}
</Field.Select> </Field.Select>
</Grid> </Grid>
</Grid> </Grid>
......
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
// import { useParams } from 'react-router'; import dayjs from 'dayjs';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form'; import { Field } from 'src/components/hook-form';
type IdentitasProps = { type IdentitasProps = {
isPengganti: boolean; isPengganti: boolean;
// disabledTambah: boolean; existingDigunggung?: any; // Data penuh dari API (opsional, untuk edit/pengganti)
// disabledHapus: boolean;
}; };
const Identitas = ({ isPengganti }: IdentitasProps) => ( const Identitas = ({ isPengganti, existingDigunggung }: IdentitasProps) => {
// const { dnId } = useParams(); const { setValue, watch } = useFormContext();
// const { setValue } = useFormContext();
// const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0); const tanggalPemotongan = watch('tglPemotongan');
// const maxKeterangan = 5; // 🧩 Auto isi Tahun & Masa Pajak berdasarkan tanggalPemotongan
useEffect(() => {
// const handleTambah = () => { if (tanggalPemotongan) {
// if (jumlahKeterangan < maxKeterangan) { const date = dayjs(tanggalPemotongan);
// setJumlahKeterangan(jumlahKeterangan + 1); setValue('thnPajak', date.format('YYYY'));
// } setValue('msPajak', date.format('MM'));
// }; } else {
setValue('thnPajak', '');
// const handleHapus = () => { setValue('msPajak', '');
// if (jumlahKeterangan > 0) { }
// const newCount = jumlahKeterangan - 1; }, [tanggalPemotongan, setValue]);
// setJumlahKeterangan(newCount);
// // reset value form field yang dihapus
// setValue(`keterangan${newCount + 1}`, null);
// }
// };
return (
<>
{/* 📋 Identitas Dasar */}
<Grid container rowSpacing={2} alignItems="center" columnSpacing={2}> <Grid container rowSpacing={2} alignItems="center" columnSpacing={2}>
{/* 📅 Tanggal & Masa Pajak */}
<Grid size={{ md: 6 }}> <Grid size={{ md: 6 }}>
<Field.DatePicker name="tglPemotongan" label="Tanggal Pemotongan" /> <Field.DatePicker
name="tglPemotongan"
label="Tanggal Pemotongan"
format="DD/MM/YYYY"
maxDate={dayjs()}
disabled={isPengganti}
/>
</Grid> </Grid>
<Grid size={{ md: 3 }}> <Grid size={{ md: 3 }}>
<Field.DatePicker name="thnPajak" label="Tahun Pajak" view="year" format="YYYY" /> <Field.DatePicker
name="thnPajak"
label="Tahun Pajak"
view="year"
format="YYYY"
disabled={isPengganti}
/>
</Grid> </Grid>
<Grid size={{ md: 3 }}> <Grid size={{ md: 3 }}>
<Field.DatePicker name="msPajak" label="Masa Pajak" view="month" format="MM" /> <Field.DatePicker
name="msPajak"
label="Masa Pajak"
views={['year', 'month']} // ✅ valid prop
openTo="month"
format="MM"
disabled={isPengganti}
/>
</Grid> </Grid>
</Grid> </Grid>
); </>
);
};
export default Identitas; export default Identitas;
import type { FC } from 'react'; import type { FC } from 'react';
import { Fragment, memo } from 'react';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material'; import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { ChevronRightRounded, CloseRounded } from '@mui/icons-material'; import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { m } from 'framer-motion'; import { m } from 'framer-motion';
import { PANDUAN_REKAM_DIGUNGGUNG } from '../../constant'; import { PANDUAN_REKAM_DIGUNGGUNG } from '../../constant';
interface PanduanDigunggungRekamProps { interface PanduanDnRekamProps {
handleOpen: () => void; handleOpen: () => void;
isOpen: boolean; isOpen: boolean;
} }
const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, isOpen }) => ( const PanduanSspRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => (
<Box position="sticky"> <Box position="sticky">
{/* Tombol toggle */} {/* Tombol toggle */}
<Box {!isOpen && (
height="100%" <Box height="100%" display="flex" justifyContent="center" alignItems="center">
display={isOpen ? 'none' : 'flex'}
justifyContent="center"
alignItems="center"
>
<Button <Button
variant="contained" variant="contained"
sx={{ sx={{
...@@ -47,6 +44,7 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -47,6 +44,7 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
</span> </span>
</Button> </Button>
</Box> </Box>
)}
{/* Konten panduan */} {/* Konten panduan */}
{isOpen && ( {isOpen && (
...@@ -62,10 +60,8 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -62,10 +60,8 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
sx={{ sx={{
backgroundColor: '#123375', backgroundColor: '#123375',
color: '#FFFFFF', color: '#FFFFFF',
padding: '16px', p: 2,
'& .MuiCardHeader-title': { '& .MuiCardHeader-title': { fontSize: 18 },
fontSize: 18,
},
}} }}
action={ action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}> <IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
...@@ -74,31 +70,39 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -74,31 +70,39 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
} }
title="Panduan Penggunaan" title="Panduan Penggunaan"
/> />
<CardContent <CardContent
sx={{ sx={{
maxHeight: 300, maxHeight: 300,
overflow: 'auto', overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 }, '&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { backgroundColor: '#f0f0f0', borderRadius: 8 }, '&::-webkit-scrollbar-track': {
'&::-webkit-scrollbar-thumb': { backgroundColor: '#123375', borderRadius: 8 }, backgroundColor: '#f0f0f0',
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: '#0d2858' }, borderRadius: 8,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: '#123375',
borderRadius: 8,
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: '#0d2858',
},
scrollbarWidth: 'thin', scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0', scrollbarColor: '#123375 #f0f0f0',
}} }}
> >
{/* Deskripsi Form */} {/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}> <Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<span style={{ fontWeight: 600 }}>Deskripsi Form:</span> <strong>Deskripsi Form:</strong>
<br /> <br />
{PANDUAN_REKAM_DIGUNGGUNG.description.intro} {PANDUAN_REKAM_DIGUNGGUNG.description.intro}
</Typography> </Typography>
<Typography variant="body2" sx={{}}> <Typography variant="body2">{PANDUAN_REKAM_DIGUNGGUNG.description.textList}</Typography>
{PANDUAN_REKAM_DIGUNGGUNG.description.textList}
</Typography>
<Box component="ol" sx={{ pl: 3, mb: 2 }}> <Box component="ol" sx={{ pl: 3, mb: 2 }}>
{PANDUAN_REKAM_DIGUNGGUNG.description.list.map((item, idx) => ( {PANDUAN_REKAM_DIGUNGGUNG.description.list.map((item, idx) => (
<Typography key={idx} variant="body2" component="li"> <Typography key={`desc-${idx}`} variant="body2" component="li">
{item} {item}
</Typography> </Typography>
))} ))}
...@@ -110,7 +114,7 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -110,7 +114,7 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
{/* Bagian-bagian */} {/* Bagian-bagian */}
{PANDUAN_REKAM_DIGUNGGUNG.sections.map((section, i) => ( {PANDUAN_REKAM_DIGUNGGUNG.sections.map((section, i) => (
<Box key={i} sx={{ mb: 2 }}> <Box key={`section-${i}`} sx={{ mb: 2 }}>
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontWeight: 'bold', fontSize: '0.95rem', mb: 0.5 }} sx={{ fontWeight: 'bold', fontSize: '0.95rem', mb: 0.5 }}
...@@ -120,21 +124,27 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -120,21 +124,27 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
<Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}> <Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}>
{section.items.map((item, idx) => ( {section.items.map((item, idx) => (
<Box key={idx} component="li" sx={{ mb: 0.5 }}> <Fragment key={`item-${i}-${idx}`}>
<Box component="li" sx={{ mb: 0.5 }}>
<Typography variant="body2" component="span"> <Typography variant="body2" component="span">
{item.text} {item.text}
</Typography> </Typography>
{item.subItems.length > 0 && ( {item.subItems?.length > 0 && (
<Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}> <Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}>
{item.subItems.map((sub, subIdx) => ( {item.subItems.map((sub, subIdx) => (
<Typography key={subIdx} variant="body2" component="li"> <Typography
key={`sub-${i}-${idx}-${subIdx}`}
variant="body2"
component="li"
>
{sub} {sub}
</Typography> </Typography>
))} ))}
</Box> </Box>
)} )}
</Box> </Box>
</Fragment>
))} ))}
</Box> </Box>
</Box> </Box>
...@@ -144,6 +154,6 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i ...@@ -144,6 +154,6 @@ const PanduanDigunggungRekam: FC<PanduanDigunggungRekamProps> = ({ handleOpen, i
</m.div> </m.div>
)} )}
</Box> </Box>
); );
export default PanduanDigunggungRekam; export default memo(PanduanSspRekam);
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import HighlightOffTwoToneIcon from '@mui/icons-material/HighlightOffTwoTone';
interface ToolbarCancelProps {
selectedRows: any[];
selectedRowsData: any[];
onCancel: (ids: number[]) => void;
}
const ToolbarCancel: React.FC<ToolbarCancelProps> = ({
selectedRows,
selectedRowsData,
onCancel,
}) => {
// Logic sederhana
const isEnabled =
selectedRows.length > 0 &&
selectedRowsData.every((row: any) => row.fgStatus === 'normal' || row.fgStatus === 'amendment');
const handleClick = () => {
if (!isEnabled) return;
const ids = selectedRowsData.map((row: any) => row.id).filter((id: any) => id !== undefined);
onCancel(ids);
};
return (
<Tooltip title={isEnabled ? `Batalkan ${selectedRows.length} data` : 'Pilih data yang valid'}>
<IconButton
onClick={handleClick}
disabled={!isEnabled}
color={isEnabled ? 'error' : 'default'}
>
<HighlightOffTwoToneIcon />
</IconButton>
</Tooltip>
);
};
export default ToolbarCancel;
const appRootKey = 'unifikasi';
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
digunggung: {
all: (params: any) => [appRootKey, 'digunggung', params],
detail: (params: any) => [appRootKey, 'digunggung', 'detail', params],
draft: [appRootKey, 'digunggung', 'draft'],
delete: [appRootKey, 'digunggung', 'delete'],
upload: [appRootKey, 'digunggung', 'upload'],
cancel: [appRootKey, 'digunggung', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'digunggungs-cetak-pdf', params],
},
};
export default queryKey;
type FilterItem = {
field: string;
operator: string;
value?: string | number | Array<string | number> | null;
join?: 'AND' | 'OR'; // optional: join connector BEFORE this item (first item usually undefined)
};
type BaseParams = Record<string, any>;
export function useAdvancedFilter() {
const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
const dateFields = new Set(['created_at', 'updated_at']);
const fieldMap: Record<string, string> = {
noBupot: 'noBupot',
};
const dbField = (field: string) => fieldMap[field] ?? field;
const escape = (v: string) => String(v).replace(/'/g, "''");
const toDbDate = (value: string | Date) => {
if (value instanceof Date) {
const y = value.getFullYear();
const m = String(value.getMonth() + 1).padStart(2, '0');
const d = String(value.getDate()).padStart(2, '0');
return `${y}${m}${d}`;
}
const digits = String(value).replace(/[^0-9]/g, '');
if (digits.length >= 8) return digits.slice(0, 8);
return digits;
};
const normalizeOp = (op: string) => op?.toString().trim();
function buildAdvancedFilter(filters?: FilterItem[] | null) {
if (!filters || filters.length === 0) return '';
const exprs: string[] = [];
const joins: ('AND' | 'OR')[] = [];
for (let i = 0; i < filters.length; i++) {
const f = filters[i];
if (!f || !f.field) continue;
const op = normalizeOp(f.operator ?? '');
const fieldName = dbField(f.field);
let expr: string | null = null;
// --- DATE FIELDS ---
if (dateFields.has(fieldName)) {
const rawVal = f.value;
if (!rawVal && !/is empty|is not empty/i.test(op)) continue;
const ymd = toDbDate(rawVal as string | Date);
if (!ymd) continue;
if (/^is$/i.test(op)) {
expr = `"${fieldName}" >= '${ymd} 00:00:00' AND "${fieldName}" <= '${ymd} 23:59:59'`;
} else if (/is on or after/i.test(op)) {
expr = `"${fieldName}" >= '${ymd}'`;
} else if (/is on or before/i.test(op)) {
expr = `"${fieldName}" <= '${ymd}'`;
}
}
// --- EMPTY ---
if (/is empty/i.test(op)) {
expr = `LOWER("${fieldName}") IS NULL`;
} else if (/is not empty/i.test(op)) {
expr = `LOWER("${fieldName}") IS NOT NULL`;
}
// --- IS ANY OF ---
if (!expr && /is any of/i.test(op)) {
let values: Array<string | number> = [];
if (Array.isArray(f.value)) values = f.value as any;
else if (typeof f.value === 'string')
values = f.value
.split(',')
.map((s) => s.trim())
.filter(Boolean);
else if (f.value != null) values = [f.value as any];
if (values.length > 0) {
if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
const ors = values.map((v) => {
const s = escape(String(v).toLowerCase());
return `LOWER("${fieldName}") LIKE LOWER('%${s}%')`;
});
expr = `(${ors.join(' OR ')})`;
} else {
const ors = values.map((v) => {
const s = escape(String(v).toLowerCase());
return `LOWER("${fieldName}") = '${s}'`;
});
expr = `(${ors.join(' OR ')})`;
}
}
}
// --- FGSTATUS special single-value is / is not ---
if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
const valRaw = f.value == null ? '' : String(f.value);
if (valRaw !== '' || /is any of|is empty|is not empty/i.test(op)) {
const valEscaped = escape(valRaw.toLowerCase());
if (/^is$/i.test(op)) {
expr = `LOWER("${fieldName}") LIKE LOWER('%${valEscaped}%')`;
} else if (/is not/i.test(op)) {
expr = `LOWER("${fieldName}") NOT LIKE LOWER('%${valEscaped}%')`;
}
}
}
// --- GENERIC ---
if (!expr) {
const valRaw = f.value == null ? '' : String(f.value);
if (valRaw !== '') {
const valEscaped = escape(valRaw.toLowerCase());
if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
expr = `"${fieldName}" ${op} '${valEscaped}'`;
} else if (/^contains$/i.test(op)) {
expr = `LOWER("${fieldName}") LIKE LOWER('%${valEscaped}%')`;
} else if (/^equals$/i.test(op)) {
const values = Array.isArray(f.value)
? (f.value as any[]).map((v) => escape(String(v).toLowerCase()))
: [escape(String(f.value).toLowerCase())];
expr = `LOWER("${fieldName}") IN (${values.map((v) => `'${v}'`).join(',')})`;
} else if (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
expr = `LOWER("${fieldName}") ${op} '${valEscaped}'`;
} else if (/^(is)$/i.test(op)) {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
} else {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
}
}
}
if (expr) {
exprs.push(expr);
const joinBefore = f.join ?? (exprs.length > 1 ? 'AND' : 'AND');
joins.push(joinBefore);
}
}
// combine expressions
if (exprs.length === 0) return '';
let out = exprs[0];
for (let i = 1; i < exprs.length; i++) {
const j = joins[i] ?? 'AND';
out = `(${out}) ${j} (${exprs[i]})`;
}
return out;
}
function buildRequestParams(base: BaseParams = {}, advanced: string) {
const out: BaseParams = { ...(base ?? {}) };
if ('noBupot' in out) {
out.nomorBupot = out.noBupot;
delete out.noBupot;
}
out.advanced = advanced || '';
return out;
}
return { buildAdvancedFilter, buildRequestParams } as const;
}
export default useAdvancedFilter;
import { useMutation } from '@tanstack/react-query';
import type { TCancelRequest, TCancelResponse } from '../types/types';
import digunggungApi from '../utils/api';
const useCancelDigunggung = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelRequest>({
mutationKey: ['cancel-digunggung'],
mutationFn: (payload) => digunggungApi.cancel(payload),
...props,
});
export default useCancelDigunggung;
import { useMutation } from '@tanstack/react-query';
import type { TBaseResponseAPI, TDeleteRequest } from '../types/types';
import digunggungApi from '../utils/api';
const useDeleteDigunggung = (props?: any) =>
useMutation<TBaseResponseAPI<null>, Error, TDeleteRequest>({
mutationKey: ['delete-digunggung'],
mutationFn: (payload) => digunggungApi.deleteDigunggung(payload),
...props,
});
export default useDeleteDigunggung;
import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query';
// import type {
// // TGetListDataTableDnResult,
// TGetListDataTableNr,
// TGetListDataTableNrResult,
// } from '../types/types';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from '../constant';
import queryKey from '../constant/queryKey';
import type {
TGetListDataTableDigunggung,
TGetListDataTableDigunggungResult,
// TGetListDataTableSsp,
// TGetListDataTableSspResult,
} from '../types/types';
import digunggungApi from '../utils/api';
export type TGetDigunggungApiWrapped = {
data: TGetListDataTableDigunggungResult[];
total: number;
pageSize: number;
page: number; // 1-based
};
// ---------- helpers (unchanged, kept for completeness) ----------
export const transformFgStatusToFgSignStatus = (fgStatus: any) => {
const splittedFgStatus = fgStatus?.split('-') || [];
if (splittedFgStatus.includes('SIGN') > 0) return FG_SIGN_STATUS.FAILED;
if (splittedFgStatus.includes('SIGNING IN PROGRESS')) return FG_SIGN_STATUS.IN_PROGRESS;
if (fgStatus === 'DUPLICATE') return FG_SIGN_STATUS.DUPLICATE;
if (fgStatus === 'NOT_MATCH_STATUS') return FG_SIGN_STATUS.NOT_MATCH_STATUS;
if (fgStatus === 'NOT_MATCH_NILAI') return FG_SIGN_STATUS.NOT_MATCH_NILAI;
if (fgStatus === 'NOT_MATCH_IDBUPOT') return FG_SIGN_STATUS.NOT_MATCH_IDBUPOT;
switch (splittedFgStatus[1]) {
case 'document signed successfully':
case 'Done':
return FG_SIGN_STATUS.SIGNED;
case 'SIGNING_IN_PROGRESS':
return FG_SIGN_STATUS.IN_PROGRESS;
case 'DUPLICATE':
return FG_SIGN_STATUS.DUPLICATE;
case 'NOT_MATCH_STATUS':
return FG_SIGN_STATUS.NOT_MATCH_STATUS;
case 'NOT_MATCH_IDBUPOT':
return FG_SIGN_STATUS.NOT_MATCH_IDBUPOT;
default:
return null;
}
};
export const getFgStatusPdf = (link: any, fgSignStatus: any) => {
if (!link || [FG_SIGN_STATUS.IN_PROGRESS].includes(fgSignStatus))
return FG_PDF_STATUS.TIDAK_TERSEDIA;
if (!link.includes('https://coretaxdjp.pajak.go.id/')) return FG_PDF_STATUS.BELUM_TERBENTUK;
return FG_PDF_STATUS.TERBENTUK;
};
export const transformSortModelToSortApiPayload = (transformedModel: any) => ({
sortingMode: transformedModel.map((item: any) => item.field).join(','),
sortingMethod: transformedModel.length > 0 ? transformedModel[0].sort : 'desc',
});
export const formatDateToDDMMYYYY = (dateString: string | null | undefined) => {
if (!dateString) return '';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
};
const normalisePropsGetSsp = (params: TGetListDataTableDigunggung) => ({
...params,
namaDok: params.dokumen_referensi?.[0]?.namaDok || '',
nomorDok: params.dokumen_referensi?.[0].nomorDok || '',
id: params.id,
npwpPemotong: params.npwpPemotong,
idBupot: params.idBupot,
internal_id: params.internal_id,
fgStatus: params.fgStatus,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
revNo: params.revNo,
thnPajak: params.thnPajak,
msPajak: params.msPajak,
kdObjPjk: params.kdObjPjk,
noBupot: params.noBupot,
idDipotong: params.userId,
glAccount: params.glAccount,
jmlBruto: params.jmlBruto,
dpp: params.jmlBruto ?? '',
pphDipotong: params.pphDipotong,
created_at: formatDateToDDMMYYYY(params.created_at),
updated_at: formatDateToDDMMYYYY(params.updated_at),
});
export const normalizeExistingSsp = (res: TGetListDataTableDigunggung) => ({
// 🧾 Data Pajak Utama
tglPemotongan: res.tglpemotongan ?? '',
thnPajak: res.thnPajak ?? '',
msPajak: res.msPajak ?? '',
// 👤 Identitas Dipotong
idDipotong: res.npwpPemotong ?? '',
// 💰 Pajak dan Penghasilan
kdObjPjk: res.kdObjPjk ?? '',
fgFasilitas: res.fgFasilitas ?? '', // ✅ FIX
noDokLainnya: res.noDokLainnya ?? '', // ✅ FIX
jmlBruto: res.jmlBruto ?? '',
dpp: res.jmlBruto ?? '',
tarif: String(res.tarif ?? ''),
pphDipotong: String(res.pphDipotong ?? ''),
// ⚙️ Tambahan field penting
pasalPph: res.kdJnsPjk ?? '',
statusPPh: res.statusPPh ?? '',
kap: res.kap ?? '',
kjs: res.kjs ?? '',
// 📄 Dokumen Referensi
namaDok: res.dokumen_referensi?.[0]?.namaDok ?? '',
nomorDok: res.dokumen_referensi?.[0]?.nomorDok ?? '',
tglDok: res.dokumen_referensi?.[0]?.tglDok ?? '',
// 🏢 Cabang / Unit
// idTku: res.idTku ?? '',
// 🆔 Metadata tambahan
idBupot: res.idBupot ?? '',
noBupot: res.noBupot ?? '',
revNo: res.revNo ?? 0,
});
const normalizeParams = (params: any) => {
const {
page = 0,
pageSize = params.limit ?? 10,
sort,
filter,
advanced,
sortingMode: sortingModeParam,
sortingMethod: sortingMethodParam,
...rest
} = params;
let sortPayload: any;
let sortingMode = sortingModeParam || '';
let sortingMethod = sortingMethodParam || '';
if (sort) {
try {
const parsed = JSON.parse(sort);
if (Array.isArray(parsed) && parsed.length > 0) {
sortPayload = parsed;
sortingMode = parsed[0]?.field ?? sortingMode;
sortingMethod = parsed[0]?.sort ?? sortingMethod;
}
} catch {
sortPayload = [];
}
}
return {
page: page + 1,
limit: pageSize,
advanced:
typeof advanced === 'string' && advanced.trim() !== ''
? advanced
: filter && !isEmpty(JSON.parse(filter))
? filter
: undefined,
...(sortPayload ? { sort: sortPayload } : {}),
sortingMode,
sortingMethod,
...rest,
};
};
export const useGetDigunggung = ({ params }: { params: any }) => {
const { page, limit, advanced, sortingMode, sortingMethod } = params;
const normalized = normalizeParams(params);
return useQuery<TGetDigunggungApiWrapped>({
queryKey: ['ssp', page, limit, advanced, sortingMode, sortingMethod],
queryFn: async () => {
const res: any = await digunggungApi.getDigunggung({ params: normalized });
const rawData: any[] = Array.isArray(res?.data) ? res.data : res?.data ? [res.data] : [];
const total = Number(res?.total ?? res?.totalRow ?? 0);
let dataArray: TGetListDataTableDigunggungResult[] = [];
const normalizeWithWorker = () =>
new Promise<TGetListDataTableDigunggungResult[]>((resolve, reject) => {
try {
const worker = new Worker(
new URL('../workers/normalizeDigunggung.worker.js', import.meta.url),
{ type: 'module' }
);
worker.onmessage = (e) => {
const { data, error } = e.data;
if (error) {
worker.terminate();
reject(new Error(error));
} else {
worker.terminate();
resolve(data as TGetListDataTableDigunggungResult[]);
}
};
worker.onerror = (err) => {
worker.terminate();
reject(err);
};
worker.postMessage(rawData);
} catch (err) {
reject(err);
}
});
try {
if (typeof Worker !== 'undefined') {
dataArray = await normalizeWithWorker();
} else {
console.warn('⚠️ Worker not supported, using sync normalization');
dataArray = rawData.map(
normalisePropsGetSsp
) as unknown as TGetListDataTableDigunggungResult[];
}
} catch (err) {
console.error('❌ Worker failed, fallback to sync normalize:', err);
dataArray = rawData.map(
normalisePropsGetSsp
) as unknown as TGetListDataTableDigunggungResult[];
}
return {
data: dataArray,
total,
pageSize: normalized.limit,
page: normalized.page,
};
},
placeholderData: (prev: any) => prev,
refetchOnWindowFocus: false,
refetchOnMount: false,
staleTime: 0,
gcTime: 0,
retry: false,
});
};
export const useGetDigunggungById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.digunggung.detail(id),
queryFn: async () => {
const res = await digunggungApi.getDigunggungById(id);
if (!res) throw new Error('Data tidak ditemukan');
const normalized = normalizeExistingSsp(res);
return normalized;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export default useGetDigunggung;
import { useQuery } from '@tanstack/react-query';
import type { TBaseResponseAPI, TGetListDataKOPDigunggungResult } from '../types/types';
import queryKey from '../constant/queryKey';
import digunggungApi from '../utils/api';
const useGetKodeObjekPajakDigunggung = (params?: Record<string, any>) =>
useQuery<TBaseResponseAPI<TGetListDataKOPDigunggungResult>>({
queryKey: queryKey.getKodeObjekPajak(params),
queryFn: () => digunggungApi.getKodeObjekPajakDigunggung(params),
});
export default useGetKodeObjekPajakDigunggung;
/* eslint-disable @typescript-eslint/no-shadow */
import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import type { TGetListDataKOPDigunggung } from '../types/types';
const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDigunggung) => {
const { watch, setValue, control } = useFormContext();
// ambil value dari form
const fgFasilitas = watch('fgFasilitas');
const fgIdDipotong = watch('fgIdDipotong');
// mapping statusPPh ke isFinal
const isFinal = kodeObjekPajakSelected?.statuspph?.toLowerCase() === 'final' ? 1 : 0;
const updateTarifValues = () => {
if (kodeObjekPajakSelected) {
let valueTarif = Number(kodeObjekPajakSelected.tarif) || 0;
if (fgFasilitas === '6') {
valueTarif = 0.5;
} else if (fgFasilitas === '8') {
valueTarif = 0;
}
setValue('tarif', valueTarif, { shouldValidate: true });
setValue('tarifLt', fgIdDipotong === '1' && isFinal === 0 ? '100' : '0', {
shouldValidate: true,
});
}
};
// watch field yang mempengaruhi perhitungan
const handlerSetPphDipotong = useWatch({
control,
name: ['thnPajak', 'fgFasilitas', 'fgIdDipotong', 'jmlBruto', 'tarif'],
});
const calculateAndSetPphDipotong = (
thnPajak: number,
fgFasilitas: string,
fgIdDipotong: string,
jmlBruto: number,
tarif: number
) => {
if (kodeObjekPajakSelected) {
const valTarif = thnPajak < 2024 && fgIdDipotong === '1' && isFinal === 0 ? tarif * 2 : tarif;
const valPphDipotong =
fgFasilitas === '8' // contoh: fasilitas tertentu PPh 0
? 0
: (jmlBruto * valTarif) / 100;
setValue('pphDipotong', Math.round(valPphDipotong || 0), {
shouldValidate: true,
});
}
};
useEffect(() => {
if (handlerSetPphDipotong.filter((item) => !item).length < 2) {
calculateAndSetPphDipotong(
Number(handlerSetPphDipotong[0]),
handlerSetPphDipotong[1] as string,
handlerSetPphDipotong[2] as string,
Number(handlerSetPphDipotong[3]),
Number(handlerSetPphDipotong[4])
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handlerSetPphDipotong]);
return {
updateTarifValues,
};
};
export default usePphDipotong;
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import type { TPostDigunggungRequest } from '../types/types';
import digunggungApi from '../utils/api';
/**
* Transformasi data form menjadi payload untuk endpoint IF_TXR_070
*/
const transformParams = ({
isPengganti = false,
...digunggungData
}: any): TPostDigunggungRequest => {
const {
id,
idBupot,
noBupot,
kanal,
npwpPemotong,
idTku,
msPajak,
tahunPajak,
fgFasilitas,
noDokLainnya,
kdObjPjk,
pasalPph,
statusPph,
dpp,
tarif,
pphDipotong,
kap,
kjs,
dokReferensi,
tglPemotongan,
userId,
revNo: initialRevNo,
glAccount,
namaDok,
nomorDok,
tglDok,
// tambahkan field yang hilang:
fgNpwpNik,
npwp,
nik,
nama,
idAkun,
} = digunggungData;
const revNo = isPengganti
? parseInt(initialRevNo?.toString() || '0', 10) + 1
: parseInt(initialRevNo?.toString() || '0', 10);
return {
id: !isPengganti ? (id ?? null) : null,
noBupot: noBupot ?? '',
idBupot: idBupot ?? '',
kanal: kanal ?? '14',
npwpPemotong: npwpPemotong ?? '',
idTku: idTku ?? '',
fgTransaction: 'NEW', // ✅ WAJIB ADA
revNo,
masaPajak: msPajak ? String(msPajak).padStart(2, '0') : '',
tahunPajak: Number(tahunPajak ?? dayjs().year()),
fgNpwpNik: fgNpwpNik ?? true, // ✅ WAJIB ADA
npwp: npwp ?? '',
nik: nik ?? '',
nama: nama ?? '',
idAkun: idAkun ?? '',
sertifikatInsentifDipotong: fgFasilitas ?? '9',
nomorSertifikatInsentif: noDokLainnya ?? '0',
kodeObjekPajak: kdObjPjk ?? '',
pasalPph: pasalPph ?? '',
statusPph: statusPph ?? '',
dpp: Number(dpp ?? '0'),
tarif: Number(tarif ?? '0'),
pphDipotong: Number(pphDipotong ?? '0'),
kap: Number(kap ?? '0'),
kjs: Number(kjs ?? '0'),
dokReferensi: (() => {
if (dokReferensi && Array.isArray(dokReferensi) && dokReferensi.length > 0)
return dokReferensi;
if (!namaDok || !nomorDok || !tglDok) return [];
const parsedDate = dayjs(tglDok);
const formatted = parsedDate.isValid() ? parsedDate.format('DDMMYYYY') : '';
return [
{
dokReferensi: namaDok,
nomorDokumen: nomorDok,
tanggal_Dokumen: formatted,
},
];
})(),
tglPemotongan: tglPemotongan ? dayjs(tglPemotongan).format('DDMMYYYY') : '',
userId: userId ?? '',
glAccount: glAccount ?? '',
};
};
/**
* Hook untuk menyimpan data SSP ke endpoint IF_TXR_070
*/
const useSaveDigunggung = (props?: any) =>
useMutation({
mutationKey: ['save-digunggung'],
mutationFn: (params: any) => digunggungApi.saveDigunggung(transformParams(params)),
...props,
});
export default useSaveDigunggung;
// hooks/useUpload.ts
import { useMutation } from '@tanstack/react-query';
import nrApi from '../utils/api';
const useUpload = (props?: any) =>
useMutation({
mutationKey: ['upload-digunggung'],
mutationFn: (payload: { id: string | number }) => nrApi.upload(payload),
...props,
});
export default useUpload;
import { create } from 'zustand';
console.log('✅ pagination store created');
type TableKey = string;
interface TablePagination {
page: number;
pageSize: number;
}
interface TableFilter {
items: any[];
}
interface PaginationState {
tables: Record<TableKey, TablePagination>;
filters: Record<TableKey, TableFilter>;
setPagination: (table: TableKey, next: Partial<TablePagination>) => void;
resetPagination: (table: TableKey) => void;
setFilter: (table: TableKey, next: Partial<TableFilter>) => void;
resetFilter: (table: TableKey) => void;
}
export const usePaginationStore = create<PaginationState>((set) => ({
tables: {},
filters: {},
setPagination: (table, next) =>
set((state) => {
const prev = state.tables[table] ?? { page: 0, pageSize: 10 };
return {
tables: {
...state.tables,
[table]: {
page: next.page ?? prev.page,
pageSize: next.pageSize ?? prev.pageSize,
},
},
};
}),
resetPagination: (table) =>
set((state) => ({
tables: {
...state.tables,
[table]: { page: 0, pageSize: state.tables[table]?.pageSize ?? 10 },
},
})),
setFilter: (table, next) =>
set((state) => ({
filters: {
...state.filters,
[table]: {
items: next.items ?? state.filters[table]?.items ?? [],
},
},
})),
resetFilter: (table) =>
set((state) => ({
filters: {
...state.filters,
[table]: { items: [] },
},
})),
}));
export type TBaseResponseAPI<T> = {
status: string;
message: string;
data: T;
time: string;
code: number;
metaPage: TBaseResponseMetaPage;
total?: number;
};
type TBaseResponseMetaPage = {
pageNum: number | null;
rowPerPage: number | null;
totalRow: number;
};
export type TGetListDataTableDigunggung = {
id: number;
npwpPemotong: string;
nitkuPemotong: string;
msPajak: string;
thnPajak: string;
kdObjPjk: string;
kdJnsPjk: string;
statusPPh: string;
jmlBruto: string;
tarif: string;
pphDipotong: string;
kap: string;
kjs: string;
fgFasilitas: string;
noDokLainnya: string;
tglpemotongan: string;
userId: string;
created_at: string;
updated_at: string;
created: string;
updated: string;
fgStatus: string;
internal_id: string;
dokumen_referensi: TDokReferensi[];
revNo: number;
noBupot: string | null;
idBupot: string | null;
glAccount: string;
glName: string | null;
};
export type TGetListDataTableDigunggungResult = TGetListDataTableDigunggung[];
export type TGetListDataKOPDigunggung = {
kode: string;
nama: string;
pasal: string;
statuspph: string;
normanetto: string;
tarif: string;
kap: string;
kjs: string;
noCertificate: number;
certofDomicile: number;
otherCert: number;
};
export type TGetListDataKOPDigunggungResult = TGetListDataKOPDigunggung[];
export type ActionItem = {
title: string;
icon: React.ReactNode;
func?: () => void;
disabled?: boolean;
};
export type TDokReferensi = {
namaDok: string;
nomorDok: string;
tglDok: string; // format: DDMMYYYY
};
export type TPostDigunggungRequest = {
id: string | null;
idBupot?: string | null;
noBupot?: string | null;
kanal: string;
npwpPemotong: string;
idTku: string;
fgTransaction: 'NEW' | 'UPDATE' | 'DELETE' | '';
revNo: number;
masaPajak: string;
tahunPajak: number;
fgNpwpNik: boolean;
npwp: string;
nik: string;
nama: string;
idAkun: string;
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPph: string;
statusPph: string;
kap: number;
kjs: number;
dokReferensi: TDokReferensi[];
dpp: number;
tarif: number;
pphDipotong: number;
tglPemotongan: string;
userId: string;
glAccount: string;
};
export type TPostUpload = {
id: string;
};
export type TDeleteRequest = {
id: string;
};
export type TCancelRequest = {
id: string | number;
tglPembatalan: string; // format: DDMMYYYY
};
export type TCancelResponse = TBaseResponseAPI<{
id: string | number;
statusBatal?: string;
message?: string;
}>;
import type {
TBaseResponseAPI,
TCancelRequest,
TCancelResponse,
TDeleteRequest,
TGetListDataKOPDigunggungResult,
TGetListDataTableDigunggungResult,
TPostDigunggungRequest,
} from '../types/types';
import unifikasiClient from './unifikasiClient';
const digunggungApi = () => {};
// API untuk get list table
digunggungApi.getDigunggung = async (config: any) => {
const {
data: { message, metaPage, data },
status: statusCode,
} = await unifikasiClient.get<TBaseResponseAPI<TGetListDataTableDigunggungResult>>(
'IF_TXR_074/',
{
...config,
}
);
if (statusCode !== 200) {
throw new Error(message);
}
return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
};
digunggungApi.getKodeObjekPajakDigunggung = async (params?: Record<string, any>) => {
const response = await unifikasiClient.get<TBaseResponseAPI<TGetListDataKOPDigunggungResult>>(
'/sandbox/mst_kop_bpcp',
{ params }
);
const body = response.data;
if (response.status !== 200 || body.status !== 'success') {
throw new Error(body.message);
}
return body;
};
digunggungApi.saveDigunggung = async (config: TPostDigunggungRequest) => {
const {
data: { message, data, code },
} = await unifikasiClient.post<TBaseResponseAPI<TPostDigunggungRequest>>('/IF_TXR_074/', {
...config,
});
if (code === 0) {
throw new Error(message);
}
return data;
};
digunggungApi.getDigunggungById = async (id: string) => {
const res = await unifikasiClient.get('/IF_TXR_074/', { params: { id } });
const {
data: { status, message, data },
status: statusCode,
} = res;
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('getDigunggungId failed:', { statusCode, status, message });
throw new Error(message || 'Gagal mengambil data Digunggung');
}
const dnData = Array.isArray(data) ? data[0] : data;
return dnData;
};
digunggungApi.upload = async ({ id }: { id: string | number }) => {
const {
data: { status, message, data, code },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_074/upload', { id });
return { status, message, data, code, statusCode };
};
digunggungApi.deleteDigunggung = async (
payload: TDeleteRequest,
config?: Record<string, any>
): Promise<any> => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post<TBaseResponseAPI<any>>('/IF_TXR_074/delete', payload, {
...config,
});
if (statusCode !== 200 || status?.toLowerCase() === 'error') {
throw new Error(message || 'Gagal menghapus data Digunggung');
}
return data;
};
digunggungApi.cancel = async ({ id, tglPembatalan }: TCancelRequest): Promise<TCancelResponse> => {
const {
data: { status, message, data, code, time, metaPage, total },
} = await unifikasiClient.post('/IF_TXR_074/batal', {
id,
tglPembatalan,
});
console.log('Cancel Digunggung response:', { code, message, status });
if (code === 0) {
throw new Error(message || 'Gagal membatalkan data');
}
return {
status,
message,
data,
code,
time,
metaPage,
total,
};
};
export default digunggungApi;
import dayjs from 'dayjs';
import { FG_FASILITAS_DN } from '../constant';
const FASILITAS_LABEL_MAP: Record<string, string> = {
[FG_FASILITAS_DN.SKB_PPH_PASAL_22]: 'SKB PPh Pasal 22',
[FG_FASILITAS_DN.SKB_PPH_PASAL_23]: 'SKB PPh Pasal 23',
[FG_FASILITAS_DN.SKB_PPH_PHTB]: 'SKB PPh PHTB',
[FG_FASILITAS_DN.DTP]: 'DTP',
[FG_FASILITAS_DN.SKB_PPH_BUNGA_DEPOSITO_DANA_PENSIUN_TABUNGAN]:
'SKB PPh Bunga Deposito Dana Pensiun Tabungan',
[FG_FASILITAS_DN.SUKET_PP23_PP52]: 'Suket PP23/PP52',
[FG_FASILITAS_DN.SKD_WPLN]: 'SKD WPLN',
[FG_FASILITAS_DN.FASILITAS_LAINNYA]: 'Fasilitas Lainnya',
[FG_FASILITAS_DN.TANPA_FASILITAS]: 'Tanpa Fasilitas',
[FG_FASILITAS_DN.SKB_PPH_PASAL_21]: 'SKB PPh Pasal 21',
[FG_FASILITAS_DN.DTP_PPH_PASAL_21]: 'DTP PPh Pasal 21',
};
const formatTanggalIndo = (isoDate?: string): string => {
if (!isoDate) return '';
return dayjs(isoDate).locale('id').format('DD MMMM YYYY');
};
/**
* Normalisasi payload Bupot Unifikasi agar sesuai format yang digunakan API cetak PDF
*/
export const normalizePayloadCetakPdf = (payload: Record<string, any>) => {
if (!payload) return payload;
const adjusted = { ...payload };
if (adjusted.tglpemotongan) {
adjusted.tglPemotongan = formatTanggalIndo(adjusted.tglpemotongan); // versi tampil
}
// === Konversi kode fasilitas ke label ===
const fasilitasCode = adjusted.sertifikatInsentifDipotong;
adjusted.sertifikatInsentifDipotong = FASILITAS_LABEL_MAP[fasilitasCode] || fasilitasCode || '';
// === Field default tambahan ===
adjusted.mixcode = adjusted.mixcode || 'mixcode';
adjusted.qrcode = adjusted.qrcode || 'qrcode';
adjusted.metodePembayaranBendahara = adjusted.metodePembayaranBendahara || '-';
adjusted.nomorSP2D = adjusted.nomorSP2D || '-';
adjusted.npwpDipotong = adjusted.npwp || '';
adjusted.namaDipotong = adjusted.namaDipotong || '';
adjusted.nitkuDipotong = adjusted.nik || '';
adjusted.namaPemotong = adjusted.namaDipotong || '';
adjusted.nitkuPemotong = adjusted.idTku || '';
adjusted.penghasilanBruto = adjusted.penghasilanBruto || '';
adjusted.tanggal_Dokumen = adjusted.dokumen_referensi[0].tanggal_Dokumen;
adjusted.status = 'Proforma';
adjusted.msPajak = adjusted.masaPajak;
adjusted.thnPajak = adjusted.tahunPajak;
adjusted.kdObjPjk = adjusted.kodeObjekPajak;
adjusted.fgPdf = adjusted.fgPdf === 'TIDAK_TERSEDIA' ? '2' : adjusted.fgPdf;
return adjusted;
};
export default normalizePayloadCetakPdf;
import axios from 'axios';
const BASE_URL = `https://nodesandbox.pajakexpress.id:1837`;
const unifikasiClient = axios.create({
baseURL: BASE_URL,
validateStatus(status) {
return (status >= 200 && status < 300) || status === 500;
},
});
// Interceptor untuk selalu update token dari localStorage
unifikasiClient.interceptors.request.use((config) => {
const jwtAccessToken = localStorage.getItem('jwt_access_token');
const xToken = localStorage.getItem('x-token');
if (jwtAccessToken) {
config.headers.Authorization = `Bearer ${jwtAccessToken}`;
}
if (xToken) {
config.headers['x-token'] = xToken;
}
return config;
});
export default unifikasiClient;
import dayjs from 'dayjs';
import { MIN_THN_PAJAK } from '../constant';
export const currentYear = dayjs().year();
export const getHighestStartingYear = (thnAwalUnifikasi: any) =>
Math.max(MIN_THN_PAJAK, thnAwalUnifikasi);
export const selectedInitialMonth = ({ thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
return highestYear > thnAwalUnifikasi ? '01' : masaAwalUnifikasi;
};
export const determineStartingMonth = ({ thnPajak, thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
const initialMonth = selectedInitialMonth({ thnAwalUnifikasi, masaAwalUnifikasi });
return thnPajak >= highestYear && thnPajak <= currentYear ? initialMonth : '';
};
// src/workers/normalizeDn.worker.js
// NOTE: keep this file plain JS - no TS imports - copy needed transform functions here.
// function formatDateToDDMMYYYY(dateString) {
// if (!dateString) return '';
// const d = new Date(dateString);
// const day = String(d.getDate()).padStart(2, '0');
// const month = String(d.getMonth() + 1).padStart(2, '0');
// const year = d.getFullYear();
// return `${day}/${month}/${year}`;
// }
// minimal transform helpers used in normalize
function transformFgStatusToFgSignStatus(fgStatus) {
const splitted = (fgStatus || '').split('-') || [];
if (splitted.includes('SIGN') > 0) return 'FAILED';
if (splitted.includes('SIGNING IN PROGRESS')) return 'IN_PROGRESS';
switch (splitted[1]) {
case 'document signed successfully':
case 'Done':
return 'SIGNED';
default:
return null;
}
}
function getFgStatusPdf(link, fgSignStatus) {
if (!link || fgSignStatus === 'IN_PROGRESS') return 'TIDAK_TERSEDIA';
if (!link.includes('https://coretaxdjp.pajak.go.id/')) return 'BELUM_TERBENTUK';
return 'TERBENTUK';
}
function normalisePropsGetDigunggung(params) {
if (!params) return params;
return {
...params,
namaDok: params.dokumen_referensi?.[0]?.namaDok || '',
nomorDok: params.dokumen_referensi?.[0]?.nomorDok || '',
id: params.id,
internal_id: params.internal_id,
fgStatus: params.fgStatus,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
};
}
// eslint-disable-next-line func-names
onmessage = function (e) {
const { data } = e;
// data should be array of items
if (!Array.isArray(data)) {
postMessage({ error: 'expected array' });
return;
}
try {
const out = data.map(normalisePropsGetDigunggung);
console.log(out);
postMessage({ data: out });
} catch (err) {
postMessage({ error: (err && err.message) || String(err) });
}
};
...@@ -14,6 +14,7 @@ interface ModalDeleteDnProps { ...@@ -14,6 +14,7 @@ interface ModalDeleteDnProps {
isOpenDialogDelete: boolean; isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void; setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string; successMessage?: string;
onConfirmDelete?: () => Promise<void> | void;
} }
/** /**
...@@ -51,6 +52,7 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({ ...@@ -51,6 +52,7 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({
isOpenDialogDelete, isOpenDialogDelete,
setIsOpenDialogDelete, setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus', successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -99,6 +101,9 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({ ...@@ -99,6 +101,9 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({
setIsOpenDialogProgressBar(true); setIsOpenDialogProgressBar(true);
await handleMultipleDelete(); await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' }); enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmDelete?.();
handleCloseModal(); handleCloseModal();
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: any) {
......
...@@ -14,7 +14,7 @@ import { useSelector } from 'react-redux'; ...@@ -14,7 +14,7 @@ import { useSelector } from 'react-redux';
import type { RootState } from 'src/store'; import type { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement'; import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { LoadingButton } from '@mui/lab'; import Button from '@mui/material/Button';
interface ModalUploadDnProps { interface ModalUploadDnProps {
dataSelected?: GridRowSelectionModel; dataSelected?: GridRowSelectionModel;
...@@ -118,6 +118,10 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -118,6 +118,10 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
setIsOpenDialogProgressBar(true); setIsOpenDialogProgressBar(true);
await handleMultipleDelete(); await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' }); enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
await onConfirmUpload?.();
handleCloseModal(); handleCloseModal();
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: any) {
...@@ -155,25 +159,16 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -155,25 +159,16 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
</Grid> </Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}> <Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<LoadingButton <Button
type="button" type="button"
disabled={!isCheckedAgreement} disabled={!isCheckedAgreement}
// onClick={onSubmit} onClick={onSubmit}
onClick={async () => {
if (onConfirmUpload) {
await onConfirmUpload();
setIsOpenDialogUpload(false);
return;
}
await onSubmit();
}}
loading={uploadDn.isPending} loading={uploadDn.isPending}
variant="contained" variant="contained"
sx={{ background: '#143B88' }} sx={{ background: '#143B88' }}
> >
Save Save
</LoadingButton> </Button>
</Stack> </Stack>
</Stack> </Stack>
</DialogUmum> </DialogUmum>
......
...@@ -111,7 +111,8 @@ const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => { ...@@ -111,7 +111,8 @@ const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => {
<Field.DatePicker <Field.DatePicker
name="msPajak" name="msPajak"
label="Masa Pajak" label="Masa Pajak"
view="month" views={['year', 'month']} // ✅ valid prop
openTo="month"
format="MM" format="MM"
disabled={isPengganti} disabled={isPengganti}
/> />
......
...@@ -585,6 +585,9 @@ export function DnListView() { ...@@ -585,6 +585,9 @@ export function DnListView() {
isOpenDialogDelete={isDeleteModalOpen} isOpenDialogDelete={isDeleteModalOpen}
setIsOpenDialogDelete={setIsDeleteModalOpen} setIsOpenDialogDelete={setIsDeleteModalOpen}
successMessage="Data berhasil dihapus" successMessage="Data berhasil dihapus"
onConfirmDelete={async () => {
await refetch();
}}
/> />
)} )}
...@@ -596,6 +599,9 @@ export function DnListView() { ...@@ -596,6 +599,9 @@ export function DnListView() {
isOpenDialogUpload={isUploadModalOpen} isOpenDialogUpload={isUploadModalOpen}
setIsOpenDialogUpload={setIsUploadModalOpen} setIsOpenDialogUpload={setIsUploadModalOpen}
successMessage="Data berhasil diupload" successMessage="Data berhasil diupload"
onConfirmUpload={async () => {
await refetch();
}}
/> />
)} )}
...@@ -607,6 +613,9 @@ export function DnListView() { ...@@ -607,6 +613,9 @@ export function DnListView() {
isOpenDialogCancel={isCancelModalOpen} isOpenDialogCancel={isCancelModalOpen}
setIsOpenDialogCancel={setIsCancelModalOpen} setIsOpenDialogCancel={setIsCancelModalOpen}
successMessage="Data berhasil diupload" successMessage="Data berhasil diupload"
onConfirmCancel={async () => {
await refetch();
}}
/> />
)} )}
......
...@@ -22,22 +22,24 @@ const formatDateDDMMYYYY = (d: Date) => { ...@@ -22,22 +22,24 @@ const formatDateDDMMYYYY = (d: Date) => {
return `${dd}${mm}${yyyy}`; return `${dd}${mm}${yyyy}`;
}; };
interface ModalCancelDnProps { interface ModalCancelNrProps {
dataSelected?: any[]; dataSelected?: any[];
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>; setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>; tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean; isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void; setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string; successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
} }
const ModalCancelNr: React.FC<ModalCancelDnProps> = ({ const ModalCancelNr: React.FC<ModalCancelNrProps> = ({
dataSelected = [], dataSelected = [],
setSelectionModel, setSelectionModel,
tableApiRef, tableApiRef,
isOpenDialogCancel, isOpenDialogCancel,
setIsOpenDialogCancel, setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan', successMessage = 'Data berhasil dibatalkan',
onConfirmCancel,
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -122,6 +124,8 @@ const ModalCancelNr: React.FC<ModalCancelDnProps> = ({ ...@@ -122,6 +124,8 @@ const ModalCancelNr: React.FC<ModalCancelDnProps> = ({
if (success.length > 0) { if (success.length > 0) {
enqueueSnackbar(successMessage, { variant: 'success' }); enqueueSnackbar(successMessage, { variant: 'success' });
processSuccess(); processSuccess();
await onConfirmCancel?.();
} }
// ✅ update cache data lokal agar status langsung berubah // ✅ update cache data lokal agar status langsung berubah
......
...@@ -14,6 +14,7 @@ interface ModalDeleteDnProps { ...@@ -14,6 +14,7 @@ interface ModalDeleteDnProps {
isOpenDialogDelete: boolean; isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void; setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string; successMessage?: string;
onConfirmDelete?: () => Promise<void> | void;
} }
/** /**
...@@ -51,6 +52,7 @@ const ModalDeleteNr: React.FC<ModalDeleteDnProps> = ({ ...@@ -51,6 +52,7 @@ const ModalDeleteNr: React.FC<ModalDeleteDnProps> = ({
isOpenDialogDelete, isOpenDialogDelete,
setIsOpenDialogDelete, setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus', successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -99,6 +101,9 @@ const ModalDeleteNr: React.FC<ModalDeleteDnProps> = ({ ...@@ -99,6 +101,9 @@ const ModalDeleteNr: React.FC<ModalDeleteDnProps> = ({
setIsOpenDialogProgressBar(true); setIsOpenDialogProgressBar(true);
await handleMultipleDelete(); await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' }); enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmDelete?.();
handleCloseModal(); handleCloseModal();
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: any) {
......
...@@ -114,7 +114,8 @@ const Identitas = ({ isPengganti, existingNr, country }: IdentitasProps) => { ...@@ -114,7 +114,8 @@ const Identitas = ({ isPengganti, existingNr, country }: IdentitasProps) => {
<Field.DatePicker <Field.DatePicker
name="masaPajak" name="masaPajak"
label="Masa Pajak" label="Masa Pajak"
view="month" views={['year', 'month']} // ✅ valid prop
openTo="month"
format="MM" format="MM"
disabled={isPengganti} disabled={isPengganti}
/> />
......
...@@ -53,7 +53,8 @@ const Identitas = ({ isPengganti, existingSsp }: IdentitasProps) => { ...@@ -53,7 +53,8 @@ const Identitas = ({ isPengganti, existingSsp }: IdentitasProps) => {
<Field.DatePicker <Field.DatePicker
name="msPajak" name="msPajak"
label="Masa Pajak" label="Masa Pajak"
view="month" views={['year', 'month']} // ✅ valid prop
openTo="month"
format="MM" format="MM"
disabled={isPengganti} disabled={isPengganti}
/> />
......
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