Commit e1874185 authored by Rais Aryaguna's avatar Rais Aryaguna

feat: add query keys and refactor helper functions for bupot-21-26 section

- Introduced a new file for query keys to manage API query keys consistently.
- Refactored the helper functions to simplify the calculation logic for BUPOT 21.
- Updated hitung.ts to handle additional calculation types including harian, pasal17, pensiun, and pesangon.
- Added a new mutation hook for handling calculations for final and tidak final BUPOT.
parent d48fb313
......@@ -55,6 +55,7 @@ const {
faktur_keterangan,
faktur_idtambahan,
satuan,
mst_kop_21,
} = endpoints.masterData;
export function useKodeNegara(): UseKodeNegaraReturn {
......@@ -163,13 +164,14 @@ export function usePenandatangan() {
// ----------------------------------------------------------------------
type KodeObjekPajakType = 'bpnr' | 'bpu' | 'bpsp' | 'all';
type KodeObjekPajakType = 'kop_21' | 'bpnr' | 'bpu' | 'bpsp' | 'all';
const KODE_OBJEK_PAJAK_ENDPOINTS: Record<KodeObjekPajakType, string> = {
bpnr: kop_bpnr,
bpu: kop_bpu,
bpsp: kop_bpsp,
all: kop_all,
kop_21: mst_kop_21,
} as const;
export function useKodeObjekPajak(type: KodeObjekPajakType, params?: QueryParams) {
......
......@@ -162,6 +162,12 @@ export const endpoints = {
upload: '/IF_TXR_028/a0/upload',
canceled: '/IF_TXR_028/a0/batal',
},
fnlTdkFnl: {
list: '/IF_TXR_028/21',
delete: '/IF_TXR_028/21/delete',
upload: '/IF_TXR_028/21/upload',
canceled: '/IF_TXR_028/21/batal',
}
},
masterData: {
kodeNegara: '/sandbox/mst_negara',
......@@ -173,6 +179,7 @@ export const endpoints = {
kop_bpu: '/sandbox/mst_kop_bpu',
kop_bpsp: '/sandbox/mst_kop_bpsp',
kop_all: '/sandbox/mst_kop_all',
mst_kop_21: '/sandbox/mst_kop_21',
faktur_keterangan: '/sandbox/mst_faktur_keterangan',
faktur_idtambahan: '/sandbox/mst_faktur_idtambahan',
},
......
import { CONFIG } from 'src/global-config';
import { FinalTidakFinalRekamView } from 'src/sections/bupot-21-26/bupot-final-tidak-Final/view';
const metadata = { title: `E-Bupot Bulanan Rekam- ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FinalTidakFinalRekamView />
</>
);
}
import { CONFIG } from 'src/global-config';
import { BupotfinalListView } from 'src/sections/bupot-21-26/bupot-final-tidak-Final/view';
const metadata = { title: `E-Bupot 21/26- ${CONFIG.appName}` };
......@@ -7,7 +8,7 @@ export default function Page() {
<>
<title>{metadata.title}</title>
<p>Bupot Final/Tidak Final</p>
<BupotfinalListView/>
</>
);
}
......@@ -113,7 +113,8 @@ export const paths = {
bulananUbah: (id: string) => `${ROOTS.PPH21}/bulanan/${id}/ubah`,
bulananPengganti: (id: string) => `${ROOTS.PPH21}/bulanan/${id}/pengganti`,
bupotFinal: `${ROOTS.PPH21}/bupot-final`,
detailsBupotFinal: (id: string) => `${ROOTS.PPH21}/bupot-final/${id}`,
bupotFinalRekam: `${ROOTS.PPH21}/bupot-final/rekam`,
bupotFinalEdit: (id: string, path:string) => `${ROOTS.PPH21}/bupot-final/${id}/${path}`,
tahuan: `${ROOTS.PPH21}/tahunan`,
detailstahuan: (id: string) => `${ROOTS.PPH21}/tahunan/${id}`,
tahunanA2: `${ROOTS.PPH21}/tahunan-a2`,
......
......@@ -59,6 +59,9 @@ const AccountChangePasswordPage = lazy(
const OverviewBupotBulananPage = lazy(() => import('src/pages/pph21/bupotBulanan'));
const OverviewBupotBulananRekamPage = lazy(() => import('src/pages/pph21/bupotBulananRekam'));
const OverviewBupotFinalTdkFinalPage = lazy(() => import('src/pages/pph21/bupotFinaltidakFinal'));
const OverviewBupotFinalTdkFinalRekamPage = lazy(
() => import('src/pages/pph21/bupotFinalTidakFinalRekam')
);
const OverviewBupotA1Page = lazy(() => import('src/pages/pph21/bupoTahunanA1'));
const OverviewBupotPasal26Page = lazy(() => import('src/pages/pph21/bupotPasal26'));
......@@ -144,6 +147,8 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'bulanan/rekam', element: <OverviewBupotBulananRekamPage /> },
{ path: 'bulanan/:id/:type', element: <OverviewBupotBulananRekamPage /> },
{ path: 'bupot-final', element: <OverviewBupotFinalTdkFinalPage /> },
{ path: 'bupot-final/rekam', element: <OverviewBupotFinalTdkFinalRekamPage /> },
{ path: 'bupot-final/:id/:type', element: <OverviewBupotFinalTdkFinalRekamPage /> },
{ path: 'tahunan', element: <OverviewBupotA1Page /> },
{ path: 'bupot-26', element: <OverviewBupotPasal26Page /> },
],
......
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 '../table/CustomFilterButton';
import CustomColumnsButton from '../table/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>
);
});
......@@ -11,9 +11,9 @@ import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { useAppSelector } from 'src/store';
import queryKey, { appRootKey, bulanan } from '../constant/queryKey';
import useUploadBulanan from '../hooks/useUploadeBulanan';
import { createTableKey, useTablePagination } from '../../paginationStore';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import useUploadBulanan from '../../hooks/useUploadeBulanan';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface DialogPenandatanganProps {
dataSelected?: GridRowSelectionModel;
......
import { Button, Stack, Typography } from '@mui/material';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { useQueryClient } from '@tanstack/react-query';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import minMax from 'dayjs/plugin/minMax';
import { enqueueSnackbar } from 'notistack';
import React, { useEffect, useMemo, useState } from 'react';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { createTableKey, useTablePagination } from '../../../paginationStore';
import useCencelBulanan from '../../hooks/useCencelBulanan';
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 ModalCancelDnProps {
dataSelected?: any[];
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
}
const ModalCancelBulanan: React.FC<ModalCancelDnProps> = ({
dataSelected = [],
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan',
}) => {
const queryClient = useQueryClient();
const TABLE_KEY = useMemo(() => createTableKey(appRootKey, bulanan), []);
const [paginationState] = useTablePagination(TABLE_KEY);
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 } = useCencelBulanan({
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();
}
// ✅ update cache data lokal agar status langsung berubah
queryClient.setQueryData(queryKey.bulanan.all(''), (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: queryKey.bulanan.all({page: paginationState.page + 1, limit: paginationState.pageSize}) });
// ⚠️ 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 ModalCancelBulanan;
......@@ -4,7 +4,7 @@ import DialogContent from '@mui/material/DialogContent';
import { enqueueSnackbar } from 'notistack';
import { useEffect, useState } from 'react';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import { useCetakBulanan } from '../../cetakpdf';
import { useCetakBulanan } from '../../../cetakpdf';
interface ModalCetakPdfDnProps {
payload?: Record<string, any>;
......
......@@ -5,9 +5,9 @@ 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 useDeleteBulanan from '../hooks/useDeleteBulanan';
import queryKey, { appRootKey, bulanan } from '../constant/queryKey';
import { createTableKey, useTablePagination } from '../../paginationStore';
import useDeleteBulanan from '../../hooks/useDeleteBulanan';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface ModalDeleteDnProps {
dataSelected?: GridRowSelectionModel;
......
......@@ -11,9 +11,9 @@ import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { useAppSelector } from 'src/store';
import queryKey, { appRootKey, bulanan } from '../constant/queryKey';
import useUploadBulanan from '../hooks/useUploadeBulanan';
import { createTableKey, useTablePagination } from '../../paginationStore';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import useUploadBulanan from '../../hooks/useUploadeBulanan';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface DialogPenandatanganProps {
dataSelected?: GridRowSelectionModel;
......
......@@ -2,7 +2,6 @@ import Grid from '@mui/material/Grid';
import dayjs from 'dayjs';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
// import { useParams } from 'react-router';
import { Field } from 'src/components/hook-form';
type IdentitasProps = {
......@@ -19,24 +18,6 @@ const Identitas = ({ isPengganti, kodeNegetaOptions }: IdentitasProps) => {
const tanggalPemotongan = watch('tglPemotongan');
const fgKaryawanAsing = watch('fgKaryawanAsing');
// const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0);
// const maxKeterangan = 5;
// const handleTambah = () => {
// if (jumlahKeterangan < maxKeterangan) {
// setJumlahKeterangan(jumlahKeterangan + 1);
// }
// };
// const handleHapus = () => {
// if (jumlahKeterangan > 0) {
// const newCount = jumlahKeterangan - 1;
// setJumlahKeterangan(newCount);
// setValue(`keterangan${newCount + 1}`, null);
// }
// };
// auto set thnPajak dan msPajak berdasarkan tanggalPemotongan
useEffect(() => {
if (!isPengganti) {
if (tanggalPemotongan) {
......
......@@ -5,13 +5,8 @@ import dayjs from 'dayjs';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
import { FG_FASILITAS_PPH_21, FG_PERHITUNGAN, FG_PERHITUNGAN_TEXT, METODE_POTONG } from 'src/sections/bupot-21-26/constant';
import { getHitungBulananErrorMessage, useHitungBulanan } from 'src/sections/bupot-21-26/hitung';
import {
FG_FASILITAS_PPH_21,
FG_PERHITUNGAN,
FG_PERHITUNGAN_TEXT,
METODE_POTONG,
} from '../../constant';
const fgPerhitunganOptions = Object.values(FG_PERHITUNGAN).map((value) => ({
value,
......
......@@ -2,7 +2,7 @@ import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { m } from 'framer-motion';
import type { FC } from 'react';
import { PANDUAN_REKAM_DN } from '../../constant';
import { PANDUAN_REKAM_DN } from 'src/sections/bupot-21-26/constant';
interface PanduanDnRekamProps {
handleOpen: () => void;
......
export const appRootKey = 'bupot-21-26';
export const bulanan = 'bulanan';
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
bulanan: {
all: (params: any) => [appRootKey, 'bulanan', params],
detail: (params: any) => [appRootKey, 'bulanan', 'detail', params],
draft: [appRootKey, 'bulanan', 'draft'],
delete: [appRootKey, 'bulanan', 'delete'],
upload: [appRootKey, 'bulanan', 'upload'],
cancel: [appRootKey, 'bulanan', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'bulanan-cetak-pdf', params],
},
};
export default queryKey;
// /* eslint-disable consistent-return */
// /* eslint-disable no-useless-return */
// import useAdvancedSearch from '@pjap/shared/hooks/useAdvancedSearch';
// import {
// transformComparisonOperatorToPatternMatchingOperator,
// transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValueToQueryValueBasedOnModelOperator,
// } from '@pjap/shared/data-grid-premium/util';
// import {
// FG_PDF_STATUS,
// FG_SIGN_STATUS,
// FG_STATUS,
// } from '@pjap/pph21/app/bukti-potong/shared/constants/BupotConstant';
// import { addLeadingZero } from '@pjap/shared/utils/number';
// import { transformDateToCommonFormat } from '@pjap/pph21/utils/formatDate';
// const useBpuAdvancedSearch = () => {
// const fieldConfigs = {
// fgStatus: {
// fieldMapping: 'fgStatus',
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => {
// switch (value) {
// case FG_STATUS.SUBMITTED:
// return "'%SUBMITTED%'".toLowerCase();
// case FG_STATUS.NORMAL:
// return "'%NORMAL%'".toLowerCase();
// case FG_STATUS.NORMAL_PENGGANTI:
// return "'%AMENDMENT%'".toLowerCase();
// case FG_STATUS.BATAL:
// return "'%CANCELLED%'".toLowerCase();
// case FG_STATUS.DRAFT:
// return "'%DRAFT%'".toLowerCase();
// case FG_STATUS.DIGANTI:
// return "'%AMENDED%'".toLowerCase();
// case FG_STATUS.PENDING:
// return "'%PENDING%'".toLowerCase();
// case FG_STATUS.ON_SCHEDULE:
// return "'%ON SCHEDULE%'".toLowerCase();
// default:
// return `'%${value}%'`.toLowerCase();
// }
// },
// },
// fgSignStatus: {
// fieldMapping: 'fgStatus',
// operator: (currentOperator, _, value) => {
// if (value === FG_SIGN_STATUS.ERROR) return 'IN';
// if (currentOperator === '!=') return 'NOT LIKE';
// return 'LIKE';
// },
// transformValue: (value) => {
// switch (value) {
// case FG_SIGN_STATUS.IN_PROGRESS:
// return "'%SIGNING_IN_PROGRESS%'".toLowerCase();
// case FG_SIGN_STATUS.FAILED:
// return "'%DJP-SIGN-MASTER%'".toLowerCase();
// case FG_SIGN_STATUS.SIGNED:
// return "'%document signed successfully%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_IDBUPOT:
// return "'%NOT_MATCH_IDBUPOT%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_STATUS:
// return "'%NOT_MATCH_STATUS%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_NILAI:
// return "'%NOT_MATCH_NILAI%'".toLowerCase();
// case FG_SIGN_STATUS.DUPLICATE:
// return "'%DUPLICATE%'".toLowerCase();
// case FG_SIGN_STATUS.ERROR:
// return "('draft','normal-done','amendment-done','amended-document signed successfully','cancelled-done','submitted-signing_in_progress')";
// default:
// return `'%${value}%'`.toLowerCase();
// }
// },
// additionalQuery: (originalValue) => {
// switch (originalValue) {
// case FG_SIGN_STATUS.ERROR:
// return `"errorMsg" IS NOT NULL AND "errorMsg" != ''`;
// default:
// return ``;
// }
// },
// },
// fgPdf: {
// fieldMapping: 'link',
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => {
// switch (value) {
// case FG_PDF_STATUS.TERBENTUK:
// return "'%https://coretaxdjp.pajak.go.id%'";
// case FG_PDF_STATUS.BELUM_TERBENTUK:
// return `'%intranet.pajak.go.id%' OR ((link = '' OR link IS NULL) AND "fgStatus" IN ('NORMAL-document signed successfully', 'NORMAL-Done', 'AMENDED-document signed successfully', 'CANCELLED-document signed successfully', 'AMENDMENT-document signed successfully'))`;
// default:
// return "'' OR link IS NULL";
// }
// },
// },
// fgKirimEmail: {
// fieldMapping: 'fgkirimemail',
// },
// msPajak: {
// fieldMapping: 'masaPajak',
// transformValue: (value) => `'${addLeadingZero(value)}'`,
// },
// masaPajak: {
// fieldMapping: 'masaPajak',
// transformValue: (value) => `'${addLeadingZero(value)}'`,
// },
// thnPajak: {
// fieldMapping: 'tahunPajak',
// },
// tahunPajak: {
// fieldMapping: 'tahunPajak',
// },
// dpp: {
// fieldMapping: 'dpp',
// },
// noBupot: {
// fieldMapping: 'nomorBupot',
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// },
// npwp: {
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// idTku: {
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// errorMsg: {
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => `'%${value}%'`.toLowerCase(),
// },
// jmlBruto: {
// fieldMapping: 'penghasilanBruto',
// },
// kdObjPjk: {
// fieldMapping: 'kodeObjekPajak',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// npwpPemotong: {
// fieldMapping: 'npwpPemotong',
// },
// namaPemotong: {
// fieldMapping: 'namaPemotong',
// },
// namaDipotong: {
// fieldMapping: 'nama',
// },
// pasalPPh: {
// fieldMapping: 'pasalPPh',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// tanggalApproval: {
// transformValue: transformDateToCommonFormat,
// },
// internal_id: {
// fieldMapping: 'internal_id',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// created: {
// fieldMapping: 'created_by',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// updated: {
// fieldMapping: 'updated_by',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// };
// const defaultFieldTypes = {
// created_at: 'date',
// updated_at: 'date',
// };
// return useAdvancedSearch({
// fieldConfigs,
// defaultFieldTypes,
// });
// };
// export default useBpuAdvancedSearch;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import bulananApi from '../utils/api';
import queryKey from '../constant/queryKey';
import type{ TPortBulananCenceledRequest } from '../types/types';
const useCencelBulanan = (
......
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import type { TPortBulananRequest } from '../types/types';
import queryKey from '../constant/queryKey';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
const useCetakPdfDn = (
props?: Omit<
......
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import type { TPortBulananRequest } from '../types/types';
import queryKey from '../constant/queryKey';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
const useDeleteBulanan = (
props?: Omit<
......
import { useQuery } from '@tanstack/react-query';
import { isEmpty } from 'lodash';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from '../constant';
import queryKey from '../constant/queryKey';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from 'src/sections/bupot-21-26/constant';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey'
import type {
TBaseResponseAPI,
TGetListDataTableDn,
......
/* eslint-disable @typescript-eslint/no-shadow */
import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import type { TGetListDataKOPDn } from '../types/types';
const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
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, type UseMutationOptions } from '@tanstack/react-query';
import dayjs from 'dayjs';
import queryKey from '../constant/queryKey';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import type { TPostBulananRequest } from '../types/types';
import bulananApi from '../utils/api';
import { FG_FASILITAS_PPH_21 } from '../../constant';
const extractKapFromKodeObjekPajak = (kodeObjekPajak: string) =>
`4111${kodeObjekPajak.split('-')[0]}`;
......@@ -66,7 +67,7 @@ const transformParams = ({ isPengganti = false, ...Data }: any): TPostBulananReq
kodeObjekPajak: kdObjPjk ?? '',
pasalPPh: kdJnsPjk ?? '',
penghasilanKotor: jmlBruto ?? 0,
tarif: tarif ?? 0,
tarif: [FG_FASILITAS_PPH_21.SKB_PPH_PASAL_21].includes(fgFasilitas) ? 0 : (tarif ?? 0),
pphDipotong: pphDipotong ?? 0,
kap: extractKapFromKodeObjekPajak(kdObjPjk ?? ''),
kjs: '100',
......
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import type { TPortBulananRequest } from '../types/types';
import queryKey from '../constant/queryKey';
import bulananApi from '../utils/api';
const useUploadBulanan = (
props?: Omit<
......
import dayjs from 'dayjs';
import { ActionRekam, 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 : '';
};
export const checkCurrentPage = (pathname: string) => {
if (pathname.includes('rekam')) return ActionRekam.REKAM;
if (pathname.includes('pengganti')) return ActionRekam.PENGGANTI;
if (pathname.includes('pembetulan')) return ActionRekam.PEMBETULAN;
if (pathname.includes('ubah')) return ActionRekam.UBAH;
return ActionRekam.REKAM;
};
......@@ -27,20 +27,20 @@ import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { DashboardContent } from 'src/layouts/dashboard';
import { RouterLink } from 'src/routes/components';
import { paths } from 'src/routes/paths';
import { FG_STATUS_BUPOT } from 'src/sections/bupot-21-26/constant';
import { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import StatusChip from 'src/sections/bupot-unifikasi/bupot-dn/components/StatusChip';
import TableHeaderLabel from 'src/shared/components/TableHeaderLabel';
import { formatRupiah } from 'src/shared/FormatRupiah/FormatRupiah';
import { useDebounce, useThrottle } from 'src/shared/hooks/useDebounceThrottle';
import { createTableKey, useTablePagination } from '../../paginationStore';
import { CustomToolbar } from '../components/CustomToolbar';
import { FG_STATUS_BUPOT } from '../constant';
import { appRootKey, bulanan } from '../constant/queryKey';
import { CustomToolbar } from '../components/dialog/CustomToolbar';
import ModalCancelBulanan from '../components/dialog/ModalCancelBulanan';
import ModalCetakPdfBulanan from '../components/dialog/ModalCetakPdfBulanan';
import ModalDeleteBulanan from '../components/dialog/ModalDeleteBulanan';
import ModalUploadBulanan from '../components/dialog/ModalUploadBulanan';
import { useAdvancedFilter } from '../hooks/useAdvancedFilter';
import useGetBulanan from '../hooks/useGetBulanan';
import ModalCancelBulanan from '../components/ModalCancelBulanan';
import ModalDeleteBulanan from '../components/ModalDeleteBulanan';
import ModalUploadBulanan from '../components/ModalUploadBulanan';
import ModalCetakPdfBulanan from '../components/ModalCetakPdfBulanan';
export type IColumnGrid = GridColDef & {
field:
......
......@@ -13,24 +13,25 @@ import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { Field } from 'src/components/hook-form';
import { DashboardContent } from 'src/layouts/dashboard';
import { paths } from 'src/routes/paths';
import Agreement from 'src/shared/components/agreement/Agreement';
import HeadingRekam from 'src/shared/components/HeadingRekam';
import FormSkeleton from 'src/shared/skeletons/FormSkeleton';
import z from 'zod';
import DialogPenandatangan from '../components/ModalUploadBulanan';
import Identitas from '../components/rekam/Identitas';
import JumlahPerhitunganForm from '../components/rekam/JumlahPerhitunganForm';
import PanduanDnRekam from '../components/rekam/PanduanDnRekam';
import PerhitunganPPhPasal21 from '../components/rekam/PerhitunganPPhPasal21';
import {
FG_FASILITAS_PPH_21,
FG_FASILITAS_PPH_21_TEXT,
KODE_OBJEK_PAJAK,
KODE_OBJEK_PAJAK_TEXT,
MockNitku,
PTKP,
PTKP_TEXT,
PTKP_TITLE,
} from '../constant';
} from 'src/sections/bupot-21-26/constant';
import Agreement from 'src/shared/components/agreement/Agreement';
import HeadingRekam from 'src/shared/components/HeadingRekam';
import FormSkeleton from 'src/shared/skeletons/FormSkeleton';
import z from 'zod';
import DialogPenandatangan from '../components/dialog/ModalUploadBulanan';
import Identitas from '../components/rekam/Identitas';
import JumlahPerhitunganForm from '../components/rekam/JumlahPerhitunganForm';
import PanduanDnRekam from '../components/rekam/PanduanDnRekam';
import PerhitunganPPhPasal21 from '../components/rekam/PerhitunganPPhPasal21';
import useGetBulanan from '../hooks/useGetBulanan';
import useSaveBulanan from '../hooks/useSaveBulanan';
import useUploadBulanan from '../hooks/useUploadeBulanan';
......@@ -234,17 +235,6 @@ export const BulananRekamView = () => {
[]
);
const MockNitku = [
{
value: '1091031210912281000000',
label: '1091031210912281000000',
},
{
value: '1091031210912281000001',
label: '1091031210912281000001',
},
];
const { data: existingBulanan, isLoading: isLoadingBulanan } = useGetBulanan({
params: {
page: 1,
......@@ -386,7 +376,7 @@ export const BulananRekamView = () => {
} catch (error: any) {
enqueueSnackbar(error.message, { variant: 'error' });
} finally {
navigate('/pph21/bulanan');
navigate(paths.pph21.bulanan);
}
};
......@@ -406,7 +396,7 @@ export const BulananRekamView = () => {
{ variant: 'success' }
);
navigate('/pph21/bulanan');
navigate(paths.pph21.bulanan);
} catch (error: any) {
enqueueSnackbar(error.message || 'Gagal menyimpan data', { variant: 'error' });
console.error('❌ SaveDn error:', error);
......
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 { LoadingButton } from '@mui/lab';
import { Grid, MenuItem, Stack } from '@mui/material';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import Agreement from 'src/shared/components/agreement/Agreement';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { useAppSelector } from 'src/store';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import useUploadBulanan from '../../hooks/useUploadeBupotFinal';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface DialogPenandatanganProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
onConfirmUpload?: () => Promise<void> | void;
}
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 DialogPenandatangan: React.FC<DialogPenandatanganProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
}) => {
const TABLE_KEY = useMemo(() => createTableKey(appRootKey, bulanan), []);
const [paginationState] = useTablePagination(TABLE_KEY);
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useAppSelector((state) => state.user.data.signer);
const queryClient = useQueryClient();
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const { mutateAsync, isPending } = useUploadBulanan({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
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 = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: queryKey.bulanan.all({page: paginationState.page + 1, limit: paginationState.pageSize}) });
}
};
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}>
<LoadingButton
type="button"
disabled={!isCheckedAgreement}
// onClick={onSubmit}
onClick={async () => {
if (onConfirmUpload) {
await onConfirmUpload();
setIsOpenDialogUpload(false);
return;
}
await onSubmit();
}}
loading={isPending}
variant="contained"
sx={{ background: '#143B88' }}
>
Save
</LoadingButton>
</Stack>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default DialogPenandatangan;
......@@ -10,9 +10,9 @@ import DialogUmum from 'src/shared/components/dialog/DialogUmum';
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 useCencelBulanan from '../hooks/useCencelBulanan';
import queryKey, { appRootKey, bulanan } from '../constant/queryKey';
import { createTableKey, useTablePagination } from '../../paginationStore';
import useCencelBupotFinal from '../../hooks/useCencelBupotFinal';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import { createTableKey, useTablePagination } from '../../../paginationStore';
dayjs.extend(minMax);
......@@ -60,7 +60,7 @@ const ModalCancelBulanan: React.FC<ModalCancelDnProps> = ({
status,
} = useDialogProgressBar();
const { mutateAsync } = useCencelBulanan({
const { mutateAsync } = useCencelBupotFinal({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
......
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import DialogContent from '@mui/material/DialogContent';
import { enqueueSnackbar } from 'notistack';
import { useEffect, useState } from 'react';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import { useCetakBulanan } from '../../../cetakpdf';
interface ModalCetakPdfDnProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
const ModalCetakPdfBulanan: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onClose }) => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { mutateAsync } = useCetakBulanan({
onError: (error: any) => {
enqueueSnackbar(error?.message || 'Gagal memuat PDF', { variant: 'error' });
setLoading(false);
},
onSuccess: (res: any) => {
const fileUrl = res?.url || res?.data?.url;
if (!fileUrl) {
enqueueSnackbar('URL PDF tidak ditemukan di respons API', { variant: 'warning' });
setLoading(false);
return;
}
setPdfUrl(fileUrl);
setLoading(false);
enqueueSnackbar(res?.MsgStatus || 'PDF berhasil dibentuk', { variant: 'success' });
},
});
useEffect(() => {
const runCetak = async () => {
if (!isOpen || !payload) return;
setLoading(true);
setPdfUrl(null);
try {
console.log('Payload final cetak PDF:', payload);
await mutateAsync(payload as any);
} catch (err) {
console.error('❌ Error cetak PDF:', err);
enqueueSnackbar('Gagal generate PDF', { variant: 'error' });
setLoading(false);
}
};
runCetak();
}, [isOpen, payload, mutateAsync]);
return (
<DialogUmum
maxWidth="lg"
isOpen={isOpen}
onClose={onClose}
title="Detail Bupot Unifikasi (PDF)"
>
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
{loading && (
<Box display="flex" justifyContent="center" alignItems="center" height="60vh">
<CircularProgress />
</Box>
)}
{!loading && pdfUrl && (
<iframe
src={pdfUrl}
style={{
width: '100%',
height: '80vh',
border: 'none',
borderRadius: 8,
}}
title="Preview PDF Bupot"
/>
)}
{!loading && !pdfUrl && (
<Box textAlign="center" color="text.secondary" py={4}>
PDF tidak tersedia untuk ditampilkan.
</Box>
)}
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakPdfBulanan;
import React, { useEffect, useMemo, 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 useDeleteBupotFinal from '../../hooks/useDeleteBupotFinal';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface ModalDeleteDnProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
}
/**
* 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 ModalDeleteBupotFinal: React.FC<ModalDeleteDnProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
}) => {
const queryClient = useQueryClient();
const TABLE_KEY = useMemo(() => createTableKey(appRootKey, bulanan), []);
const [paginationState] = useTablePagination(TABLE_KEY);
// 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 } = useDeleteBupotFinal({
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' });
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: queryKey.bulanan.all({page:paginationState.page +1, limit: paginationState.pageSize}) });
}
};
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 ModalDeleteBupotFinal;
import { LoadingButton } from '@mui/lab';
import { Grid, MenuItem, Stack } from '@mui/material';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import Agreement from 'src/shared/components/agreement/Agreement';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { useAppSelector } from 'src/store';
import queryKey, { appRootKey, bulanan } from 'src/sections/bupot-21-26/constant/queryKey';
import useUploadBulanan from '../../hooks/useUploadeBupotFinal';
import { createTableKey, useTablePagination } from '../../../paginationStore';
interface DialogPenandatanganProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
onConfirmUpload?: () => Promise<void> | void;
}
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 ModalUploadBupotFinal: React.FC<DialogPenandatanganProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
}) => {
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useAppSelector((state) => state.user.data.signer);
const queryClient = useQueryClient();
const TABLE_KEY = useMemo(() => createTableKey(appRootKey, bulanan), []);
const [paginationState] = useTablePagination(TABLE_KEY);
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const { mutateAsync, isPending } = useUploadBulanan({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
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 = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: queryKey.bulanan.all({page: paginationState.page + 1, limit: paginationState.pageSize}) });
}
};
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}>
<LoadingButton
type="button"
disabled={!isCheckedAgreement}
// onClick={onSubmit}
onClick={async () => {
if (onConfirmUpload) {
await onConfirmUpload();
setIsOpenDialogUpload(false);
return;
}
await onSubmit();
}}
loading={isPending}
variant="contained"
sx={{ background: '#143B88' }}
>
Save
</LoadingButton>
</Stack>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalUploadBupotFinal;
import { Divider, Grid } from '@mui/material';
import dayjs from 'dayjs';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import {
BUPOT_REFERENSI,
BUPOT_REFERENSI_TEXT,
MockNitku,
} from 'src/sections/bupot-21-26/constant';
function DokumenReferensi() {
const { setValue } = useFormContext();
const options = Object.entries(BUPOT_REFERENSI).map(([key, value]) => ({
value,
label: BUPOT_REFERENSI_TEXT[value],
}));
return (
<Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
<Grid size={{ md: 12 }}>
<Divider sx={{ fontWeight: 'bold' }} textAlign="left">
Jenis Dokumen Referensi
</Divider>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Autocomplete
name="namaDokumenReferensi"
label="Jenis Dokumen Referensi"
options={options}
onChange={(_, value) => setValue('namaDokumenReferensi', value)}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text
name="nomorDokumenReferensi"
label="Nomor Dokumen Referensi"
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tanggalDokumenReferensi"
label="Tanggal Dokumen Referensi"
format="DD/MM/YYYY"
maxDate={dayjs()}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Autocomplete name="idTku" label="NITKU Pemotong" options={MockNitku} />
</Grid>
</Grid>
);
}
export default DokumenReferensi;
import Grid from '@mui/material/Grid';
import dayjs from 'dayjs';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
// import { useParams } from 'react-router';
import { Field } from 'src/components/hook-form';
type IdentitasProps = {
isPengganti?: boolean;
};
const Identitas = ({ isPengganti }: IdentitasProps) => {
const { setValue, watch } = useFormContext();
const tanggalPemotongan = watch('tglPemotongan');
// auto set thnPajak dan msPajak berdasarkan tanggalPemotongan
useEffect(() => {
if (!isPengganti) {
if (tanggalPemotongan) {
const date = dayjs(tanggalPemotongan);
setValue('tahunPajak', date.format('YYYY'));
setValue('masaPajak', date.format('MM'));
setValue('tanggalDokumenReferensi', date)
} else {
setValue('tahunPajak', '');
setValue('masaPajak', '');
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tanggalPemotongan, !isPengganti]);
return (
<>
<Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tglPemotongan"
label="Tanggal Pemotongan"
format="DD/MM/YYYY"
maxDate={dayjs()}
/>
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker
name="tahunPajak"
label="Tahun Pajak"
view="year"
format="YYYY"
readOnly
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker
name="masaPajak"
label="Masa Pajak"
view="month"
format="MM"
readOnly
disabled={isPengganti}
/>
</Grid>
{/* NPWP dengan onChange langsung */}
<Grid size={{ md: 6 }}>
<Field.Text
name="npwp"
label="NPWP"
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 16); // hanya angka, max 16
setValue('npwp', value, { shouldValidate: true, shouldDirty: true });
setValue('nitku', value.length === 16 ? value + '000000' : value, {
shouldValidate: true,
shouldDirty: true,
});
}}
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text
name="nitku"
label="NITKU"
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="namaDipotong" label="Nama" disabled={isPengganti} />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="alamatDipotong" label="Alamat" />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="email" label="Email (optional)" disabled={isPengganti} />
</Grid>
</Grid>
{/* Tambah / Hapus Keterangan */}
{/* <Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Box
sx={{
borderRadius: '18px',
border: jumlahKeterangan >= maxKeterangan ? '1px solid #eee' : '1px solid #2e7d3280',
color: jumlahKeterangan >= maxKeterangan ? '#eee' : '#2e7d3280',
p: '0px 10px',
}}
>
<Button disabled={jumlahKeterangan >= maxKeterangan} onClick={handleTambah}>
Tambah Keterangan
</Button>
</Box>
<Box
sx={{
borderRadius: '18px',
border: jumlahKeterangan === 0 ? '1px solid #eee' : '1px solid #f44336',
color: jumlahKeterangan === 0 ? '#eee' : '#f44336',
p: '0px 10px',
}}
>
<Button disabled={jumlahKeterangan === 0} onClick={handleHapus}>
Hapus Keterangan
</Button>
</Box>
</Box>
<Box sx={{ mb: 3 }}>
{Array.from({ length: jumlahKeterangan }).map((_, i) => (
<Grid size={{ md: 12 }} key={i}>
<Field.Text
sx={{ mb: 2 }}
name={`keterangan${i + 1}`}
label={`Keterangan Tambahan ${i + 1}`}
/>
</Grid>
))}
</Box> */}
</>
);
};
export default Identitas;
/* eslint-disable react/jsx-no-useless-fragment */
import { CalculateRounded, CheckCircleRounded } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab';
import { Alert, Grid } from '@mui/material';
import dayjs from 'dayjs';
import { useEffect, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
import {
FG_FASILITAS_PPH_21,
FG_PERHITUNGAN,
FG_PERHITUNGAN_TEXT,
KODE_OBJEK_PAJAK,
METODE_POTONG,
} from 'src/sections/bupot-21-26/constant';
import {
getHitungBulananErrorMessage,
useHitungBupotFinalTidakFinal,
} from 'src/sections/bupot-21-26/hitung';
const fgPerhitunganOptions = Object.values(FG_PERHITUNGAN).map((value) => ({
value,
label: FG_PERHITUNGAN_TEXT[value],
}));
function JumlahPerhitunganForm({
ptkpOptions,
}: {
ptkpOptions: {
value: string;
label: string;
}[];
}) {
const { watch, getValues, setValue, trigger } = useFormContext();
const kdObjPjk = watch('kdObjPjk');
const { mutate, isPending } = useHitungBupotFinalTidakFinal({
onSuccess: (data) => {
console.log('✅ Berhasil hitung PPh21:', data);
const tunjanganPPh = data.pph21ditanggungperusahaan ?? data.tunjanganPPh;
setValue(
'pph21',
[FG_FASILITAS_PPH_21.SKB_PPH_PASAL_21].includes(watch('fgFasilitas')) ? 0 : data.pphDipotong
);
setValue('tarif', data.tarif || kdObjPjk.dpp);
setValue('tunjanganPPh', tunjanganPPh || 0);
},
onError: (error) => {
console.error('❌ Error:', getHitungBulananErrorMessage(error));
},
});
const handleHitung = async () => {
const validasi = await trigger(['phBruto', 'ptkp', 'fgPerhitungan']);
if (!validasi) return;
const {
phBruto: penghasilanBruto,
ptkp,
fgPerhitungan,
tglPemotongan,
dppPersen,
jenisHitung,
akumulasiJmlBruto,
} = getValues();
console.log('🚀 ~ handleHitung ~ fgPerhitungan:', { fgPerhitungan });
const tglBupot = dayjs(tglPemotongan).format('DDMMYYYY');
const data = {
tglBupot,
dppPersen,
jenisHitung,
status: ptkp.value,
kodeObjekPajak: kdObjPjk.value,
penghasilanKotor: penghasilanBruto,
akumulasiJmlBruto: akumulasiJmlBruto || '0',
metode: fgPerhitungan !== '0' ? METODE_POTONG.GROSS_UP : METODE_POTONG.GROSS, // Simplify conditional assignment
};
mutate(data as any);
};
const isOpenAkumulasiPerhitunganForm = useMemo(() => {
if ([KODE_OBJEK_PAJAK.FINAL_01, KODE_OBJEK_PAJAK.FINAL_02].includes(kdObjPjk.value))
return true;
return false;
}, [kdObjPjk.value]);
useEffect(() => {
if (!isOpenAkumulasiPerhitunganForm) {
setValue('akumulasiJmlBruto', 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenAkumulasiPerhitunganForm]);
return (
<Grid container rowSpacing={2} columnSpacing={2} sx={{ my: 3 }}>
{isOpenAkumulasiPerhitunganForm ? (
<>
<Grid size={{ md: 5 }}>
<Alert variant="outlined" icon={<CheckCircleRounded />} severity="warning">
Transaksi dilakukan dalam kurun waktu 2 tahun
</Alert>
</Grid>
<Grid size={{ md: 7 }}>
<RHFNumeric name="akumulasiJmlBruto" label="Jumlah Akumulasi (Rp)" />
</Grid>
</>
) : (
<></>
)}
<Grid size={{ md: 3 }}>
<Field.RadioGroup
row
name="fgPerhitungan"
label="Metode Pemotongan"
options={fgPerhitunganOptions.filter((a) => a.value !== FG_PERHITUNGAN.MIXED)}
/>
</Grid>
<Grid size={{ md: 9 }}>
<Field.Autocomplete name="ptkp" label="Status PTKP" options={ptkpOptions} />
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric name="phBruto" label="Jumlah Penghasilan (Rp)" />
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric name="tunjanganPPh" label="Tunjangan PPh 21 (Rp)" readOnly />
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric name="dppPersen" label="Dpp (%)" allowDecimalValue maxValue={100} readOnly />
</Grid>
<Grid size={{ md: 5 }}>
<RHFNumeric name="tarif" label="Tarif (%)" allowDecimalValue maxValue={100} readOnly />
</Grid>
<Grid size={{ md: 5 }}>
<RHFNumeric name="pph21" label="PPh Pasal 21" readOnly />
</Grid>
<Grid size={{ md: 2 }} alignSelf="center">
<LoadingButton
variant="contained"
fullWidth
size="large"
color="primary"
onClick={handleHitung}
loading={isPending}
startIcon={<CalculateRounded />}
>
Hitung
</LoadingButton>
</Grid>
</Grid>
);
}
export default JumlahPerhitunganForm;
import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { m } from 'framer-motion';
import type { FC } from 'react';
import { PANDUAN_REKAM_DN } from 'src/sections/bupot-21-26/constant';
interface PanduanDnRekamProps {
handleOpen: () => void;
isOpen: boolean;
}
const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => (
<Box position="sticky">
{/* Tombol toggle */}
<Box
height="100%"
display={isOpen ? 'none' : 'flex'}
justifyContent="center"
alignItems="center"
>
<Button
variant="contained"
sx={{
height: 'fit-content',
right: 0,
borderRadius: 0,
minWidth: 35,
pt: 3,
pb: 3,
fontWeight: 'bold',
fontSize: 16,
backgroundColor: '#143B88',
}}
size="small"
onClick={handleOpen}
>
<span
style={{
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
display: 'flex',
alignItems: 'center',
}}
>
Panduan Penggunaan
<ChevronRightRounded sx={{ fontSize: 30 }} />
</span>
</Button>
</Box>
{/* Konten panduan */}
{isOpen && (
<m.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1, transition: { delay: 0.2 } }}
>
<Card>
<CardHeader
avatar={
<img src="/assets/icon_panduan_penggunaan_1.svg" alt="Panduan" loading="lazy" />
}
sx={{
backgroundColor: '#123375',
color: '#FFFFFF',
padding: '16px',
'& .MuiCardHeader-title': {
fontSize: 18,
},
}}
action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
<CloseRounded />
</IconButton>
}
title="Panduan Penggunaan"
/>
<CardContent
sx={{
maxHeight: 300,
overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { backgroundColor: '#f0f0f0', borderRadius: 8 },
'&::-webkit-scrollbar-thumb': { backgroundColor: '#123375', borderRadius: 8 },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: '#0d2858' },
scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0',
}}
>
{/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<span style={{ fontWeight: 600 }}>Deskripsi Form:</span>
<br />
{PANDUAN_REKAM_DN.description.intro}
</Typography>
<Typography variant="body2" sx={{}}>
{PANDUAN_REKAM_DN.description.textList}
</Typography>
<Box component="ol" sx={{ pl: 3, mb: 2 }}>
{PANDUAN_REKAM_DN.description.list.map((item, idx) => (
<Typography key={idx} variant="body2" component="li">
{item}
</Typography>
))}
</Box>
<Typography variant="body2" sx={{ mb: 2 }}>
{PANDUAN_REKAM_DN.description.closing}
</Typography>
{/* Bagian-bagian */}
{PANDUAN_REKAM_DN.sections.map((section, i) => (
<Box key={i} sx={{ mb: 2 }}>
<Typography
variant="body2"
sx={{ fontWeight: 'bold', fontSize: '0.95rem', mb: 0.5 }}
>
{section.title}
</Typography>
<Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}>
{section.items.map((item, idx) => (
<Box key={idx} component="li" sx={{ mb: 0.5 }}>
<Typography variant="body2" component="span">
{item.text}
</Typography>
{item.subItems.length > 0 && (
<Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}>
{item.subItems.map((sub, subIdx) => (
<Typography key={subIdx} variant="body2" component="li">
{sub}
</Typography>
))}
</Box>
)}
</Box>
))}
</Box>
</Box>
))}
</CardContent>
</Card>
</m.div>
)}
</Box>
);
export default PanduanDnRekam;
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import {
FG_FASILITAS_MASTER_KEY,
FG_FASILITAS_PPH_21,
FG_FASILITAS_PPH_21_TEXT,
} from 'src/sections/bupot-21-26/constant';
type PPHDipotongProps = {
kodeObjectPajak: {
value: string;
label: string;
}[];
fgFasilitasOptions: {
value: string;
label: string;
}[];
};
const PerhitunganPPhPasal21 = ({ kodeObjectPajak, fgFasilitasOptions }: PPHDipotongProps) => {
const { watch, setValue } = useFormContext();
const fgFasilitas = watch('fgFasilitas');
const kdObjPjk = watch('kdObjPjk');
const handleChangeKodeObjekPajak = (value: any) => {
[
{ key: 'jenisHitung', value: value?.jenisHitung || '' },
{ key: 'dppPersen', value: value?.dpp || '' },
{ key: 'pasalPPh', value: value?.pasal || '' },
{ key: 'statusPph', value: value?.statuspph || '' },
{ key: 'kap', value: value?.kap || '' },
{ key: 'kjs', value: value?.kjs || '' },
//reset value
{
key: 'fgFasilitas',
value: {
value: FG_FASILITAS_PPH_21.TANPA_FASILITAS,
label: FG_FASILITAS_PPH_21_TEXT[FG_FASILITAS_PPH_21.TANPA_FASILITAS],
},
},
{ key: 'jenisPerhitungan', value: '' },
{ key: 'fgPerhitungan', value: '0' },
{ key: 'tunjanganPPh', value: 0 },
{ key: 'tarif', value: 0 },
{ key: 'pph21', value: 0 },
{ key: 'akumulasiJmlBruto', value: 0 },
].forEach(({ key, value: val }) => {
setValue(key, val);
});
};
const filterFgFasilitasOptions = useCallback(
(options: any[]) =>
options?.filter((item) => kdObjPjk[FG_FASILITAS_MASTER_KEY[item.value]] === 1),
[kdObjPjk]
);
return (
<Grid container rowSpacing={2} columnSpacing={2}>
{/* Divider */}
<Grid size={{ md: 12 }}>
<Divider sx={{ fontWeight: 'bold' }} textAlign="left">
Perhitungan PPh Pasal 21
</Divider>
</Grid>
{/* Kode objek pajak */}
<Grid size={{ md: 12 }}>
<Field.Autocomplete
name="kdObjPjk"
label="Kode Objek Pajak"
options={kodeObjectPajak}
onChange={(_, value) => {
setValue('kdObjPjk', value);
handleChangeKodeObjekPajak(value);
}}
/>
</Grid>
{/* Fasilitas */}
<Grid size={{ md: 6 }}>
<Field.Autocomplete
name="fgFasilitas"
label="Fasilitas"
options={filterFgFasilitasOptions(fgFasilitasOptions)}
/>
</Grid>
{/* Dokumen lainnya */}
<Grid size={{ md: 6 }}>
<Field.Text
name="noDokLainnya"
label="Nomor Dokumen Lainnya"
disabled={['9', ''].includes(fgFasilitas.value)}
sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
/>
</Grid>
</Grid>
);
};
export default PerhitunganPPhPasal21;
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;
......@@ -2,9 +2,9 @@ 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';
import type { ActionItem } from '../../types/types';
interface CustomToolbarProps extends GridToolbarProps {
actions?: ActionItem[][];
......
type FilterItem = {
field: string;
operator: string;
value?: string | number | Array<string | number> | null;
join?: 'AND' | 'OR';
};
type BaseParams = Record<string, any>;
/**
* Advanced filtering hook untuk building SQL WHERE clauses
*
* @example
* const { buildAdvancedFilter, buildRequestParams } = useAdvancedFilter();
* const filters = [{ field: 'noBupot', operator: 'contains', value: '123' }];
* const sql = buildAdvancedFilter(filters);
*/
export function useAdvancedFilter() {
// ✅ Konstanta untuk field types
const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
const dateFields = new Set(['created_at', 'updated_at']);
/**
* ✅ FIXED: Ubah mapping dari nomorBupot → noBupot
* Sekarang konsisten: frontend menggunakan noBupot, backend juga noBupot
*/
const fieldMap: Record<string, string> = {
// Tambahkan mapping lain jika diperlukan di sini
// Contoh: 'frontendField': 'backendField'
};
/**
* Get database field name with mapping
*/
const dbField = (field: string): string => fieldMap[field] ?? field;
/**
* Escape single quotes untuk prevent SQL injection
* ⚠️ NOTE: Ini partial protection, gunakan parameterized queries di backend!
*/
const escape = (v: string): string => String(v).replace(/'/g, "''");
/**
* Convert various date formats to YYYYMMDD
*/
const toDbDate = (value: string | Date): string => {
try {
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;
} catch (error) {
console.warn('Invalid date format:', value);
return '';
}
};
/**
* ✅ IMPROVED: Normalize operator dengan lowercase
*/
const normalizeOp = (op: string): string =>
op?.toString().trim().toLowerCase() || '';
/**
* Build advanced filter SQL WHERE clause
*/
function buildAdvancedFilter(filters?: FilterItem[] | null): string {
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;
// ============================================
// 1️⃣ DATE FIELDS
// ============================================
if (dateFields.has(fieldName)) {
const rawVal = f.value;
if (!rawVal && !op.match(/is empty|is not empty/)) continue;
const ymd = toDbDate(rawVal as string | Date);
if (!ymd) continue;
if (op === 'is') {
expr = `"${fieldName}" >= '${ymd} 00:00:00' AND "${fieldName}" <= '${ymd} 23:59:59'`;
} else if (op === 'is on or after') {
expr = `"${fieldName}" >= '${ymd}'`;
} else if (op === 'is on or before') {
expr = `"${fieldName}" <= '${ymd}'`;
}
}
// ============================================
// 2️⃣ EMPTY/NOT EMPTY
// ============================================
if (op === 'is empty') {
expr = `"${fieldName}" IS NULL`;
} else if (op === 'is not empty') {
expr = `"${fieldName}" IS NOT NULL`;
}
// ============================================
// 3️⃣ IS ANY OF (Multiple values)
// ============================================
if (!expr && op === 'is any of') {
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) {
// ✅ IMPROVED: Normalize field name comparison
const isStatusField = fieldName.toLowerCase() === 'fgstatus';
if (isStatusField) {
// Status field: LIKE for partial matching
const ors = values.map((v) => {
const s = escape(String(v).toLowerCase());
return `LOWER("${fieldName}") LIKE '%${s}%'`;
});
expr = `(${ors.join(' OR ')})`;
} else {
// Other fields: Exact match
const ors = values.map((v) => {
const s = escape(String(v).toLowerCase());
return `LOWER("${fieldName}") = '${s}'`;
});
expr = `(${ors.join(' OR ')})`;
}
}
}
// ============================================
// 4️⃣ FGSTATUS SPECIAL (Single value)
// ============================================
if (!expr && fieldName.toLowerCase() === 'fgstatus') {
const valRaw = f.value == null ? '' : String(f.value);
if (valRaw !== '' && !['is any of', 'is empty', 'is not empty'].includes(op)) {
const valEscaped = escape(valRaw.toLowerCase());
if (op === 'is') {
expr = `LOWER("${fieldName}") LIKE '%${valEscaped}%'`;
} else if (op === 'is not') {
expr = `LOWER("${fieldName}") NOT LIKE '%${valEscaped}%'`;
}
}
}
// ============================================
// 5️⃣ GENERIC OPERATORS
// ============================================
if (!expr) {
const valRaw = f.value == null ? '' : String(f.value);
if (valRaw !== '') {
const valEscaped = escape(valRaw.toLowerCase());
const isNumericField = numericFields.has(fieldName);
// Numeric field operators
if (isNumericField && /^(=|>=|<=|>|<)$/.test(op)) {
expr = `"${fieldName}" ${op} '${valEscaped}'`;
}
// Text operators
else if (op === 'contains') {
expr = `LOWER("${fieldName}") LIKE '%${valEscaped}%'`;
}
else if (op === 'equals') {
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 (op === 'is') {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
}
// Fallback
else {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
}
}
}
// Add expression with proper join
if (expr) {
exprs.push(expr);
const joinBefore = f.join ?? 'AND';
joins.push(joinBefore);
}
}
// Build final SQL
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;
}
/**
* ✅ FIXED: Tidak ada lagi field mapping untuk noBupot
* Build request parameters dengan clean up undefined values
*/
function buildRequestParams(base: BaseParams = {}, advanced: string): BaseParams {
const out: BaseParams = {};
// Copy all defined params
Object.keys(base).forEach((key) => {
if (base[key] !== undefined) {
out[key] = base[key];
}
});
// ✅ REMOVED: Field mapping noBupot → nomorBupot
// Sekarang frontend dan backend sama-sama gunakan 'noBupot'
// Add advanced filter if exists
if (advanced && advanced.trim() !== '') {
out.advanced = advanced.trim();
}
// Clean up undefined sorting params
if (out.sortingMode === undefined) {
delete out.sortingMode;
}
if (out.sortingMethod === undefined) {
delete out.sortingMethod;
}
return out;
}
return {
buildAdvancedFilter,
buildRequestParams
} as const;
}
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import type{ TPortBulananCenceledRequest } from '../types/types';
const useCencelBulanan = (
props?: Omit<
UseMutationOptions<any, Error, TPortBulananCenceledRequest, unknown>,
'mutationKey' | 'mutationFn'
>
) =>
useMutation<any, Error, TPortBulananCenceledRequest, unknown>({
mutationKey: queryKey.bulanan.upload,
mutationFn: (params: TPortBulananCenceledRequest) => bulananApi.batal(params),
...props,
});
export default useCencelBulanan;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import type { TPortBulananRequest } from '../types/types';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
const useCetakPdfDn = (
props?: Omit<
UseMutationOptions<any, Error, TPortBulananRequest, unknown>,
'mutationKey' | 'mutationFn'
>
) =>
useMutation<any, Error, TPortBulananRequest, unknown>({
mutationKey: queryKey.bulanan.upload,
mutationFn: (params: TPortBulananRequest) => bulananApi.upload(params),
...props,
});
export default useCetakPdfDn;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import type { TPortBulananRequest } from '../types/types';
import bulananApi from '../utils/api';
const useDeleteBulanan = (
props?: Omit<
UseMutationOptions<any, Error, TPortBulananRequest, unknown>,
'mutationKey' | 'mutationFn'
>
) =>
useMutation<any, Error, TPortBulananRequest, unknown>({
mutationKey: queryKey.bulanan.upload,
mutationFn: (params: TPortBulananRequest) => bulananApi.delete(params),
...props,
});
export default useDeleteBulanan;
import { useQuery } from '@tanstack/react-query';
import { isEmpty } from 'lodash';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from 'src/sections/bupot-21-26/constant';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import type { TBaseResponseAPI, TGetListDataTableBupotFinalResult } from '../types/types';
import finalTdkFinalApi from '../utils/api';
export const transformFgStatusToFgSignStatus = (fgStatus: any) => {
// console.log('🚀 ~ transformFgStatusToFgSignStatus ~ fgStatus:', fgStatus);
const splittedFgStatus = fgStatus?.split('-') || [];
if (splittedFgStatus.includes('SIGN') > 0) {
// failed
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 (isEmpty(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',
});
const normalisePropsGetBupotFinal = (params: TGetListDataTableBupotFinalResult[0]) => ({
...params,
namaDokumenReferensi: params.dokumen_referensi[0].dokReferensi,
nomorDokumenReferensi: params.dokumen_referensi[0].nomorDokumen,
tanggalDokumenReferensi: params.dokumen_referensi[0].tanggal_Dokumen,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
idDipotong: params.userId,
});
const normalisPropsParmas = (params: any) => {
const sorting = !isEmpty(params.sort) ? transformSortModelToSortApiPayload(params.sort) : {};
return {
...params,
masaPajak: params.msPajak || null,
tahunPajak: params.thnPajak || null,
npwp: params.idDipotong || null,
advanced: isEmpty(params.advanced) ? undefined : params.advanced,
...sorting,
feature: 'tdkfinal',
};
};
const useGetBupotFinal = ({ params, ...props }: any) => {
const query = useQuery<TBaseResponseAPI<TGetListDataTableBupotFinalResult>>({
queryKey: queryKey.bulanan.all(params),
queryFn: async () => {
const response = await finalTdkFinalApi.getList({ params: normalisPropsParmas(params) });
return {
...response,
data: response.data.map((data) => normalisePropsGetBupotFinal(data)),
};
},
initialData: {
data: [],
total: 0,
},
refetchOnWindowFocus: false,
...props,
});
return query;
};
export default useGetBupotFinal;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import dayjs from 'dayjs';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
import { FG_FASILITAS_PPH_21 } from '../../constant';
import type { TPostBupotFinalRequest } from '../types/types';
import bupotFinalTdkFinalApi from '../utils/api';
const transformParams = ({ isPengganti = false, ...Data }: any): TPostBupotFinalRequest => {
const {
id,
idBupot,
noBupot,
masaPajak,
tahunPajak,
npwp,
nitku,
namaDipotong,
fgFasilitas,
noDokLainnya,
kdObjPjk,
phBruto,
pasalPPh,
tarif,
pphDipotong,
revNo: initialRevNo,
tglPemotongan,
idTku,
fgGrossUp,
alamatDipotong,
statusPtkp,
dppPersen,
kap,
kjs,
akumulasiJmlBruto,
namaDokumenReferensi,
nomorDokumenReferensi,
tanggalDokumenReferensi,
tunjanganPPh
} = Data;
const revNo = isPengganti ? parseInt(initialRevNo || 0, 10) + 1 : parseInt(initialRevNo || 0, 10);
const npwpLog = localStorage.getItem('npwp_log') ?? '';
return {
id: !isPengganti ? (id ?? null) : null,
revNo,
idBupot,
noBupot,
npwpPemotong: npwpLog,
idTku: idTku ?? '',
masaPajak,
tahunPajak,
fgTransaction: id ? 'EDIT' : 'NEW',
fgNpwpNik: true,
npwp,
nik: nitku,
nama: namaDipotong ?? '',
fgGrossUp: fgGrossUp ?? 1,
fgJnsBupot: '21',
alamat: alamatDipotong ?? '',
dataDetilBp21: {
sertifikatInsentifDipotong: fgFasilitas ?? '9',
nomorSertifikatInsentif: noDokLainnya ?? '',
kodeObjekPajak: kdObjPjk ?? '',
pasalPPh,
statusPPh: statusPtkp,
kap,
kjs,
penghasilanKotorSebelumnya: akumulasiJmlBruto || '0',
penghasilanKotor: phBruto ?? 0,
tarif: [FG_FASILITAS_PPH_21.SKB_PPH_PASAL_21].includes(fgFasilitas) ? 0 : (tarif ?? 0),
pphDipotong: pphDipotong ?? 0,
tunjanganPPh,
NormaPenghasilan: Number(dppPersen || 50),
dokReferensi: [
{
dokReferensi: namaDokumenReferensi,
nomorDokumen: nomorDokumenReferensi,
tanggal_Dokumen: dayjs(tanggalDokumenReferensi).format('DDMMYYYY'),
},
],
},
tglPemotongan: tglPemotongan ? dayjs(tglPemotongan).format('DDMMYYYY') : '',
feature: 'final',
kanal: '14',
};
};
const useSaveBupotFnlTdkFnl = (
props?: Omit<UseMutationOptions<any, Error, any, unknown>, 'mutationKey' | 'mutationFn'>
) =>
useMutation({
mutationKey: queryKey.bulanan.draft,
mutationFn: (params: any) => bupotFinalTdkFinalApi.save(transformParams(params)),
...props,
});
export default useSaveBupotFnlTdkFnl;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import type { TPortBulananRequest } from '../types/types';
import queryKey from 'src/sections/bupot-21-26/constant/queryKey';
const useUploadBulanan = (
props?: Omit<
UseMutationOptions<any, Error, TPortBulananRequest, unknown>,
'mutationKey' | 'mutationFn'
>
) =>
useMutation<any, Error, TPortBulananRequest, unknown>({
mutationKey: queryKey.bulanan.upload,
mutationFn: (params: TPortBulananRequest) => bulananApi.upload(params),
...props,
});
export default useUploadBulanan;
export type TBaseResponseAPI<T> = {
status: string;
message: string;
data: T;
time: string;
code: number;
metaPage: TBaseResponseMetaPage;
total?: number;
};
export type TBaseResponseCreateAPI<T> = {
code: number;
data?: T;
message: string;
status: string;
time: Date;
};
export interface BupotRecord {
// --- Kunci numerik dinamis ("1" sampai "53") ---
// [key: `${number}`]: number | undefined;
id: number;
idBupot: string | null;
noBupot: string | null;
thnPajak: string;
msPajak: string;
namaPemotong: string | null;
fgIdDipotong: string; // "true" / "false" (string)
idDipotong: string;
namaDipotong: string;
tglPemotongan: string;
kdJnsPjk: string;
namaTtd: string | null;
nikNpwpTtd: string | null;
created_at: string;
updated_at: string;
errorMsg: string | null;
created: string;
updated: string;
email: string | null;
npwp16Pemotong: string;
nitkuPemotong: string;
npwp16Dipotong: string;
fgKirimEmail: number;
statusEmail: string | null;
messageid: string | null;
passphrasePenandatangan: string | null;
dcPenandatangan: string | null;
serialNumberPenandatangan: string | null;
userId: string;
foreignEmployee: string; // "true" / "false" (string)
passportNo: string;
countryCode: string | null;
statusPtkp: string;
jmlPtkp: number;
posisiJabatan: string;
kdObjPjk: string;
nmObjPjk: string | null;
pasalPPh: string | null;
bruto: string;
tarif: string;
pphDipotong: string;
fgStatus: string;
fgFasilitas: string;
noDokLainnya: string;
kap: string;
kjs: string;
internal_id: string;
fgLapor: number;
revNo: number;
tglPembatalan: string | null;
fgGrossUp: number;
link: string | null;
glAccount: string;
fgkirimemail: string;
tunjanganPPh: string;
pph21ditanggungperusahaan: string;
pph21ditanggungkaryawan: string;
alamat: string;
keterangan1: string | null;
keterangan2: string | null;
keterangan3: string | null;
keterangan4: string | null;
keterangan5: string | null;
}
type TBaseResponseMetaPage = {
pageNum: number | null;
rowPerPage: number | null;
totalRow: number;
};
export type TGetListDataTableBupotFinal = {
alamat: string;
bruto: string;
countryCode: string | null;
created?: string;
created_at: string;
dokReferensi: string;
email: string | null;
errorMsg: string | null;
fgFasilitas: string;
fgGrossUp: number;
fgIdDipotong: string;
fgKirimEmail: string;
fgLapor?: string;
fgPdf: string;
fgSignStatus: string | null;
fgStatus: string;
fgkirimemail: string;
foreignEmployee: string;
glAccount: string | null;
glName: string | null;
id: number;
idBupot: string | null;
idDipotong: string;
internal_id: string;
jmlBruto?: string;
jmlPtkp: number;
kap: string;
kdJnsPjk: string;
kdObjPjk: string;
keterangan1: string | null;
keterangan2: string | null;
keterangan3: string | null;
keterangan4: string | null;
keterangan5: string | null;
kjs: string;
link: string | null;
masaPajak: string;
metodePembayaranBendahara: string;
msPajak: string;
namaDipotong?: string;
namaNegara: string | null;
namaPenandatangan: string | null;
nitkuPemotong: string;
noBupot: string | null;
noDokLainnya: string;
nomorDokumen: string;
nomorSP2D: string;
npwp16Dipotong: string;
npwp16Pemotong: string;
npwpNikPenandatangan: string | null;
npwpPemotong?: string;
passportNo: string;
pasalPPh: string;
posisiJabatan: string;
pph21ditanggungkaryawan: string;
pph21ditanggungperusahaan: string;
pphDipotong: string;
revNo: number;
statusPtkp: string;
tarif: string;
tglpemotongan: string;
thnPajak: string;
tunjanganPPh: string;
updated?: string;
updated_at: string;
userId: string;
};
export type TGetListDataTableBupotFinalResult = {
id: number;
idBupot: null | string;
noBupot: null | string;
revNo:string;
fgTransaction:string;
npwpPemotong:string;
namaPemotong: null | string;
idTku:string;
masaPajak:string;
tahunPajak:string;
fgNpwpNik:string;
npwp:string;
nik:string;
nama:string;
foreignEmployee:string;
passportNo:string;
countryCode: null | string;
statusPPh:string;
posisiJabatan:string;
kodeObjekPajak:string;
pasalPPh:string;
penghasilanKotorSebelumnya:string;
penghasilanKotor:string;
tarif:string;
pphDipotong:string;
NormaPenghasilan:string;
kap:string;
kjs:string;
sertifikatInsentifDipotong:string;
nomorSertifikatInsentif:string;
tglpemotongan: null | string;
tglpembatalan: null | string;
userId:string;
created_at:string;
updated_at:string;
created:string;
updated:string;
namaPenandatangan: null | string;
npwpNikPenandatangan: null | string;
fgStatus:string;
alamat:string;
dokumen_referensi: [
{
dokReferensi: null | string;
nomorDokumen:string;
tanggal_Dokumen:string;
tanggalSP2D: null | string;
},
];
link: null | string;
email: null | string;
glAccount: null | string;
fgkirimemail:string;
fgGrossUp: number;
tunjanganPPh: null | string;
glName: null | string;
errorMsg: null | string;
keterangan1: null | string;
keterangan2: null | string;
keterangan3: null | string;
keterangan4: null | string;
keterangan5: null | string;
internal_id:string;
namaNegara: null | string;
}[];
export type TGetListDataKOPBupotFinal = {
dtp: number;
kap: string;
kjs: string;
kode: string;
nama: string;
noCertificate: number;
normanetto: string;
otherCert: number;
pasal: string;
skbBungaTabungan: number;
skbPHTB: number;
skbPasal22: number;
skbPasal23: number;
statuspph: string;
suket: number;
tarif: string;
};
export type TGetListDataKOPBupotFinalResult = TGetListDataKOPBupotFinal[];
export type ActionItem = {
title: string;
icon: React.ReactNode;
func?: () => void;
disabled?: boolean;
};
export type TPostBulananRequest = {
id?: number | null;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: string;
fgNpwpNik: boolean;
npwp: string;
nik: string;
nama: string;
fgJnsBupot: string;
alamat: string;
dataDetilA0: {
foreignEmployee: boolean;
passportNo: string;
countryCode: string | null;
statusPtkp: string;
jmlPtkp: string;
posisiJabatan: string;
kodeObjekPajak: string;
pasalPPh: string;
penghasilanKotor: number;
tarif: number;
pphDipotong: number;
kap: string;
kjs: string;
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
};
tglPemotongan: string;
noBupot?: string;
idBupot?: string;
revNo: number;
fgGrossUp: number;
};
export type TPostBupotFinalRequest = {
id?: number | null;
noBupot?: string;
idBupot?: string;
revNo?: number;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: string;
fgTransaction: 'NEW' | 'EDIT' | 'CANCELED'; // create & edit = NEW, pengganti = EDIT, cancel = CANCELED
fgNpwpNik: boolean;
npwp: string;
nik: string;
nama: string;
fgJnsBupot: string;
alamat: string;
dataDetilBp21: {
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPPh: string;
statusPPh: string;
kap: string;
kjs: string;
penghasilanKotorSebelumnya: string;
penghasilanKotor: number;
tarif: number;
pphDipotong: number;
tunjanganPPh: string;
NormaPenghasilan: number;
dokReferensi: {
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
}[];
};
tglPemotongan: string;
feature: 'final';
kanal: '14';
fgGrossUp: number;
};
export type TPortBulananRequest = {
id: string;
};
export type TPortBulananCenceledRequest = {
tglPembatalan: string;
} & TPortBulananRequest;
import { fetcher, endpoints } from 'src/lib/axios-ctas-box';
import type {
BupotRecord,
TBaseResponseAPI,
TGetListDataKOPBupotFinalResult,
TGetListDataTableBupotFinalResult,
TPortBulananCenceledRequest,
TPortBulananRequest,
TPostBupotFinalRequest,
} from '../types/types';
const {list, canceled, delete: deleteAPI, upload} = endpoints.pph21.fnlTdkFnl
const finalTdkFinalApi = () => {};
// API untuk get list table
finalTdkFinalApi.getList = async (config: any) => {
const response = await fetcher<TBaseResponseAPI<TGetListDataTableBupotFinalResult>>([
list,
{
method: 'GET',
...config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to fetch final tidak final data');
}
const { metaPage, data } = response;
return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
};
// ✅ adjust biar bisa terima params
finalTdkFinalApi.getKodeObjekPajak = async (params?: Record<string, any>) => {
const response = await fetcher<TBaseResponseAPI<TGetListDataKOPBupotFinalResult>>([
'/sandbox/mst_kop_bpu',
{
method: 'GET',
params, // ⬅️ inject ke query string
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to fetch kode objek pajak');
}
return response;
};
finalTdkFinalApi.save = async (config: TPostBupotFinalRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord[]>>([
list,
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to save bulanan data');
}
return response.data;
};
finalTdkFinalApi.upload = async (config: TPortBulananRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
upload,
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to upload bulanan data');
}
return response.data;
};
finalTdkFinalApi.delete = async (config: TPortBulananRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
deleteAPI,
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to delete bulanan data');
}
return response.data;
};
finalTdkFinalApi.batal = async (config: TPortBulananCenceledRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
canceled,
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to delete bulanan data');
}
return response.data;
};
export default finalTdkFinalApi;
export * from './final-tdk-final-list-view';
export * from './final-tdk-final-rekam-view';
......@@ -6,8 +6,8 @@ import {
import type { AxiosError } from 'axios';
import dayjs from 'dayjs';
import { endpoints, fetcherCetakPDF } from 'src/lib/axios-ctas-box';
import { FG_FASILITAS_PPH_21_TEXT, KODE_OBJEK_PAJAK_TEXT } from './bupot-bulanan/constant';
import queryKey from './bupot-bulanan/constant/queryKey';
import { FG_FASILITAS_PPH_21_TEXT, KODE_OBJEK_PAJAK_TEXT } from './constant';
import queryKey from './constant/queryKey';
interface ApiCetakResponse {
KdStatus: string;
......
......@@ -4,9 +4,17 @@
// } from '@pjap/efaktur/constants/efakturConstant';
export const FG_FASILITAS = {
YES: '1',
NO: '0',
BUPOT26: '2',
SKB_PPH_PASAL_22: '1',
SKB_PPH_PASAL_23: '2',
SKB_PPH_PHTB: '3',
DTP: '4',
SKB_PPH_BUNGA_DEPOSITO_DANA_PENSIUN_TABUNGAN: '5',
SUKET_PP23_PP52: '6',
SKD_WPLN: '7',
FASILITAS_LAINNYA: '8',
TANPA_FASILITAS: '9',
SKB_PPH_PASAL_21: '10',
DTP_PPH_PASAL_21: '11',
};
export const FG_PERHITUNGAN = {
......@@ -747,3 +755,15 @@ export const ActionRekam = {
RETUR: 'retur',
PEMBETULAN: 'pembetulan',
};
export const MockNitku = [
{
value: '1091031210912281000000',
label: '1091031210912281000000',
},
{
value: '1091031210912281000001',
label: '1091031210912281000001',
},
];
\ No newline at end of file
export const appRootKey = 'bupot-21-26';
export const bulanan = 'bulanan';
export const bupotfinal = 'bupot-final-tidak-final'
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
bulanan: {
all: (params: any) => [appRootKey, bulanan, params],
detail: (params: any) => [appRootKey, bulanan, 'detail', params],
draft: [appRootKey, bulanan, 'draft'],
delete: [appRootKey, bulanan, 'delete'],
upload: [appRootKey, bulanan, 'upload'],
cancel: [appRootKey, bulanan, 'cancel'],
cetakPdf: (params: any) => [appRootKey, bulanan,'cetak-pdf', params],
},
bupotfinal: {
all: (params: any) => [appRootKey, bupotfinal, params],
detail: (params: any) => [appRootKey, bupotfinal, 'detail', params],
draft: [appRootKey, bupotfinal, 'draft'],
delete: [appRootKey, bupotfinal, 'delete'],
upload: [appRootKey, bupotfinal, 'upload'],
cancel: [appRootKey, bupotfinal, 'cancel'],
cetakPdf: (params: any) => [appRootKey, bupotfinal,'cetak-pdf', params],
},
};
export default queryKey;
This diff is collapsed.
This diff is collapsed.
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