Commit 92033d4f authored by Rais Aryaguna's avatar Rais Aryaguna

feat: add ModalUploadBulanan component for uploading data with progress tracking

refactor: update useAdvancedFilter hook for improved SQL clause building and operator normalization

feat: implement useCencelBulanan hook for canceling monthly records

feat: create useCetakPdfBulanan hook for generating PDF reports

feat: add useDeleteBulanan hook for deleting monthly records

fix: update useUploadBulanan hook to use unified request type

chore: update types for bulanan requests and responses

feat: enhance bulanan API utility functions for delete and cancel operations

feat: integrate new modals (upload, delete, cancel, print) into bulanan list view

fix: replace DialogPenandatangan with ModalUploadBulanan in bulanan record view

feat: add cetakpdf.ts for handling PDF generation logic
parent d86bf9fc
......@@ -82,6 +82,16 @@ export const axiosHitung = createAxiosInstance(
}
);
export const axiosCetakPDF = createAxiosInstance(
`${API_CONFIGS.apiJava.baseURL}:${API_CONFIGS.apiJava.portCetak}`,
`${API_CONFIGS.apiJava.name} Cetak`,
{
useBasicAuth: true,
username: API_CONFIGS.apiJava.AuthUser,
password: API_CONFIGS.apiJava.AuthPass,
}
);
export default axiosnodesandbox;
type FetcherArgs = string | [string, AxiosRequestConfig & { method?: string }];
......@@ -122,6 +132,28 @@ export const fetcherHitung = async <T = unknown>(args: FetcherArgs): Promise<T>
}
};
export const fetcherCetakPDF = async <T = unknown>(args: FetcherArgs): Promise<T> => {
try {
const [url, config = {}] = Array.isArray(args) ? args : [args, {}];
const { method = 'GET', headers, ...restConfig } = config;
const res = await axiosCetakPDF.request<T>({
url,
method,
headers: {
password: '',
...headers,
},
...restConfig,
});
return res.data;
} catch (error) {
console.error('[Java API] Fetcher failed:', error);
throw error;
}
};
export const endpoints = {
pph21: {
bulanan: {
......@@ -154,4 +186,9 @@ export const endpoints = {
tahunan: '/pph21/v1/hitung/ctas/yearly',
tahunanA2: 'IF_TXR_055/a2',
},
cetak: {
bulanan: '/report/ctas/bpm',
finalTidakFinal: '/report/ctas/bp21',
tahunanA1: '/report/ctas/a1',
},
} as const;
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;
......@@ -3,7 +3,7 @@ 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, useState } from 'react';
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';
......@@ -11,8 +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 from '../constant/queryKey';
import queryKey, { appRootKey, bulanan } from '../constant/queryKey';
import useUploadBulanan from '../hooks/useUploadeBulanan';
import { createTableKey, useTablePagination } from '../../paginationStore';
interface DialogPenandatanganProps {
dataSelected?: GridRowSelectionModel;
......@@ -53,6 +54,9 @@ const DialogPenandatangan: React.FC<DialogPenandatanganProps> = ({
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);
......@@ -110,7 +114,7 @@ const DialogPenandatangan: React.FC<DialogPenandatanganProps> = ({
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: queryKey.bulanan.all('') });
queryClient.invalidateQueries({ queryKey: queryKey.bulanan.all({page: paginationState.page + 1, limit: paginationState.pageSize}) });
}
};
......
import React, { useEffect, useMemo, useState } from 'react';
import { Stack, Button, Typography } from '@mui/material';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import minMax from 'dayjs/plugin/minMax';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import 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';
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;
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 useDeleteBulanan from '../hooks/useDeleteBulanan';
import queryKey, { appRootKey, bulanan } from '../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 ModalDeleteBulanan: 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 } = useDeleteBulanan({
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 ModalDeleteBulanan;
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 '../constant/queryKey';
import useUploadBulanan from '../hooks/useUploadeBulanan';
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 ModalUploadBulanan: 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 ModalUploadBulanan;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import queryKey from '../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 '../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 bulananApi from '../utils/api';
import type { TPortBulananRequest } from '../types/types';
import queryKey from '../constant/queryKey';
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 { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import bulananApi from '../utils/api';
import type { TPortBulananUploadRequest } from '../types/types';
import type { TPortBulananRequest } from '../types/types';
import queryKey from '../constant/queryKey';
const useUploadBulanan = (
props?: Omit<
UseMutationOptions<any, Error, TPortBulananUploadRequest, unknown>,
UseMutationOptions<any, Error, TPortBulananRequest, unknown>,
'mutationKey' | 'mutationFn'
>
) =>
useMutation<any, Error, TPortBulananUploadRequest, unknown>({
useMutation<any, Error, TPortBulananRequest, unknown>({
mutationKey: queryKey.bulanan.upload,
mutationFn: (params: TPortBulananUploadRequest) => bulananApi.upload(params),
mutationFn: (params: TPortBulananRequest) => bulananApi.upload(params),
...props,
});
......
......@@ -225,6 +225,10 @@ export type TPostBulananRequest = {
fgGrossUp: number;
};
export type TPortBulananUploadRequest = {
export type TPortBulananRequest = {
id: string;
};
export type TPortBulananCenceledRequest = {
tglPembatalan: string
} & TPortBulananRequest
\ No newline at end of file
......@@ -4,7 +4,8 @@ import type {
TBaseResponseAPI,
TGetListDataKOPDnResult,
TGetListDataTableDnResult,
TPortBulananUploadRequest,
TPortBulananCenceledRequest,
TPortBulananRequest,
TPostBulananRequest,
} from '../types/types';
......@@ -61,7 +62,7 @@ bulananApi.save = async (config: TPostBulananRequest) => {
return response.data;
};
bulananApi.upload = async (config: TPortBulananUploadRequest) => {
bulananApi.upload = async (config: TPortBulananRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
'/IF_TXR_028/a0/upload',
{
......@@ -77,4 +78,36 @@ bulananApi.upload = async (config: TPortBulananUploadRequest) => {
return response.data;
};
bulananApi.delete = async (config: TPortBulananRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
'/IF_TXR_028/a0/delete',
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to delete bulanan data');
}
return response.data;
};
bulananApi.batal = async (config: TPortBulananCenceledRequest) => {
const response = await fetcher<TBaseResponseAPI<BupotRecord>>([
'/IF_TXR_028/a0/batal',
{
method: 'POST',
data: config,
},
]);
if (!response || response.status !== 'success') {
throw new Error(response?.message || 'Failed to delete bulanan data');
}
return response.data;
};
export default bulananApi;
......@@ -37,6 +37,10 @@ import { FG_STATUS_BUPOT } from '../constant';
import { appRootKey, bulanan } from '../constant/queryKey';
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:
......@@ -589,10 +593,46 @@ export function BulananListView() {
}}
/>
{modals.delete && <div>Modatal delete</div>}
{modals.upload && <div>Modatal upload</div>}
{modals.cancel && <div>Modatal cancel</div>}
{modals.preview && previewPayload && <div>Modatal preview</div>}
{modals.delete && (
<ModalDeleteBulanan
dataSelected={rowSelectionModel}
setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef}
isOpenDialogDelete={modals.delete}
setIsOpenDialogDelete={() => toggleModal('delete', false)}
successMessage="Data berhasil dihapus"
/>
)}
{modals.upload && (
<ModalUploadBulanan
dataSelected={rowSelectionModel}
setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef}
isOpenDialogUpload={modals.upload}
setIsOpenDialogUpload={() => toggleModal('upload', false)}
successMessage="Data berhasil diupload"
/>
)}
{modals.cancel && (
<ModalCancelBulanan
dataSelected={dataSelectedRef.current}
setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef}
isOpenDialogCancel={modals.cancel}
setIsOpenDialogCancel={() => toggleModal('cancel', false)}
successMessage="Data berhasil Canceled"
/>
)}
{modals.preview && previewPayload && (
<ModalCetakPdfBulanan
payload={previewPayload}
isOpen={modals.preview}
onClose={() => {
toggleModal('preview', false);
setPreviewPayload(undefined);
}}
/>
)}
</DashboardContent>
);
}
......@@ -17,7 +17,7 @@ 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/DialogPenandatangan';
import DialogPenandatangan from '../components/ModalUploadBulanan';
import Identitas from '../components/rekam/Identitas';
import JumlahPerhitunganForm from '../components/rekam/JumlahPerhitunganForm';
import PanduanDnRekam from '../components/rekam/PanduanDnRekam';
......
import {
useMutation,
type UseMutationOptions,
type UseMutationResult,
} from '@tanstack/react-query';
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';
interface ApiCetakResponse {
KdStatus: string;
MsgStatus: string;
arraybuff: string;
url: string;
}
export interface transfromParamCetakBupotBulananProps {
noBupot: string;
masaPajak: string;
tahunPajak: string;
pasalPPh: string;
status: string;
statusPPh: string;
npwpDipotong: string;
namaDipotong: string;
nitkuDipotong: string;
sertifikatInsentifDipotong: string;
kodeObjekPajak: string;
namaObjekPajak: string;
penghasilanBruto: string;
tarif: string;
pphDipotong: string;
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
npwpPemotong: string;
namaPemotong: string;
nitkuPemotong: string;
tglPemotongan: string;
namaPenandatangan: string;
nomorSertifikatInsentif: string;
metodePembayaranBendahara: string;
nomorSP2D: string;
qrcode: string;
mixcode: string;
}
export interface transFromPramsCetakBupotFinalTidakFinalProps {
noBupot: string;
masaPajak: string;
tahunPajak: string;
pasalPPh: string;
status: string;
statusPPh: string;
npwp: string;
namaDipotong: string;
nitkuDipotong: string;
sertifikatInsentifDipotong: string;
kodeObjekPajak: string;
namaObjekPajak: string;
penghasilanBruto: string;
tarif: string;
pphDipotong: string;
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
npwpPemotong: string;
namaPemotong: string;
nitkuPemotong: string;
tglPemotongan: string;
namaPenandatangan: string;
nomorSertifikatInsentif: string;
metodePembayaranBendahara: string;
nomorSP2D: string;
qrcode: string;
mixcode: string;
}
export interface transFromCetakBupotTahuananA1 {
qrcode: string;
mixcode: string;
noBupot: string;
status: string;
fgSatuPemberiKerja: string;
fgStatusPemotonganPph: string;
pasalPPh: string;
masaPajakAwal: string;
masaPajakAkhir: string;
tahunPajak: string;
npwpDipotong: string;
namaDipotong: string;
alamat: string;
jnsKelamin: string;
statusPtkp: string;
jmlPtkp: string;
nominalPtkp: string;
kodeObjekPajak: string;
pasalPph: string;
fgKaryawanAsing: string;
passport: string;
kdNegara: string;
posisiJabatan: string;
gajiPensiun: string;
tunjanganPPh: string;
tunjanganLainnyaLembur: string;
honorarium: string;
premiAsuransi: string;
natura: string;
tantiemBonus: string;
biayaJabatan: string;
iuranPensiun: string;
zakat: string;
fgFasilitas: string;
totalPenghasilanBruto: string;
totalPengurang: string;
totalPenghasilanNeto: string;
totalPenghasilanNetoDariBupotSebelumnya: string;
totalPenghasilanNetoPph21: string;
pkpSetahunDisetahunkan: string;
pph21SetahunDisetahunkan: string;
pph21Terutang: string;
pph21DariBupotSebelumnya: string;
pph21DapatDikreditkan: string;
pph21WithheldDtp: string;
pph21KurangLebihBayar: string;
tglPemotongan: string;
namaPenandatangan: string;
namaPemotong: string;
npwpPemotong: string;
nitkuPemotong: string;
}
type MutationProps<T> = Omit<
UseMutationOptions<ApiCetakResponse, AxiosError, T>,
'mutationKey' | 'mutationFn'
>;
const transformParamsBulanan = ({
keterangan1,
keterangan2,
keterangan3,
keterangan4,
...params
}: any): transfromParamCetakBupotBulananProps => ({
...params,
noBupot: params.noBupot,
masaPajak: params.msPajak,
tahunPajak: params.thnPajak,
pasalPPh: params.pasalPPh,
status: 'Proforma',
email: params.email || '',
namaNegara: params.countryCode || '',
statusPPh: params.statusPPh, // Final atau Tidak Final
npwpDipotong: params.npwp16Dipotong,
namaDipotong: params.namaDipotong,
nitkuDipotong: params.idDipotong,
sertifikatInsentifDipotong: FG_FASILITAS_PPH_21_TEXT[params.fgFasilitas],
nomorDokumen: params.noDokLainnya || '-',
tanggal_Dokumen: '-',
dokReferensi: '-',
kodeObjekPajak: params.kdObjPjk,
namaObjekPajak: KODE_OBJEK_PAJAK_TEXT[params.kdObjPjk],
penghasilanBruto: `${Number(params.bruto) + Number(params.pph21ditanggungperusahaan)}`,
tarif: `${params.tarif}`,
pphDipotong: `${params.pphDipotong}`,
npwpPemotong: params.npwp16Pemotong,
namaPemotong: params.namaPemotong,
nitkuPemotong: params.nitkuPemotong,
tglPemotongan: dayjs(params.tglpemotongan).format('DD MMMM YYYY'),
namaPenandatangan: params.namaPenandatangan,
nomorSertifikatInsentif: params.noDokLainnya,
metodePembayaranBendahara: '',
nomorSP2D: '',
qrcode: 'qrcode',
mixcode: 'mixcode',
link: '',
});
const { bulanan, finalTidakFinal, tahunanA1 } = endpoints.cetak;
const cetakBulanan = async (
params: transfromParamCetakBupotBulananProps
): Promise<ApiCetakResponse> => {
const response = await fetcherCetakPDF<ApiCetakResponse>([
bulanan,
{
method: 'POST',
data: transformParamsBulanan(params),
},
]);
if (!response.url) {
throw new Error('Gagal cetak PDF Bulana ');
}
return response;
};
const cetakFinalTidakFinal = async (
params: transFromPramsCetakBupotFinalTidakFinalProps
): Promise<ApiCetakResponse> => {
const response = await fetcherCetakPDF<ApiCetakResponse>([
finalTidakFinal,
{
method: 'POST',
data: params,
},
]);
if (!response.url) {
throw new Error('Gagal cetak PDF Bulana ');
}
return response;
};
const cetakTahunanA1 = async (params: any): Promise<ApiCetakResponse> => {
const response = await fetcherCetakPDF<ApiCetakResponse>([
tahunanA1,
{
method: 'POST',
data: params,
},
]);
if (!response.url) {
throw new Error('Gagal cetak PDF Bulana ');
}
return response;
};
export function useCetakBulanan(
props?: MutationProps<transfromParamCetakBupotBulananProps>
): UseMutationResult<ApiCetakResponse, AxiosError, transfromParamCetakBupotBulananProps> {
return useMutation({
mutationKey: queryKey.bulanan.cetakPdf(bulanan),
mutationFn: cetakBulanan,
...props,
});
}
export function useCetakFinalTidakFinal(
props?: MutationProps<transFromPramsCetakBupotFinalTidakFinalProps>
): UseMutationResult<ApiCetakResponse, AxiosError, transFromPramsCetakBupotFinalTidakFinalProps> {
return useMutation({
mutationKey: queryKey.bulanan.cetakPdf(finalTidakFinal),
mutationFn: cetakFinalTidakFinal,
...props,
});
}
export function useCetakTahunanA1(
props?: MutationProps<transFromCetakBupotTahuananA1>
): UseMutationResult<ApiCetakResponse, AxiosError, transFromCetakBupotTahuananA1> {
return useMutation({
mutationKey: queryKey.bulanan.cetakPdf(tahunanA1),
mutationFn: cetakTahunanA1,
...props,
});
}
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