Commit eaae2cf4 authored by Fachri's avatar Fachri

Retur PM (Advanced Filter, Toolbar, Rekam)

parent ba652467
import { CONFIG } from 'src/global-config';
import { ReturFakturPmListView } from 'src/sections/faktur/returPm/view';
const metadata = { title: `Retur Faktur PM - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<ReturFakturPmListView />
</>
);
}
import { CONFIG } from 'src/global-config';
import ReturRekamPmView from 'src/sections/faktur/returPm/view/returPmRekamView';
const metadata = { title: `Retur Faktur PM - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<ReturRekamPmView />
</>
);
}
......@@ -115,23 +115,20 @@ export const paths = {
bulananUbah: (id: string) => `${ROOTS.PPH21}/bulanan/${id}/ubah`,
bulananPengganti: (id: string) => `${ROOTS.PPH21}/bulanan/${id}/pengganti`,
bupotFinal: `${ROOTS.PPH21}/bupot-final`,
bupotFinalRekam: `${ROOTS.PPH21}/bupot-final/rekam`,
bupotFinalEdit: (id: string, path:string) => `${ROOTS.PPH21}/bupot-final/${id}/${path}`,
tahunan: `${ROOTS.PPH21}/tahunan`,
tahunanRekam: `${ROOTS.PPH21}/tahunan/rekam`,
bupotTahunanEdit: (id: string, path:string) => `${ROOTS.PPH21}/tahunan/${id}/${path}`,
detailsBupotFinal: (id: string) => `${ROOTS.PPH21}/bupot-final/${id}`,
tahuan: `${ROOTS.PPH21}/tahunan`,
detailstahuan: (id: string) => `${ROOTS.PPH21}/tahunan/${id}`,
tahunanA2: `${ROOTS.PPH21}/tahunan-a2`,
detailstahunanA2: (id: string) => `${ROOTS.PPH21}/tahunan-a2/${id}`,
bupot26: `${ROOTS.PPH21}/bupot-26`,
bupot26Rekam: `${ROOTS.PPH21}/bupot-26/rekam`,
bupot26Edit: (id: string, path:string) => `${ROOTS.PPH21}/bupot-26/${id}/${path}`,
detailsbupot26: (id: string) => `${ROOTS.PPH21}/bupot-26/${id}`,
},
faktur: {
root: ROOTS.FAKTUR,
pm: `${ROOTS.FAKTUR}/pm`,
pk: `${ROOTS.FAKTUR}/pk`,
rekamPk: `${ROOTS.FAKTUR}/pk/new`,
returRekamPm: (id: string) => `${ROOTS.FAKTUR}/retur-pm/${id}/retur`,
returPm: `${ROOTS.FAKTUR}/retur/pm`,
returPk: `${ROOTS.FAKTUR}/retur/pk`,
dlk: `${ROOTS.FAKTUR}/dokumen-lain/pajak-keluaran`,
......
......@@ -46,6 +46,11 @@ const OverviewFakturPkRekamPage = lazy(() => import('src/pages/faktur/fakturReka
const OverviewFakturPmPage = lazy(() => import('src/pages/faktur/fakturPm'));
// Retur
const OverviewReturFakturPmPage = lazy(() => import('src/pages/faktur/returPm'));
const OverviewReturRekamFakturPmPage = lazy(() => import('src/pages/faktur/returRekamPm'));
// Overview
const IndexPage = lazy(() => import('src/pages/dashboard'));
......@@ -176,6 +181,8 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'pk/new', element: <OverviewFakturPkRekamPage /> },
{ path: 'pk/:id/:type', element: <OverviewFakturPkRekamPage /> },
{ path: 'pm', element: <OverviewFakturPmPage /> },
{ path: 'retur/pm', element: <OverviewReturFakturPmPage /> },
{ path: 'retur-pm/:id/:type', element: <OverviewReturRekamFakturPmPage /> },
],
},
];
......@@ -473,15 +473,6 @@ const FakturPkRekamView = () => {
disabled={!isCheckedAgreement}
variant="outlined"
sx={{ color: '#143B88' }}
// onClick={async () => {
// try {
// const values = methods.getValues();
// await onSubmit(values);
// } catch (error) {
// console.error('❌ Validation error:', error);
// }
// }}
// onClick={methods.handleSubmit(onSubmit)}
onClick={methods.handleSubmit((data) => {
const raw = methods.getValues(); // 🔥 ambil full form
......@@ -489,8 +480,6 @@ const FakturPkRekamView = () => {
...data,
objekFaktur: raw.objekFaktur, // merge manual
};
// onSubmit(final);
onSubmit(final);
})}
>
......
......@@ -75,6 +75,10 @@ const numericFields = new Set([
'ppnbm',
'jumlahuangmuka',
'jumlahUangMuka',
'masakreditpajak',
'masaKreditPajak',
'tahunkreditpajak',
'tahunKreditPajak',
]);
// date fields
......
// import React, { useEffect, useState } from 'react';
// import { useQueryClient } from '@tanstack/react-query';
// import { enqueueSnackbar } from 'notistack';
// import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
// import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
// import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
// import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
// import useCancelFakturPk from '../../hooks/useCancelFakturPK';
// interface ModalCancelFakturProps {
// dataSelected?: GridRowSelectionModel;
// setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
// tableApiRef?: React.MutableRefObject<any>;
// isOpenDialogCancel: boolean;
// setIsOpenDialogCancel: (v: boolean) => void;
// successMessage?: string;
// onConfirmCancel?: () => Promise<void> | void;
// }
// /**
// * Normalize various possible shapes of grid selection model into array of ids.
// * Handles:
// * - array of ids: [1, 2, 'a']
// * - Set-of-ids: new Set([1,2])
// * - object like { ids: Set(...), type: 'include' }
// * - fallback: tries to extract keys if it's an object map
// */
// const normalizeSelection = (sel?: any): (string | number)[] => {
// if (!sel) return [];
// if (Array.isArray(sel)) return sel as (string | number)[];
// if (sel instanceof Set) return Array.from(sel) as (string | number)[];
// if (typeof sel === 'object') {
// // common shape from newer MUI: { ids: Set(...), type: 'include' }
// if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// // maybe it's a map-like object { '1': true, '2': true }
// const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
// if (maybeIds.length > 0) {
// // try to convert numeric-like keys to number where applicable
// return maybeIds.map((k) => {
// const n = Number(k);
// return Number.isNaN(n) ? k : n;
// });
// }
// }
// return [];
// };
// const ModalReturFakturPM: React.FC<ModalCancelFakturProps> = ({
// dataSelected,
// setSelectionModel,
// tableApiRef,
// isOpenDialogCancel,
// setIsOpenDialogCancel,
// successMessage = 'Data berhasil dibatalkan',
// onConfirmCancel,
// }) => {
// const queryClient = useQueryClient();
// // custom hooks for progress state
// const {
// numberOfData,
// setNumberOfData,
// numberOfDataFail,
// numberOfDataProcessed,
// numberOfDataSuccess,
// processSuccess,
// processFail,
// resetToDefault,
// status,
// } = useDialogProgressBar();
// const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// // React Query mutation for delete
// const { mutateAsync } = useCancelFakturPk({
// 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);
// setSelectionModel?.(undefined);
// };
// const handleCloseModal = () => {
// setIsOpenDialogCancel(false);
// resetToDefault();
// };
// const onSubmit = async () => {
// try {
// setIsOpenDialogProgressBar(true);
// await handleMultipleDelete();
// enqueueSnackbar(successMessage, { variant: 'success' });
// await onConfirmCancel?.();
// handleCloseModal();
// clearSelection();
// } catch (error: any) {
// enqueueSnackbar(error?.message || 'Gagal membantalkan data', { variant: 'error' });
// } finally {
// // sesuaikan queryKey jika perlu; tetap panggil invalidasi
// queryClient.invalidateQueries({ queryKey: ['faktur', 'pk'] });
// }
// };
// useEffect(() => {
// setNumberOfData(normalizeSelection(dataSelected).length);
// }, [isOpenDialogCancel, dataSelected, setNumberOfData]);
// return (
// <>
// <DialogConfirm
// fullWidth
// maxWidth="xs"
// title="Apakah Anda yakin akan melakukan Retur Pajak Masukan?"
// description=""
// actionTitle="Iya"
// isOpen={isOpenDialogCancel}
// 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 ModalReturFakturPM;
import React, { useMemo } from 'react';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import { useNavigate } from 'react-router';
interface ModalReturFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
type?: 'retur' | 'ubah';
}
/**
* Ambil single ID dari berbagai bentuk rowSelectionModel
*/
const getSingleId = (sel?: any): string | null => {
if (!sel) return null;
if (Array.isArray(sel)) return sel[0]?.toString() ?? null;
if (sel instanceof Set) return Array.from(sel)[0]?.toString() ?? null;
if (sel?.ids instanceof Set) return Array.from(sel.ids)[0]?.toString() ?? null;
return null;
};
const ModalReturFakturPM: React.FC<ModalReturFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
type = 'retur',
}) => {
const navigate = useNavigate();
const selectedId = useMemo(() => getSingleId(dataSelected), [dataSelected]);
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
};
const clearSelection = () => {
tableApiRef?.current?.setRowSelectionModel?.([]);
setSelectionModel?.(undefined);
};
const handleSubmit = () => {
if (!selectedId) return;
handleCloseModal();
clearSelection();
navigate(`/faktur/retur-pm/${selectedId}/${type}`);
};
return (
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan melakukan Retur Pajak Masukan?"
description=""
actionTitle="Iya"
isOpen={isOpenDialogCancel}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={handleSubmit}
/>
);
};
export default ModalReturFakturPM;
......@@ -4,7 +4,7 @@ import fakturApi from '../utils/api';
const useCancelFakturPk = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelRequest>({
mutationKey: ['cancel-faktur-pk'],
mutationKey: ['cancel-faktur-pm'],
mutationFn: (payload) => fakturApi.cancel(payload),
...props,
});
......
......@@ -30,7 +30,7 @@ import { enqueueSnackbar } from 'notistack';
import { usePaginationStore } from '../store/paginationStore';
import StatusChip from '../components/StatusChip';
import { useDebounce, useThrottle } from 'src/shared/hooks/useDebounceThrottle';
import useAdvancedFilter from '../hooks/useAdvancedFilterFakturPK';
import useAdvancedFilter from '../hooks/useAdvancedFilterFakturPM';
import { CustomToolbar } from '../components/CustomToolbar';
import useGetFakturPM, { formatDateToDDMMYYYY } from '../hooks/useGetFakturPM';
import { formatRupiah } from 'src/shared/FormatRupiah/FormatRupiah';
......@@ -38,6 +38,7 @@ import ModalCetakFakturPM from '../components/dialog/ModalCetakFakturPM';
import ModalUploadCreditedPM from '../components/dialog/ModalUploadCreditedPM';
import ModalConfirmationWaitingFaktur from '../components/dialog/ModalConfirmationWaitingFakturPM';
import ModalPrepolulatedPM from '../components/dialog/ModalPrepopulatedPM';
import ModalReturFakturPM from '../components/dialog/ModalReturFakturPM';
export type IColumnGrid = GridColDef & {
field:
......@@ -108,6 +109,7 @@ export function FakturPmListView() {
const [previewPayload, setPreviewPayload] = useState<Record<string, any> | undefined>(undefined);
const [statusPengkreditan, setStatusPengkreditan] = useState<TPengkreditan>('CREDITED');
const [isPrepopulatedOpen, setIsPrepopulatedOpen] = useState(false);
const [isReturOpen, setIsReturOpen] = useState(false);
const dataSelectedRef = useRef<any[]>([]);
const [selectionVersion, setSelectionVersion] = useState(0);
......@@ -422,24 +424,35 @@ export function FakturPmListView() {
canReplacement: false,
canCancel: false,
canCredit: false,
canRetur: false,
};
}
const isSingle = count === 1;
// const allDraft = dataSelected.every((d) => d.statusfaktur === FG_STATUS_FAKTUR_PK.DRAFT);
const allApproved = dataSelected.every((d) => d.statusfaktur === FG_STATUS_FAKTUR_PK.APPROVED);
const allWaitingAmendment = dataSelected.every(
(d) => d.statusfaktur === 'WAITING FOR AMENDMENT'
);
const allWaitingCancellation = dataSelected.every(
(d) => d.statusfaktur === 'WAITING FOR CANCELLATION'
);
const allValidForRetur = dataSelected.every(
(d) =>
d.statusfaktur === FG_STATUS_FAKTUR_PK.APPROVED &&
d.buyerstatus !== 'INVALID' &&
d.statuspembeli != null
);
return {
canDetail: isSingle,
canReplacement: allWaitingAmendment,
canCancel: allWaitingCancellation,
canCredit: allApproved,
canRetur: isSingle && allValidForRetur,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectionVersion]);
......@@ -492,8 +505,8 @@ export function FakturPmListView() {
{
title: 'Retur',
icon: <SwapHorizontalCircleTwoTone sx={{ width: 26, height: 26 }} />,
func: () => setIsPenggantiModalOpen(true),
disabled: !validatedActions.canReplacement,
func: () => setIsReturOpen(true),
disabled: !validatedActions.canRetur,
},
],
[
......@@ -623,6 +636,17 @@ export function FakturPmListView() {
/>
</DashboardContent>
{isReturOpen && (
<ModalReturFakturPM
dataSelected={rowSelectionModel}
setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef}
isOpenDialogCancel={isReturOpen}
setIsOpenDialogCancel={setIsReturOpen}
type="retur"
/>
)}
{isUploadModalOpen && (
<ModalUploadCreditedPM
dataSelected={rowSelectionModel}
......
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;
This diff is collapsed.
import * as React from 'react';
import type { GridToolbarProps } from '@mui/x-data-grid-premium';
import { GridToolbarContainer } from '@mui/x-data-grid-premium';
import { Stack, Divider, IconButton, Tooltip } from '@mui/material';
import type { ActionItem } from '../types/types';
import { CustomFilterButton } from './CustomFilterButton';
import CustomColumnsButton from './CustomColumnsButton';
interface CustomToolbarProps extends GridToolbarProps {
actions?: ActionItem[][];
columns: any[]; // GridColDef[]
filterModel: any;
setFilterModel: (m: any) => void;
statusOptions?: { value: string; label: string }[];
}
// ✅ React.memo mencegah render ulang kalau props sama
export const CustomToolbar = React.memo(function CustomToolbar({
actions = [],
columns,
filterModel,
setFilterModel,
statusOptions = [],
...gridToolbarProps
}: CustomToolbarProps) {
return (
<GridToolbarContainer
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 1.5,
}}
{...gridToolbarProps}
>
<Stack direction="row" alignItems="center" gap={1}>
{actions.map((group, groupIdx) => (
<Stack key={groupIdx} direction="row" gap={0.5} alignItems="center">
{group.map((action, idx) => (
<Tooltip key={idx} title={action.title}>
<span>
<IconButton
sx={{ color: action.disabled ? 'action.disabled' : '#123375' }}
size="small"
onClick={action.func}
disabled={action.disabled}
>
{action.icon}
</IconButton>
</span>
</Tooltip>
))}
{groupIdx < actions.length - 1 && <Divider orientation="vertical" flexItem />}
</Stack>
))}
</Stack>
<Stack direction="row" alignItems="center" gap={0.5}>
<CustomColumnsButton />
<CustomFilterButton
columns={columns}
filterModel={filterModel}
setFilterModel={setFilterModel}
statusOptions={statusOptions}
/>
</Stack>
</GridToolbarContainer>
);
});
import * as React from 'react';
import { Grid, Typography, Divider, Box } from '@mui/material';
interface ListDetailItem {
label: React.ReactNode;
value: React.ReactNode;
}
interface ListDetailBuilderProps {
rows?: ListDetailItem[];
labelWidth?: number; // optional, default 4
spacingY?: number; // optional, default 2
}
export default function ListDetailBuilder({
rows = [],
labelWidth = 4,
spacingY = 2,
}: ListDetailBuilderProps) {
if (rows.length === 0) return null;
return (
<Box
sx={{
width: '100%',
bgcolor: 'background.paper',
borderRadius: 2,
overflow: 'hidden',
}}
>
{rows.map((row, index) => (
<React.Fragment key={index}>
<Grid container alignItems="flex-start" spacing={2} sx={{ px: 2, py: spacingY }}>
<Grid size={{ xs: 12, md: labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
fontWeight: 600,
color: 'text.secondary',
whiteSpace: 'pre-wrap',
}}
>
{row.label}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 12 - labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
color: 'text.primary',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{row.value ?? '-'}
</Typography>
</Grid>
</Grid>
{index < rows.length - 1 && <Divider sx={{ mx: 2 }} />}
</React.Fragment>
))}
</Box>
);
}
import React from 'react';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
import { HourglassTopRounded } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
type Props = {
value?: string;
revNo?: number;
valid?: string;
credit?: string | null;
};
const StatusChip: React.FC<Props> = ({ value, credit, valid }) => {
if (!value) return <Chip label="" size="small" />;
let mainComponent: React.ReactNode = <Chip label={value} size="small" />;
if (value === 'WAITING FOR AMENDMENT') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Normal Pengganti"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Tooltip title="Menunggu Persetujuan">
<IconButton
size="small"
sx={{
backgroundColor: 'orange',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'orange',
},
}}
>
<HourglassTopRounded style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
</Box>
);
} else if (value === 'APPROVED') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Normal"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Chip
label="Approved"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
</Box>
);
} else if (value === 'CANCELLED') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Batal"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Chip
label="Approved"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
</Box>
);
} else if (value === 'DRAFT') {
mainComponent = (
<Chip
label="Draft"
size="small"
variant="outlined"
sx={{
borderColor: '#9e9e9e',
color: '#616161',
borderRadius: '8px',
}}
/>
);
}
// ✅ Gabungkan komponen utama + tambahan
return (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
{mainComponent}
</Box>
);
};
export default React.memo(StatusChip);
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useCancelReturPM from '../../hooks/useCancelReturPM';
interface ModalDeleteReturPM {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalCancelReturPM: React.FC<ModalDeleteReturPM> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil di Cancel',
onConfirmCancel,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useCancelReturPM({
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 handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(
ids.map(async (rowId) => {
const row = tableApiRef?.current?.getRow(rowId);
if (!row) {
throw new Error(`Data row dengan id ${rowId} tidak ditemukan`);
}
return mutateAsync({
id: String(row.id),
nomorFaktur: row.nomorfakturdiretur,
nomorRetur: row.nomorretur,
npwpPenjual: row.npwppenjual,
});
})
);
};
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 = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmCancel?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['retur', 'pm'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan melakukan pembatalan?"
description=""
actionTitle="Ya"
isOpen={isOpenDialogCancel}
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 ModalCancelReturPM;
import React, { useEffect, useState } from 'react';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent';
import ListDetailBuilder from '../ListDetailItem';
import StatusChip from '../StatusChip';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
interface ModalCetakReturPMProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
type Row = { label: string; value: React.ReactNode };
const ModalCetakReturPM: React.FC<ModalCetakReturPMProps> = ({ payload, isOpen, onClose }) => {
const [rows, setRows] = useState<Row[]>([]);
useEffect(() => {
if (!payload) return;
const formattedRows: Row[] = [
{ label: 'NPWP Penjual', value: payload.npwppenjual },
{ label: 'Nama Penjual', value: payload.kdObjPjk || '-' },
{ label: 'Nomor Retur', value: payload.nomorretur || '-' },
{
label: 'Nomor Faktur Pajak Direktur',
value: payload.nomorfakturdiretur,
},
{
label: 'Tanggal Retur',
value: payload.tanggalretur
? dayjs(payload.tanggalretur).local().format('DD/MM/YYYY')
: '-',
},
{
label: 'Masa Retur',
value: payload.masapajakretur,
},
{
label: 'Tahun Retur',
value: payload.tahunpajakretur,
//
},
{
label: 'Status',
value: <StatusChip value={payload.statusretur} />,
},
{
label: 'DPP',
value: `${Number(payload.nilaireturdpp || 0)}`,
},
{
label: 'DPP Lain',
value: `${Number(payload.nilaireturdpplain || 0)}`,
},
{
label: 'PPN',
value: `${Number(payload.nilaireturppn || 0)}`,
},
{
label: 'PPnBM',
value: `${Number(payload.nilaireturppnbm || 0)}`,
},
{
label: 'Tanggal Approval',
value: payload.tanggalapproval || '-',
},
{
label: 'Keterangan Tambahan',
value: payload.userId || '-',
},
{
label: 'Penandatangan',
value: payload.namapenandatangan || '-',
},
{
label: 'Referensi',
value: payload.userId || '-',
},
{
label: 'User Perekam',
value: payload.created_by || '-',
},
{
label: 'Tanggal Rekam',
value: payload.created_at || '-',
},
{
label: 'User Pengubah',
value: payload.updated_by || '-',
},
{
label: 'Tanggal Ubah',
value: payload.updated_at || '-',
},
];
setRows(formattedRows);
}, [payload]);
return (
<DialogUmum
maxWidth="lg"
isOpen={isOpen}
onClose={onClose}
title="Detail Retur Faktur Pajak Masukan"
>
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
<ListDetailBuilder rows={rows} />
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakReturPM;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import type { TValidateFakturPMRequest } from '../../types/types';
import useConfirmationWaitingFakturPM from 'src/sections/faktur/fakturPm/hooks/useConfirmationWaitingFakturPM';
// type ConfirmationVariant = 'CANCEL' | 'PENGGANTI';
interface ModalConfirmationWaitingFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
variant: 'CANCEL' | 'PENGGANTI';
}
/** normalize berbagai bentuk selection menjadi array id */
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') {
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// kalau berbentuk map/object {'1': true, '2': true}
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalConfirmationWaitingFaktur: React.FC<ModalConfirmationWaitingFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Proses berhasil',
onConfirmCancel,
variant,
}) => {
const queryClient = useQueryClient();
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// gunakan hook mutation; pemanggil modal bisa inject onSuccess/onError jika ingin
const { mutateAsync } = useConfirmationWaitingFakturPM({
onSuccess: () => {
processSuccess();
},
onError: () => {
processFail();
},
});
const MODAL_CONFIG = {
CANCEL: {
title: 'Apakah Anda yakin akan melakukan pembatalan?',
description: '',
actionTitle: 'Batalkan',
successMessage: 'Faktur berhasil dibatalkan',
},
PENGGANTI: {
title: 'Apakah Anda yakin akan melakukan pengganti?',
description: '',
actionTitle: 'Konfirmasi',
successMessage: 'Faktur berhasil dikonfirmasi',
},
} as const;
const cfg = MODAL_CONFIG[variant];
const normalizeNomorFaktur = (val?: string | null) => {
if (!val) return 'INV#';
return val.startsWith('INV#') ? val : `INV#${val}`;
};
const doRequests = async (ids: (string | number)[]) =>
Promise.allSettled(
ids.map(async (id) => {
let nomorFaktur = 'INV#';
// ambil row dulu
try {
const row = tableApiRef?.current?.getRow?.(id);
if (row?.nomorfaktur) {
nomorFaktur = normalizeNomorFaktur(String(row.nomorfaktur));
}
} catch {
// ignore
}
const payload: TValidateFakturPMRequest = {
id: Number(id),
nomorFaktur,
};
return mutateAsync(payload);
})
);
const clearSelection = () => {
// clear grid selection via apiRef if available
try {
tableApiRef?.current?.setRowSelectionModel?.([]);
} catch {
// ignore if API differs
}
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const onSubmit = async () => {
try {
const ids = normalizeSelection(dataSelected);
if (!ids.length) {
enqueueSnackbar('Tidak ada data terpilih', { variant: 'warning' });
return;
}
setNumberOfData(ids.length);
setIsOpenDialogProgressBar(true);
const settled = await doRequests(ids);
// count results
const successCount = settled.filter((r) => r.status === 'fulfilled').length;
const failCount = settled.length - successCount;
// show feedback
if (successCount > 0) {
enqueueSnackbar(successMessage, { variant: 'success' });
}
if (failCount > 0) {
enqueueSnackbar(`${failCount} item gagal diproses`, { variant: 'error' });
}
await onConfirmCancel?.();
// close + reset
handleCloseModal();
clearSelection();
// invalidasi queries (sesuaikan queryKey dengan kebutuhan)
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
} catch (err: any) {
enqueueSnackbar(err?.message || 'Proses gagal', { variant: 'error' });
} finally {
setIsOpenDialogProgressBar(false);
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="sm"
title={cfg.title}
description=""
actionTitle="Ya"
isOpen={isOpenDialogCancel}
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 ModalConfirmationWaitingFaktur;
......@@ -5,16 +5,16 @@ 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 useCancelFakturPk from '../../hooks/useCancelFakturPK';
import useDeleteReturPM from '../../hooks/useDeleteReturPM';
interface ModalCancelFakturProps {
interface ModalDeleteReturPM {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
onConfirmDelete?: () => Promise<void> | void;
}
/**
......@@ -45,14 +45,14 @@ const normalizeSelection = (sel?: any): (string | number)[] => {
return [];
};
const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
const ModalDeleteReturPM: React.FC<ModalDeleteReturPM> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan',
onConfirmCancel,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => {
const queryClient = useQueryClient();
......@@ -72,7 +72,7 @@ const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useCancelFakturPk({
const { mutateAsync } = useDeleteReturPM({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
......@@ -88,12 +88,11 @@ const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
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);
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
setIsOpenDialogDelete(false);
resetToDefault();
};
......@@ -103,31 +102,31 @@ const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmCancel?.();
await onConfirmDelete?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal membantalkan data', { variant: 'error' });
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['faktur', 'pk'] });
queryClient.invalidateQueries({ queryKey: ['retur', 'pm'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin ingin melakukan pembatalan?"
description=""
actionTitle="Iya"
isOpen={isOpenDialogCancel}
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}
......@@ -149,4 +148,4 @@ const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
);
};
export default ModalCancelFakturPM;
export default ModalDeleteReturPM;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import { Field } from 'src/components/hook-form';
import MenuItem from '@mui/material/MenuItem';
import { useSelector } from 'react-redux';
import type { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form';
import Button from '@mui/material/Button';
import useUploadReturPM from '../../hooks/useUploadReturPM';
interface ModalUploadReturPMProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
* 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 ModalUploadReturPM: React.FC<ModalUploadReturPMProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
const UploadReturPM = useUploadReturPM();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useSelector((state: RootState) => state.user.data.signer);
const { mutateAsync } = useUploadReturPM({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
await onConfirmUpload?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogUpload, dataSelected, setNumberOfData]);
return (
<>
<FormProvider {...methods}>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleCloseModal}
title="Upload Bukti Potong"
>
<Stack spacing={2} sx={{ mt: 2 }}>
<Grid size={{ md: 12 }}>
<Field.Select name="signer" label="NPWP/NIK Penandatangan">
<MenuItem value={signer}>{signer}</MenuItem>
</Field.Select>
</Grid>
<Grid size={12}>
<Agreement
isCheckedAgreement={isCheckedAgreement}
setIsCheckedAgreement={setIsCheckedAgreement}
text="Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah saya isi dengan benar secara elektronik sesuai dengan"
/>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<Button
type="button"
disabled={!isCheckedAgreement}
onClick={onSubmit}
loading={UploadReturPM.isPending}
variant="contained"
sx={{ background: '#143B88' }}
>
Save
</Button>
</Stack>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalUploadReturPM;
import Grid from '@mui/material/Grid';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
type InfoReturProps = {
minTanggalRetur?: Dayjs;
};
const InfoRetur = ({ minTanggalRetur }: InfoReturProps) => {
const { control, setValue } = useFormContext();
const tanggalRetur = useWatch({
control,
name: 'tanggalRetur',
});
useEffect(() => {
if (!tanggalRetur) return;
const d = dayjs(tanggalRetur);
if (!d.isValid()) return;
// 🔹 Tahun Pajak
setValue('tahunPajakRetur', d.format('YYYY'));
// 🔹 Masa Pajak (bulan)
setValue('masaPajakRetur', d.format(), { shouldValidate: true });
}, [tanggalRetur, setValue]);
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid sx={{ mt: 3 }} size={{ md: 6 }}>
<Field.DatePicker
name="tanggalRetur"
label="Tanggal Retur"
slotProps={{ textField: { helperText: '' } }}
minDate={minTanggalRetur}
maxDate={dayjs()}
format="DD/MM/YYYY"
/>
</Grid>
<Grid sx={{ mt: 3 }} size={{ md: 3 }}>
<Field.DatePicker
name="tahunPajakRetur"
label="Tahun Pajak"
views={['year']}
format="YYYY"
disabled
/>
</Grid>
<Grid sx={{ mt: 3 }} size={{ md: 3 }}>
<Field.DatePicker
name="masaPajakRetur"
label="Masa Pajak"
views={['year', 'month']}
openTo="month"
format="MM"
disabled
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="nomorFakturDiretur" label="Nomor Faktur Direktur" disabled />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="npwpPembeli" label="NPWP Penjual" disabled />
</Grid>
</Grid>
);
};
export default InfoRetur;
import React from 'react';
import Grid from '@mui/material/Grid';
import Divider from '@mui/material/Divider';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
// ----------------------------------------------------------------------
const TotalTransaksi: React.FC = () => (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12 }} sx={{ mt: 3 }}>
<Divider sx={{ fontWeight: 'bold', fontSize: '1rem', mb: 2 }} textAlign="left">
Total Transaksi
</Divider>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalJumlahBarang"
label="Total Jumlah Barang"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalHarga"
label="Total Harga (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalDiskon"
label="Total Diskon (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalDpp"
label="Jumlah DPP (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalDppLain"
label="Jumlah DPP Lain (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalPpn"
label="Jumlah PPN (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalPpnbm"
label="Jumlah PPnBM (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
</Grid>
);
export default TotalTransaksi;
import React from 'react';
import Grid from '@mui/material/Grid';
import Divider from '@mui/material/Divider';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
import Typography from '@mui/material/Typography';
// ----------------------------------------------------------------------
type Props = {
data?: any | null;
type?: string;
isLoading?: boolean;
};
const TotalTransaksiRetur: React.FC<Props> = ({ data = null, isLoading = false, type }) => {
const formatRupiah = (v: number | null | undefined) =>
v === null || v === undefined ? '' : new Intl.NumberFormat('id-ID').format(Math.round(v));
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12 }} sx={{ mt: 3 }}>
<Divider sx={{ fontWeight: 'bold', fontSize: '1rem', mb: 2 }} textAlign="left">
Total Transaksi Retur
</Divider>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="nilaiReturJmlBrg"
label="Total Retur Jumlah Barang"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="nilaiRetur"
label="Total Harga Retur"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="nilaiReturDiskon"
label="Total Retur Diskon"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="nilaiReturDpp"
label="Total Retur DPP (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Max Total DPP:{' '}
{type === 'retur' ? formatRupiah(data.maxTotalDpp) : formatRupiah(data.returMaxDpp)}
</Typography>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="nilaiReturDppLain"
label="Total Retur DPP Nilai Lain (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Max Total DPP Nilai Lain:{' '}
{type === 'retur'
? formatRupiah(data.maxTotalDppLain)
: formatRupiah(data.returMaxDppLain)}
</Typography>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="nilaiReturPpn"
label="Total Retur PPN (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Max Total PPN:{' '}
{type === 'retur' ? formatRupiah(data.maxTotalPpn) : formatRupiah(data.returMaxPpn)}
</Typography>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="nilaireturppnbm"
label="Total Retur PPnBM (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Max Total PPnBM:{' '}
{type === 'retur' ? formatRupiah(data.maxTotalPpnbm) : formatRupiah(data.returMaxPpnbm)}
</Typography>
</Grid>
</Grid>
);
};
export default TotalTransaksiRetur;
export const Steps = [
{
icon: '1',
label: 'Dokumen Transaksi',
},
{
icon: '2',
label: 'Lawan Transaksi',
},
{
icon: '3',
label: 'Detail Transaksi',
},
];
export const Identitas = {
NPWP: '4',
NIK: '1',
PASSPORT: '2',
ID_LAIN: '3',
};
export const TransaksiRekamAction = {
CREATE: 'create',
UPDATE: 'update',
};
export const identitasOptions = [
{
text: 'NPWP',
value: Identitas.NPWP,
},
{
text: 'NIK',
value: Identitas.NIK,
},
{
text: 'Passport',
value: Identitas.PASSPORT,
},
{
text: 'ID Lain',
value: Identitas.ID_LAIN,
},
];
export const jenisTransaksiOptions = [
{
label: '01 : Kepada pihak lain selain Pemungut PPN',
id: 'TD.00301',
},
{
label: '02 : kepada Pemungut PPN Instansi Pemerintah',
id: 'TD.00302',
},
{
label: '03 : kepada Pemungut PPN selain Instansi Pemerintah',
id: 'TD.00303',
},
{
label: '04 : penyerahan yang menggunakan Dasar Pengenaan Pajak (DPP) Nilai Lain',
id: 'TD.00304',
},
{
label: '05 : penyerahan yang PPN-nya dipungut dengan besaran tertentu',
id: 'TD.00305',
},
{
label: '06 : kepada pemegang paspor luar negeri dalam rangka VAT Refund for Tourist',
id: 'TD.00306',
},
{
label:
'07 : penyerahan yang mendapat fasilitas PPN dan/atau PPnBM Tidak Dipungut atau Ditanggung Pemerintah',
id: 'TD.00307',
},
{
label: '08 : penyerahan yang mendapat fasilitas dibebaskan dari pengenaan PPN dan/atau PPnBM',
id: 'TD.00308',
},
{
label: '09 : penyerahan aktiva yang menurut tujuan semula tidak untuk diperjualbelikan',
id: 'TD.00309',
},
{
label: '10 : untuk penyerahan lainnya',
id: 'TD.00310',
},
];
export const JenisFaktur = (() => ({
FAKTUR_PAJAK: '0',
FAKTUR_PAJAK_PENGGANTI: '1',
}))();
export const JenisTransaksi = (() => ({
KEPADA_PIHAK_YANG_BUKAN_PEMUNGUT: 'TD.00301',
KEPADA_PEMUNGUT_BENDAHARAWAN: 'TD.00302',
KEPADA_PEMUNGUT_SELAIN_BEDAHARAWAN: 'TD.00303',
DPP_NILAI_LAIN: 'TD.00304',
BESARAN_TERTAENTU: 'TD.00305',
PENYERAHAN_LAINNYA: 'TD.00306',
PENYERAHAN_YANG_PPN_TIDAK_DIPUNGUT: 'TD.00307',
PENYERAHAN_YANG_PPN_DIPUNGUT: 'TD.00308',
AKTIVA: 'TD.00309',
TARIF_NORMAL: 'TD.00310',
}))();
export const jenisFakturOptions = [
{
label: '0 : Faktur Pajak',
id: '0',
},
{
label: '1 : Faktur Pajak Pengganti',
id: '1',
},
];
export const FgUangMukaInRadioInput = (() => ({
NORMAL: 0,
UANG_MUKA: 1,
PELUNASAN: 2,
}))();
export const FgPengganti = (() => ({
NORMAL: '0',
PENGGANTI: '1',
}))();
export const PkRekamActionUbah = (() => ({
NORMAL: 'normal',
PENGGANTI: 'pengganti',
}))();
export const PkRekamAction = (() => ({
REKAM: 'new',
UBAH: 'ubah',
PENGGANTI: 'pengganti',
}))();
export const UnitBarangOptions = [
{
label: 'g',
id: '01',
},
{
label: 'Kg',
id: '02',
},
{
label: 'm',
id: '03',
},
{
label: 'Km',
id: '04',
},
{
label: 'Ons',
id: '05',
},
];
export const FG_STATUS_DN = {
DRAFT: 'DRAFT',
NORMAL_DONE: 'NORMAL-Done',
AMENDED: 'AMENDED',
CANCELLED: 'CANCELLED',
};
export const FG_STATUS_FAKTUR_PK = {
DRAFT: 'DRAFT',
AMENDED: 'AMENDED',
CANCELLED: 'CANCELLED',
APPROVED: 'APPROVED',
WAITING_FOR_AMENDMENT: 'WAITING FOR AMENDMENT',
WAITING_FOR_CANCELLATION: 'WAITING FOR CANCELLATION',
};
export const FG_FASILITAS_DN = {
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 PANDUAN_REKAM_DIGUNGGUNG = {
description: {
intro:
'Form ini digunakan untuk melakukan perekaman/perubahan data Bukti Setor atas PPh yang disetor sendiri.\n',
textList: '',
list: [],
closing: 'Berikut ini petunjuk pengisian:',
},
sections: [
{
title: '',
items: [
{
text: 'Jenis Bukti Penyetoran, tentukan dokumen jenis bukti penyetoran.',
subItems: [],
},
{
text: 'Nomor Bukti Penyetoran/Pemindahbukuan, silakan rekam nomor bukti penyetoran/pemindahbukuan.',
subItems: [],
},
{
text: 'Tahun dan masa Pajak, jika Anda merekam Bukti Penyetoran, tentukan tahun dan masa pajak saat.',
subItems: [],
},
{
text: 'Kode Objek Pajak, pilihlah Kode Objek Pajak dari pilihan yang tersedia, Anda dapat mengetikan kata kunci untuk mempercepat pencarian objek pajak.',
subItems: [],
},
{
text: 'Tanggal Setor, silakan rekam tanggal penyetoran.',
subItems: [],
},
{
text: 'Isikan nilai nominal Penghasilan Bruto dan Jumlah Setor pada kotak yang tersedia.',
subItems: [],
},
{
text: 'Pastikan isian Anda telah lengkap dan benar, klik tombol simpan untuk menyimpan data.',
subItems: [],
},
],
},
],
};
export const JENIS_INVOICE = {
UANG_MUKA: 'uang-muka',
PELUNASAN: 'pelunasan',
FULL_PAYMENT: 'full-payment',
};
export const JENIS_INVOICE_TEXT = {
[JENIS_INVOICE.UANG_MUKA]: 'Uang Muka',
[JENIS_INVOICE.PELUNASAN]: 'Pelunasan',
[JENIS_INVOICE.FULL_PAYMENT]: 'Full Payment',
};
export const MIN_THN_PAJAK = 2022;
const appRootKey = 'unifikasi';
const queryKey = {
faktuPM: {
all: (params: any) => [appRootKey, 'fakturPk', params],
detail: (params: any) => [appRootKey, 'fakturPm', 'detail', params],
draft: [appRootKey, 'fakturPk', 'draft'],
delete: [appRootKey, 'fakturPk', 'delete'],
upload: [appRootKey, 'fakturPk', 'upload'],
cancel: [appRootKey, 'fakturPk', 'cancel'],
},
};
export default queryKey;
......@@ -24,6 +24,13 @@ export function useAdvancedFilter() {
'ppnbm',
'jumlahuangmuka',
'jumlahUangMuka',
'nilaireturdpp',
'nilaireturdpplain',
'nilaireturdiskon',
'nilaireturppn',
'nilaireturppnbm',
'tahunpajakretur',
'masapajakretur',
]);
// date fields (columns that expect date operators)
......@@ -33,6 +40,7 @@ export function useAdvancedFilter() {
'created_at',
'updated_at',
'tanggal_approval',
'tanggalretur',
]);
// fields that should be treated as "status" (use LIKE semantics)
......
import { useMutation } from '@tanstack/react-query';
import type { TCancelReturPMRequest, TCancelResponse } from '../types/types';
import fakturApi from '../utils/api';
const useCancelReturPM = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelReturPMRequest>({
mutationKey: ['cancel-retur-pm'],
mutationFn: (payload) => fakturApi.cancelReturPM(payload),
...props,
});
export default useCancelReturPM;
import { useMutation } from '@tanstack/react-query';
import type { TDeleteRequest } from '../types/types';
import fakturApi from '../utils/api';
const useDeleteReturPM = (props?: any) =>
useMutation<any, Error, TDeleteRequest>({
mutationKey: ['delete-retur-pm'],
mutationFn: (payload) => fakturApi.deleteReturPM(payload),
...props,
});
export default useDeleteReturPM;
import { useQuery } from '@tanstack/react-query';
import type { TGoodsResult } from '../types/types';
import fakturApi from '../utils/api';
export const useGetGoods = (params?: Record<string, any>) =>
useQuery<TGoodsResult>({
queryKey: ['goods-faktur-pk'],
queryFn: async () => {
const res = await fakturApi.getGoods(params);
return res.data; // ✅ langsung array negara
},
});
export default useGetGoods;
import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query';
import queryKey from '../constant/queryKey';
import type { TableReturFakturPMResult } from '../types/types';
import fakturApi from '../utils/api';
export type TGetFakturPMApiWrapped = {
data: TableReturFakturPMResult[];
total: number;
pageSize: number;
page: number; // 1-based
};
/**
* Format tanggal ke format dd/mm/yyyy
*/
export const formatDateToDDMMYYYY = (dateString: string | null | undefined) => {
if (!dateString) return '';
const [year, month, day] = dateString.split('T')[0].split('-');
return `${day}/${month}/${year}`;
};
/**
* Normalisasi params API — otomatis bedakan antara "list mode" dan "search mode"
*/
const normalizeParams = (params: any) => {
if (!params) return {};
const {
page,
pageSize,
sort,
filter,
advanced,
sortingMode: sortingModeParam,
sortingMethod: sortingMethodParam,
...rest
} = params;
// Deteksi apakah user sedang minta list (ada pagination/sorting/filter)
const isListRequest =
page !== undefined ||
pageSize !== undefined ||
sort !== undefined ||
filter !== undefined ||
advanced !== undefined;
// 🔸 Kalau bukan list (misal hanya nomorFaktur), langsung kembalikan tanpa modifikasi
if (!isListRequest) {
return rest;
}
// 🔹 Kalau list, pakai logika sorting & pagination seperti sebelumnya
let sortPayload: any;
let sortingMode = sortingModeParam || '';
let sortingMethod = sortingMethodParam || '';
if (sort) {
try {
const parsed = JSON.parse(sort);
if (Array.isArray(parsed) && parsed.length > 0) {
sortPayload = parsed;
sortingMode = parsed[0]?.field ?? sortingMode;
sortingMethod = parsed[0]?.sort ?? sortingMethod;
}
} catch {
sortPayload = [];
}
}
return {
page: (page ?? 0) + 1,
limit: pageSize ?? 10,
advanced:
typeof advanced === 'string' && advanced.trim() !== ''
? advanced
: filter && !isEmpty(JSON.parse(filter))
? filter
: undefined,
...(sortPayload ? { sort: sortPayload } : {}),
sortingMode,
sortingMethod,
...rest,
};
};
/**
* Hook utama untuk GET faktur PK
* Bisa digunakan untuk:
* - List data (dengan pagination, sorting, dsb.)
* - Search by nomorFaktur (atau param lain tanpa pagination)
*/
export const useGetReturFakturPM = ({ params }: { params: any }) => {
const normalized = normalizeParams(params);
const { page, limit, advanced, sortingMode, sortingMethod, ...rest } = normalized;
const isListRequest =
page !== undefined ||
limit !== undefined ||
advanced !== undefined ||
sortingMode ||
sortingMethod;
return useQuery<TGetFakturPMApiWrapped>({
queryKey: isListRequest
? ['faktur-pk', page, limit, advanced, sortingMode, sortingMethod]
: ['faktur-pk', rest], // supaya cache beda antara list dan search tunggal
queryFn: async () => {
const res: any = await fakturApi.getReturPM({ params: normalized });
const rawData: any[] = Array.isArray(res?.data) ? res.data : res?.data ? [res.data] : [];
const total = Number(res?.total ?? res?.totalRow ?? 0);
return {
data: rawData as TableReturFakturPMResult[],
total,
pageSize: normalized.limit ?? 0,
page: normalized.page ?? 1,
};
},
placeholderData: (prev: any) => prev,
refetchOnWindowFocus: false,
refetchOnMount: false,
staleTime: 0,
gcTime: 0,
retry: false,
});
};
/**
* Hook untuk get detail faktur PK by ID
*/
export const useGetFakturPMById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.faktuPM.detail(id),
queryFn: async () => {
const res = await fakturApi.getFakturPMById(id);
if (!res) throw new Error('Data tidak ditemukan');
return res;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export const useGetReturPMById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.faktuPM.detail(id),
queryFn: async () => {
const res = await fakturApi.getReturPMById(id);
if (!res) throw new Error('Data tidak ditemukan');
return res;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export default useGetReturFakturPM;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
import type { TPostReturPMRequest, TBaseResponseAPI } from '../types/types';
type TPostReturPMResponse = TBaseResponseAPI<{
id: string;
nomorRetur: string;
}>;
const usePostReturPM = (props?: any) =>
useMutation<TPostReturPMResponse, Error, TPostReturPMRequest>({
mutationKey: ['create-retur-pm'],
mutationFn: (payload) => fakturApi.postReturPM(payload),
...props,
});
export default usePostReturPM;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
import type { TUploadReturPMRequest, TUploadReturPMResponse } from '../types/types';
const useUploadReturPM = (props?: any) =>
useMutation<TUploadReturPMResponse, Error, TUploadReturPMRequest>({
mutationKey: ['upload-retur-pm'],
mutationFn: (payload) => fakturApi.uploadReturPM(payload),
...props,
});
export default useUploadReturPM;
import type { TReturPmForm } from '../types/types';
const mapObjekFakturToRetur = (item: any) => ({
brgJasa: item.brgJasa,
kdBrgJasa: item.kdBrgJasa,
namaBrgJasa: item.namaBrgJasa,
satuanBrgJasa: item.satuanBrgJasa,
hargaSatuan: Number(item.hargaSatuan),
jmlBrgJasa: Number(item.jmlBrgJasa),
returJmlBrgJasa: Number(item.jmlBrgJasa), // default full retur
totalHarga: Number(item.totalHarga),
diskon: Number(item.diskon ?? 0),
returDiskon: 0,
cekDppLain: item.cekDppLain ? 1 : 0,
dpp: Number(item.dpp),
returDpp: Number(item.dpp ?? 0),
dppLain: Number(item.dppLain),
returDppLain: Number(item.dppLain ?? 0),
tarifPpn: Number(item.tarifPpn),
ppn: Number(item.ppn),
returPpn: Number(item.ppn ?? 0),
tarifPpnbm: Number(item.tarifPpnbm ?? 0),
ppnbm: Number(item.ppnbm ?? 0),
returPpnbm: Number(item.ppnbm ?? 0),
});
export const mapTrx015ToReturForm = (res: any): TReturPmForm => ({
// header
masaPajakRetur: res.masapajak,
tahunPajakRetur: res.tahunpajak,
// API -> Form
tanggalRetur: new Date(),
nomorFakturDiretur: res.nomorfaktur,
npwpPembeli: res.npwppembeli,
npwpPenjual: res.npwppenjual,
isCreditable: 1,
// total (WAJIB ADA)
nilaiReturDpp: Number(res.totaldpp ?? 0),
nilaiReturDppLain: Number(res.totaldpplain ?? 0),
nilaiReturDiskon: 0,
nilaiReturPpn: Number(res.totalppn ?? 0),
nilaireturppnbm: Number(res.totalppnbm ?? 0),
// detail
objekFaktur: res.objekFaktur.map(mapObjekFakturToRetur),
});
import type { TReturPmForm, TReturPmObjekFaktur } from '../types/types';
const mapObjekFakturTrx017ToRetur = (item: any): TReturPmObjekFaktur => ({
brgJasa: item.brgJasa,
kdBrgJasa: item.kdBrgJasa,
namaBrgJasa: item.namaBrgJasa,
satuanBrgJasa: item.satuanBrgJasa,
hargaSatuan: Number(item.hargaSatuan),
jmlBrgJasa: Number(item.jmlBrgJasa),
returJmlBrgJasa: Number(item.returJmlBrgJasa),
totalHarga: Number(item.totalHarga),
diskon: Number(item.diskon ?? 0),
returDiskon: Number(item.returDiskon ?? 0),
cekDppLain: Number(item.cekDppLain ?? 0),
dpp: Number(item.dpp ?? 0),
returDpp: Number(item.returDpp ?? 0),
dppLain: Number(item.dppLain ?? 0),
returDppLain: Number(item.returDppLain ?? 0),
tarifPpn: Number(item.tarifPpn ?? 0),
ppn: Number(item.ppn ?? 0),
returPpn: Number(item.returPpn ?? 0),
tarifPpnbm: Number(item.tarifPpnbm ?? 0),
ppnbm: Number(item.ppnbm ?? 0),
returPpnbm: Number(item.returPpnbm ?? 0),
});
export const mapTrx017ToReturForm = (res: any): TReturPmForm => ({
// header
masaPajakRetur: res.masapajakretur,
tahunPajakRetur: res.tahunpajakretur,
tanggalRetur: new Date(),
nomorFakturDiretur: res.nomorfakturdiretur,
npwpPembeli: res.npwppembeli,
npwpPenjual: res.npwppenjual,
isCreditable: Number(res.iscreditable) as 0 | 1,
// total
nilaiReturDpp: Number(res.nilaireturdpp ?? 0),
nilaiReturDppLain: Number(res.nilaireturdpplain ?? 0),
nilaiReturDiskon: Number(res.nilaireturdiskon ?? 0),
nilaiReturPpn: Number(res.nilaireturppn ?? 0),
nilaireturppnbm: Number(res.nilaireturppnbm ?? 0),
// detail
objekFaktur: Array.isArray(res.objekFaktur)
? res.objekFaktur.map(mapObjekFakturTrx017ToRetur)
: [],
});
import { z } from 'zod';
export const BarangJasaSchema = z
.object({
// ======================
// EXISTING (TIDAK DIUBAH)
// ======================
type: z.string().nonempty('Type wajib diisi'),
kdBrgJasa: z.string().nonempty('Kode Barang Jasa wajib diisi'),
namaBrgJasa: z.string().trim().nonempty('Nama Barang/Jasa wajib diisi'),
satuanBrgJasa: z.string().nonempty('Satuan Barang Jasa wajib diisi'),
jmlBrgJasa: z
.string()
.trim()
.nonempty('Jumlah Barang wajib diisi')
.refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
message: 'Jumlah Barang wajib diisi dengan angka lebih dari 0',
}),
hargaSatuan: z.string().trim().nonempty('Harga Satuan wajib diisi'),
totalHarga: z.string().trim().nonempty('Harga Total wajib diisi'),
diskon: z
.string()
.trim()
.nonempty('Diskon wajib diisi')
.refine((val) => !isNaN(Number(val)) && Number(val) >= 0, {
message: 'Diskon wajib diisi dengan angka valid',
}),
fgPmk: z.string().optional(),
dpp: z.string().nonempty('DPP wajib diisi'),
tarifPpn: z.string().nonempty('Tarif PPN wajib diisi'),
dppLain: z
.string()
.min(1, 'DPP Nilai Lain wajib diisi')
.refine((val) => !isNaN(Number(val)), { message: 'DPP Nilai Lain harus berupa angka' }),
ppn: z
.string()
.min(1, 'PPN wajib diisi')
.refine((val) => !isNaN(Number(val)), { message: 'PPN harus berupa angka' }),
ppnbm: z
.string()
.min(1, 'PPnBM wajib diisi')
.refine((val) => !isNaN(Number(val)), { message: 'PPnBM harus berupa angka' }),
tarifPpnbm: z
.string()
.min(1, 'Tarif PPnBM wajib diisi')
.refine((val) => !isNaN(Number(val)), { message: 'Tarif PPnBM harus berupa angka' }),
// ======================
// ➕ FIELD RETUR (BARU)
// ======================
returJmlBrgJasa: z
.string()
.optional()
.refine((v) => !v || (!isNaN(Number(v)) && Number(v) >= 0), {
message: 'Retur jumlah harus berupa angka valid',
}),
totalHargaRetur: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Total harga retur harus berupa angka',
}),
returDiskon: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Retur diskon harus berupa angka',
}),
returDpp: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Retur DPP harus berupa angka',
}),
returDppLain: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Retur DPP Nilai Lain harus berupa angka',
}),
returPpn: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Retur PPN harus berupa angka',
}),
returPpnbm: z
.string()
.optional()
.refine((v) => !v || !isNaN(Number(v)), {
message: 'Retur PPnBM harus berupa angka',
}),
})
// ======================
// EXISTING REFINE (TIDAK DIUBAH)
// ======================
.refine(
(data) => {
const hargaTotal = Number(data.totalHarga);
const diskon = Number(data.diskon);
if (isNaN(hargaTotal) || isNaN(diskon)) return true;
return diskon <= hargaTotal;
},
{
message: 'Diskon tidak boleh lebih dari Harga Total',
path: ['diskon'],
}
)
// ======================
// EXISTING + RETUR SUPER REFINE
// ======================
.superRefine((data, ctx) => {
const dpp = Number(data.dpp || 0);
const fieldsToCheck = [
{ key: 'dppLain', label: 'DPP Nilai Lain' },
{ key: 'ppn', label: 'PPN' },
{ key: 'ppnbm', label: 'PPnBM' },
{ key: 'tarifPpnbm', label: 'Tarif PPnBM' },
] as const;
fieldsToCheck.forEach(({ key, label }) => {
const val = Number(data[key] || 0);
if (!isNaN(val) && val > dpp) {
ctx.addIssue({
code: 'custom',
path: [key],
message: `${label} tidak boleh lebih dari jumlah DPP`,
});
}
});
// ======================
// ➕ VALIDASI RETUR
// ======================
const jml = Number(data.jmlBrgJasa || 0);
const retur = Number(data.returJmlBrgJasa || 0);
if (retur > jml) {
ctx.addIssue({
code: 'custom',
path: ['returJmlBrgJasa'],
message: 'Jumlah retur tidak boleh lebih besar dari jumlah barang',
});
}
const totalRetur = Number(data.totalHargaRetur || 0);
const returDiskon = Number(data.returDiskon || 0);
if (returDiskon > totalRetur) {
ctx.addIssue({
code: 'custom',
path: ['returDiskon'],
message: 'Diskon retur tidak boleh lebih besar dari total harga retur',
});
}
});
export type TBarangJasaSchema = z.infer<typeof BarangJasaSchema>;
import dayjs from 'dayjs';
import { z } from 'zod';
export const ReturPmSchema = z.object({
// masaPajakRetur: z.string().min(1),
// tahunPajakRetur: z.string(),
// tanggalRetur: z.string().length(8),
// tanggalRetur: z.preprocess((val) => {
// if (val instanceof Date) {
// return dayjs(val).format('DDMMYYYY');
// }
// return val;
// }, z.string().nonempty('Tanggal retur wajib diisi')),
tahunPajakRetur: z.preprocess((val) => {
if (val instanceof Date) return dayjs(val).format('YYYY');
// jika kadang value Dayjs (unlikely) - ikut dayjs parsing
return val;
}, z.string().nonempty('Tahun Pajak wajib diisi')),
masaPajakRetur: z.preprocess((val) => {
if (val instanceof Date) return dayjs(val).format('MM');
return val;
}, z.string().nullable()),
tanggalRetur: z.preprocess((val) => {
if (val instanceof Date) return dayjs(val).format('DDMMYYYY');
return val;
}, z.string().nonempty('Tanggal retur wajib diisi')),
nomorFakturDiretur: z.string().min(1),
npwpPembeli: z.string().length(16),
npwpPenjual: z.string().length(16),
isCreditable: z.union([z.literal(0), z.literal(1)]),
nilaiReturDpp: z.number(),
nilaiReturDppLain: z.number(),
nilaiReturDiskon: z.number(),
nilaiReturPpn: z.number(),
nilaireturppnbm: z.number(),
objekFaktur: z.array(
z.object({
brgJasa: z.enum(['GOODS', 'SERVICES']),
kdBrgJasa: z.string(),
namaBrgJasa: z.string(),
satuanBrgJasa: z.string(),
hargaSatuan: z.number(),
jmlBrgJasa: z.number(),
returJmlBrgJasa: z.number(),
totalHarga: z.number(),
diskon: z.number(),
returDiskon: z.number(),
cekDppLain: z.number(),
dpp: z.number(),
returDpp: z.number(),
dppLain: z.number(),
returDppLain: z.number(),
tarifPpn: z.number(),
ppn: z.number(),
returPpn: z.number(),
tarifPpnbm: z.number(),
ppnbm: z.number(),
returPpnbm: z.number(),
})
),
});
import { create } from 'zustand';
console.log('✅ pagination store created');
type TableKey = string;
interface TablePagination {
page: number;
pageSize: number;
}
interface TableFilter {
items: any[];
}
interface PaginationState {
tables: Record<TableKey, TablePagination>;
filters: Record<TableKey, TableFilter>;
setPagination: (table: TableKey, next: Partial<TablePagination>) => void;
resetPagination: (table: TableKey) => void;
setFilter: (table: TableKey, next: Partial<TableFilter>) => void;
resetFilter: (table: TableKey) => void;
}
export const usePaginationStore = create<PaginationState>((set) => ({
tables: {},
filters: {},
setPagination: (table, next) =>
set((state) => {
const prev = state.tables[table] ?? { page: 0, pageSize: 10 };
return {
tables: {
...state.tables,
[table]: {
page: next.page ?? prev.page,
pageSize: next.pageSize ?? prev.pageSize,
},
},
};
}),
resetPagination: (table) =>
set((state) => ({
tables: {
...state.tables,
[table]: { page: 0, pageSize: state.tables[table]?.pageSize ?? 10 },
},
})),
setFilter: (table, next) =>
set((state) => ({
filters: {
...state.filters,
[table]: {
items: next.items ?? state.filters[table]?.items ?? [],
},
},
})),
resetFilter: (table) =>
set((state) => ({
filters: {
...state.filters,
[table]: { items: [] },
},
})),
}));
This diff is collapsed.
This diff is collapsed.
const formatDate = (iso: string) => {
if (!iso) return '';
const d = new Date(iso);
const dd = String(d.getUTCDate()).padStart(2, '0');
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
const yyyy = d.getUTCFullYear();
return `${dd}${mm}${yyyy}`;
};
export const normalizePayloadCetakPdfDetail = (raw: any) => {
// 🧮 Hitung total diskon dari objekFaktur
const totalDiskon = Array.isArray(raw.objekFaktur)
? raw.objekFaktur.reduce((sum: any, x: any) => sum + Number(x.diskon ?? 0), 0)
: 0;
return {
alamatPembeli: raw.alamatpembeli || '',
alamatPenjual: raw.alamatpenjual || 'Jati Bening, Bekasi',
detailTransaksi: raw.detailtransaksi || '',
emailPembeli: raw.emailpembeli || '',
fgPelunasan: raw.fgpelunasan || false,
fgUangMuka: raw.fguangmuka || false,
idKeteranganTambahan: raw.idketerangantambahan || '',
idLainPembeli: raw.idlainpembeli || '',
jumlahUangMuka: String(raw.jumlahuangmuka || '0'),
kdNegaraPembeli: raw.kdnegarapembeli || '',
keteranganTambahan: raw.keterangantambahan || '',
kodeApproval: raw.kodeapproval || '',
masaPajak: raw.masapajak || '',
namaPembeli: raw.namapembeli || '',
namaPenandatangan: raw.namapenandatangan || raw.tkupembeli || '',
namaPenjual: raw.namatokopenjual || '',
nikPaspPembeli: raw.nikpasppembeli || '',
nomorFaktur: raw.nomorfaktur || '',
npwpPembeli: raw.npwppembeli || '',
npwpPenjual: raw.npwppenjual || '',
objekFaktur: (raw.objekFaktur || []).map((x: any) => ({
brgJasa: x.brgJasa,
kdBrgJasa: x.kdBrgJasa,
namaBrgJasa: x.namaBrgJasa,
satuanBrgJasa: x.satuanBrgJasa,
jmlBrgJasa: String(x.jmlBrgJasa),
hargaSatuan: String(x.hargaSatuan),
totalHarga: String(x.totalHarga),
diskon: String(x.diskon),
dpp: String(x.dpp),
dppLain: String(x.dppLain),
ppn: String(x.ppn),
ppnbm: String(x.ppnbm),
tarifPpn: String(x.tarifPpn),
tarifPpnbm: String(x.tarifPpnbm),
cekDppLain: String(x.cekDppLain),
})),
qrcode: raw.qrcode || 'urlttd',
refDoc: raw.refdoc || '',
referensi: raw.referensi || '',
statusFaktur: raw.statusfaktur || '',
tahunPajak: raw.tahunpajak || '',
tanggalFaktur: formatDate(raw.tanggalfaktur),
tempatPenandatangan: raw.tempatpenandatangan || 'BEKASI',
// <<-- hasil penjumlahan diskon
totalDiskon: String(totalDiskon),
totalDpp: String(raw.totaldpp || '0'),
totalDppLain: String(raw.totaldpplain || '0'),
totalPpn: String(raw.totalppn || '0'),
totalPpnbm: String(raw.totalppnbm || '0'),
};
};
import axios from 'axios';
const BASE_URL = `https://nodesandbox.pajakexpress.id:1837`;
const unifikasiClient = axios.create({
baseURL: BASE_URL,
validateStatus(status) {
return (status >= 200 && status < 300) || status === 500;
},
});
// Interceptor untuk selalu update token dari localStorage
unifikasiClient.interceptors.request.use((config) => {
const jwtAccessToken = localStorage.getItem('jwt_access_token');
const xToken = localStorage.getItem('x-token');
if (jwtAccessToken) {
config.headers.Authorization = `Bearer ${jwtAccessToken}`;
}
if (xToken) {
config.headers['x-token'] = xToken;
}
return config;
});
export default unifikasiClient;
This diff is collapsed.
export * from './retur-pm-list-view';
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