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;
......@@ -7,18 +7,44 @@ type FilterItem = {
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> = {
noBupot: 'nomorBupot',
// Tambahkan mapping lain jika diperlukan di sini
// Contoh: 'frontendField': 'backendField'
};
const dbField = (field: string) => fieldMap[field] ?? field;
const escape = (v: string) => String(v).replace(/'/g, "''");
/**
* 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, "''");
const toDbDate = (value: string | Date) => {
/**
* 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');
......@@ -28,11 +54,22 @@ export function useAdvancedFilter() {
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 '';
}
};
const normalizeOp = (op: string) => op?.toString().trim();
/**
* ✅ IMPROVED: Normalize operator dengan lowercase
*/
const normalizeOp = (op: string): string =>
op?.toString().trim().toLowerCase() || '';
function buildAdvancedFilter(filters?: FilterItem[] | null) {
/**
* Build advanced filter SQL WHERE clause
*/
function buildAdvancedFilter(filters?: FilterItem[] | null): string {
if (!filters || filters.length === 0) return '';
const exprs: string[] = [];
......@@ -41,54 +78,70 @@ export function useAdvancedFilter() {
for (let i = 0; i < filters.length; i++) {
const f = filters[i];
if (!f || !f.field) continue;
const op = normalizeOp(f.operator ?? '');
const fieldName = dbField(f.field);
let expr: string | null = null;
// --- DATE FIELDS ---
// ============================================
// 1️⃣ DATE FIELDS
// ============================================
if (dateFields.has(fieldName)) {
const rawVal = f.value;
if (!rawVal && !/is empty|is not empty/i.test(op)) continue;
if (!rawVal && !op.match(/is empty|is not empty/)) continue;
const ymd = toDbDate(rawVal as string | Date);
if (!ymd) continue;
if (/^is$/i.test(op)) {
if (op === 'is') {
expr = `"${fieldName}" >= '${ymd} 00:00:00' AND "${fieldName}" <= '${ymd} 23:59:59'`;
} else if (/is on or after/i.test(op)) {
} else if (op === 'is on or after') {
expr = `"${fieldName}" >= '${ymd}'`;
} else if (/is on or before/i.test(op)) {
} else if (op === 'is on or before') {
expr = `"${fieldName}" <= '${ymd}'`;
}
}
// --- EMPTY ---
if (/is empty/i.test(op)) {
expr = `LOWER("${fieldName}") IS NULL`;
} else if (/is not empty/i.test(op)) {
expr = `LOWER("${fieldName}") IS NOT NULL`;
// ============================================
// 2️⃣ EMPTY/NOT EMPTY
// ============================================
if (op === 'is empty') {
expr = `"${fieldName}" IS NULL`;
} else if (op === 'is not empty') {
expr = `"${fieldName}" IS NOT NULL`;
}
// --- IS ANY OF ---
if (!expr && /is any of/i.test(op)) {
// ============================================
// 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')
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];
} else if (f.value != null) {
values = [f.value as any];
}
if (values.length > 0) {
if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
// ✅ 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 LOWER('%${s}%')`;
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}'`;
......@@ -98,85 +151,100 @@ export function useAdvancedFilter() {
}
}
// --- FGSTATUS special single-value is / is not ---
if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
// ============================================
// 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/i.test(op)) {
if (valRaw !== '' && !['is any of', 'is empty', 'is not empty'].includes(op)) {
const valEscaped = escape(valRaw.toLowerCase());
if (/^is$/i.test(op)) {
expr = `LOWER("${fieldName}") LIKE LOWER('%${valEscaped}%')`;
} else if (/is not/i.test(op)) {
expr = `LOWER("${fieldName}") NOT LIKE LOWER('%${valEscaped}%')`;
if (op === 'is') {
expr = `LOWER("${fieldName}") LIKE '%${valEscaped}%'`;
} else if (op === 'is not') {
expr = `LOWER("${fieldName}") NOT LIKE '%${valEscaped}%'`;
}
}
}
// --- GENERIC ---
// ============================================
// 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);
if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
// Numeric field operators
if (isNumericField && /^(=|>=|<=|>|<)$/.test(op)) {
expr = `"${fieldName}" ${op} '${valEscaped}'`;
} else if (/^contains$/i.test(op)) {
expr = `LOWER("${fieldName}") LIKE LOWER('%${valEscaped}%')`;
} else if (/^equals$/i.test(op)) {
}
// 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 (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
expr = `LOWER("${fieldName}") ${op} '${valEscaped}'`;
} else if (/^(is)$/i.test(op)) {
}
else if (op === 'is') {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
} else {
}
// Fallback
else {
expr = `LOWER("${fieldName}") = '${valEscaped}'`;
}
}
}
// Add expression with proper join
if (expr) {
exprs.push(expr);
const joinBefore = f.join ?? (exprs.length > 1 ? 'AND' : 'AND');
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: Clean undefined values dan handle sorting dengan benar
* ✅ FIXED: Tidak ada lagi field mapping untuk noBupot
* Build request parameters dengan clean up undefined values
*/
function buildRequestParams(base: BaseParams = {}, advanced: string) {
function buildRequestParams(base: BaseParams = {}, advanced: string): BaseParams {
const out: BaseParams = {};
// ✅ Copy semua base params kecuali yang undefined
// Copy all defined params
Object.keys(base).forEach((key) => {
if (base[key] !== undefined) {
out[key] = base[key];
}
});
// ✅ Field mapping
if ('noBupot' in out) {
out.nomorBupot = out.noBupot;
delete out.noBupot;
}
// ✅ REMOVED: Field mapping noBupot → nomorBupot
// Sekarang frontend dan backend sama-sama gunakan 'noBupot'
// ✅ Hanya tambahkan advanced jika ada isinya
// Add advanced filter if exists
if (advanced && advanced.trim() !== '') {
out.advanced = advanced.trim();
}
// ✅ Clean up undefined sorting (jangan kirim ke backend)
// Clean up undefined sorting params
if (out.sortingMode === undefined) {
delete out.sortingMode;
}
......@@ -187,5 +255,8 @@ export function useAdvancedFilter() {
return out;
}
return { buildAdvancedFilter, buildRequestParams } as const;
return {
buildAdvancedFilter,
buildRequestParams
} as const;
}
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