Commit 2e5d7b73 authored by Fachri's avatar Fachri

Dokumen Lain Masukan Feature

parent b2cc201a
import { CONFIG } from 'src/global-config';
import { DlkListView } from 'src/sections/faktur/dlk/view';
const metadata = { title: `Retur Faktur PM - ${CONFIG.appName}` };
const metadata = { title: `Dokumen Lain Keluaran - ${CONFIG.appName}` };
export default function Page() {
return (
......
import { CONFIG } from 'src/global-config';
import DokumenLainKeluaranRekamView from 'src/sections/faktur/dlk/view/dokumenLainKeluaranRekamView';
const metadata = { title: `Faktur - ${CONFIG.appName}` };
const metadata = { title: `Dokumen Lain Keluaran Rekam - ${CONFIG.appName}` };
export default function Page() {
return (
......
import { CONFIG } from 'src/global-config';
import { DlmListView } from 'src/sections/faktur/dlm/view';
const metadata = { title: `Dokumen Lain Masukan - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<DlmListView />
</>
);
}
import { CONFIG } from 'src/global-config';
import DokumenLainMasukanRekamView from 'src/sections/faktur/dlm/view/dokumenLainMasukanRekamView';
const metadata = { title: `Dokumen Lain Masukan Rekam - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<DokumenLainMasukanRekamView />
</>
);
}
......@@ -116,15 +116,15 @@ export const paths = {
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}`,
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}`,
bupotTahunanEdit: (id: string, path: string) => `${ROOTS.PPH21}/tahunan/${id}/${path}`,
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}`,
bupot26Edit: (id: string, path: string) => `${ROOTS.PPH21}/bupot-26/${id}/${path}`,
},
faktur: {
root: ROOTS.FAKTUR,
......@@ -139,6 +139,7 @@ export const paths = {
returDlk: `${ROOTS.FAKTUR}/dokumen-lain/retur-pajak-keluaran`,
returDlm: `${ROOTS.FAKTUR}/dokumen-lain/retur-pajak-masukan`,
rekamDlk: `${ROOTS.FAKTUR}/dokumen-lain/pajak-keluaran/new`,
rekamDlm: `${ROOTS.FAKTUR}/dokumen-lain/pajak-masukan/new`,
},
// DASHBOARD
dashboard: {
......
......@@ -58,6 +58,9 @@ const OverviewReturFakturPkPage = lazy(() => import('src/pages/faktur/returPk'))
const OverviewDokumenLainKeluaranPage = lazy(() => import('src/pages/faktur/dlk'));
const OverviewDokumenLainKeluaranRekamPage = lazy(() => import('src/pages/faktur/dlkRekam'));
const OverviewDokumenLainMasukanPage = lazy(() => import('src/pages/faktur/dlm'));
const OverviewDokumenLainMasukanRekamPage = lazy(() => import('src/pages/faktur/dlmRekam'));
// Overview
const IndexPage = lazy(() => import('src/pages/dashboard'));
......@@ -200,6 +203,15 @@ export const dashboardRoutes: RouteObject[] = [
path: 'dokumen-lain/pajak-keluaran/new',
element: <OverviewDokumenLainKeluaranRekamPage />,
},
{ path: 'dokumen-lain/pajak-masukan', element: <OverviewDokumenLainMasukanPage /> },
{
path: 'dokumen-lain/pajak-masukan/:id/:type',
element: <OverviewDokumenLainMasukanRekamPage />,
},
{
path: 'dokumen-lain/pajak-masukan/new',
element: <OverviewDokumenLainMasukanRekamPage />,
},
],
},
];
......@@ -9,9 +9,10 @@ type Props = {
revNo?: number;
fgpelunasan?: string | boolean;
fguangmuka?: string | boolean;
fgpengganti?: string;
};
const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka }) => {
const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka, fgpengganti }) => {
if (!value) return <Chip label="" size="small" />;
const extraComponent = (() => {
......@@ -97,6 +98,7 @@ const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka }) => {
</Box>
);
} else if (value === 'APPROVED') {
const isPengganti = fgpengganti === 'TD.00401';
mainComponent = (
<Box
sx={{
......@@ -107,7 +109,7 @@ const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka }) => {
}}
>
<Chip
label="Normal"
label={isPengganti ? 'Normal - Pengganti' : 'Normal'}
size="small"
variant="outlined"
sx={{
......
......@@ -229,6 +229,7 @@ export function DlkListView() {
value={value?.value}
fgpelunasan={value?.row?.fgpelunasan}
fguangmuka={value?.row?.fguangmuka}
fgpengganti={value?.row?.fgpengganti}
/>
),
},
......@@ -344,13 +345,13 @@ export function DlkListView() {
[]
);
const handleEditData = useCallback(
(type = 'ubah') => {
const handleNavigateBySelection = useCallback(
(type: 'ubah' | 'retur' | 'pengganti') => {
const selectedRow = dataSelectedRef.current[0];
if (!selectedRow) return;
navigate(`/faktur/dokumen-lain/pajak-keluaran/${selectedRow.id}/${type}`);
},
[navigate]
);
......@@ -457,7 +458,7 @@ export function DlkListView() {
{
title: 'Edit',
icon: <EditNoteTwoTone sx={{ width: 26, height: 26 }} />,
func: () => handleEditData('ubah'),
func: () => handleNavigateBySelection('ubah'),
disabled: !validatedActions.canEdit,
},
{
......@@ -483,7 +484,7 @@ export function DlkListView() {
{
title: 'Pengganti',
icon: <FileOpenTwoTone sx={{ width: 26, height: 26 }} />,
func: () => handleEditData('pengganti'),
func: () => handleNavigateBySelection('pengganti'),
disabled: !validatedActions.canReplacement,
},
{
......@@ -497,13 +498,13 @@ export function DlkListView() {
{
title: 'Retur',
icon: <SwapHorizontalCircleTwoTone sx={{ width: 26, height: 26 }} />,
func: () => setIsReturOpen(true),
func: () => handleNavigateBySelection('retur'),
disabled: !validatedActions.canRetur,
},
],
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[validatedActions, refetch, handleEditData]
[validatedActions, refetch, handleNavigateBySelection]
);
const pinnedColumns = useMemo(
......
import React from 'react';
import { Alert, Typography, Button, IconButton, Box } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
interface AlertInformationDppLainProps {
onClose: () => void;
}
const AlertInformationDppLain: React.FC<AlertInformationDppLainProps> = ({ onClose }) => (
<Alert
icon={
<Box
component="img"
src="/assets/icon-info-dpp-lain.svg"
alt="Info DPP Lain"
sx={{ width: 60, height: 60 }}
/>
}
severity="warning"
sx={{
position: 'relative',
border: '1px solid #FACC15', // yellow-400
alignItems: 'center',
bgcolor: '#FFFBEB', // yellow-50
}}
>
{/* Main Content */}
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Nilai DPP Nilai Lain/DPP diisi dengan memperhatikan ketentuan perpajakan yang berlaku.
</Typography>
<Typography variant="body2">Referensi:</Typography>
{/* List */}
<Box component="ul" sx={{ listStyle: 'none', pl: 0 }}>
<Box component="li" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#663C00' }} />
<Typography variant="body2" component="span" sx={{ pt: 1 }}>
Peraturan Menteri Keuangan Nomor 131 Tahun 2024.{' '}
<Button
component="a"
href="https://datacenter.ortax.org/ortax/aturan/show/26049"
target="_blank"
rel="noreferrer"
variant="text"
color="primary"
size="small"
sx={{
minWidth: 0,
p: 0,
textTransform: 'none',
'&:hover': { bgcolor: 'transparent' },
}}
>
Klik di sini
</Button>
</Typography>
</Box>
</Box>
</Box>
{/* Close Button */}
<IconButton
aria-label="Close Dialog"
onClick={onClose}
size="small"
sx={{ position: 'absolute', top: 8, right: 8 }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
);
export default AlertInformationDppLain;
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, AddTaskOutlined, TaskAltOutlined } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
type Props = {
value?: string;
revNo?: number;
fgpelunasan?: string | boolean;
fguangmuka?: string | boolean;
fgpengganti?: string;
credit?: string;
};
const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka, fgpengganti, credit }) => {
if (!value) return <Chip label="" size="small" />;
const componentCredit = (() => {
if (credit === 'UNCREDITED') {
return (
<Chip
label="UNCREDITED"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
} else if (credit === 'CREDITED') {
return (
<Chip
label="CREDITED"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
} else if (credit === '') {
return (
<Chip
label="Belum diKreditkan"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
}
return null;
})();
const extraComponent = (() => {
if (fgpelunasan) {
return (
<Tooltip title="Pelunasan">
<IconButton
size="small"
sx={{
backgroundColor: 'blue',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'blue',
},
}}
>
<TaskAltOutlined style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
);
}
if (fguangmuka) {
return (
<Tooltip title="Uang Muka">
<IconButton
size="small"
sx={{
backgroundColor: 'green',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'green',
},
}}
>
<AddTaskOutlined style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
);
}
return null;
})();
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') {
const isPengganti = fgpengganti === 'TD.00401';
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label={isPengganti ? 'Normal - Pengganti' : '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 === 'AMENDED') {
mainComponent = (
<Chip
label="Diganti"
size="small"
variant="outlined"
sx={{
color: '#fff',
backgroundColor: '#f38c28',
borderRadius: '8px',
fontWeight: 500,
border: 'none',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.15)',
}}
/>
);
} else if (value === 'CANCELLED') {
mainComponent = (
<Chip
label="Batal"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
} else if (value === 'DRAFT') {
mainComponent = (
<Chip
label="Draft"
size="small"
variant="outlined"
sx={{
borderColor: '#9e9e9e',
color: '#616161',
borderRadius: '8px',
}}
/>
);
} else if (value === 'WAITING FOR CANCELLATION') {
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,
}}
/>
<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>
);
}
// ✅ Gabungkan komponen utama + tambahan
return (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
{mainComponent}
{componentCredit}
{extraComponent}
</Box>
);
};
export default React.memo(StatusChip);
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
} from '@mui/material';
interface CancelConfirmationDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
selectedCount: number;
}
const CancelConfirmationDialog: React.FC<CancelConfirmationDialogProps> = ({
open,
onClose,
onConfirm,
selectedCount,
}) => (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Konfirmasi Pembatalan</DialogTitle>
<DialogContent>
<Typography>
Apakah Anda yakin ingin membatalkan {selectedCount} data yang dipilih?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Batal</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Ya, Batalkan
</Button>
</DialogActions>
</Dialog>
);
export default CancelConfirmationDialog;
import React, { useEffect, 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 useCancelDokumenLainMasukan from '../../hooks/useCancelDokumenLainMasukan';
interface ModalCancelDokumenLainMasukanProps {
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 ModalCancelDokumenLainMasukan: React.FC<ModalCancelDokumenLainMasukanProps> = ({
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 } = useCancelDokumenLainMasukan({
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 ingin melakukan pembatalan?"
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 ModalCancelDokumenLainMasukan;
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';
import {
getDetailTransaksiText,
getDokumenTransaksiText,
getKdTransaksiText,
} from '../../constant';
dayjs.extend(utc);
dayjs.extend(timezone);
interface ModalCetakDokumenLainMasukanProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
type Row = { label: string; value: React.ReactNode };
const ModalCetakDokumenLainMasukan: React.FC<ModalCetakDokumenLainMasukanProps> = ({
payload,
isOpen,
onClose,
}) => {
const [rows, setRows] = useState<Row[]>([]);
useEffect(() => {
if (!payload) return;
const formattedRows: Row[] = [
{
label: 'NPWP Penjual',
value: payload.npwppembeli,
},
{
label: 'Nama Penjual',
value: payload.namapenjual,
},
{
label: 'Kode Dokumen',
value: getDokumenTransaksiText(payload.dokumentransaksi),
},
{ label: 'Nomor Dokumen', value: payload.nomordokumen },
{
label: 'Tanggal Dokumen',
value: payload.tanggaldokumen
? dayjs(payload.tanggaldokumen).local().format('DD/MM/YYYY')
: '-',
},
{
label: 'Masa Pajak',
value: payload.masapajak,
},
{
label: 'Tahun Pajak',
value: payload.tahunpajak,
},
{
label: 'Status',
value: <StatusChip value={payload.statusdokumen} />,
},
{
label: 'Jumlah DPP',
value: `${Number(payload.jumlahdpp || 0)}`,
},
{
label: 'Jumlah PPN',
value: `${Number(payload.jumlahppn || 0)}`,
},
{
label: 'Jumlah PPnBM',
value: `${Number(payload.jumlahppnbm || 0)}`,
},
{
label: 'Tanggal Approval',
value: payload.tanggalapproval
? dayjs(payload.tanggalapproval).local().format('DD/MM/YYYY')
: '-',
},
{
label: 'Nama Penandatangan',
value: payload.namapenandatangan || '-',
},
{
label: 'Keterangan Tambahan',
value: payload.keterangantambahan || '-',
},
{
label: 'Referensi',
value: payload.referensi || '-',
},
{
label: 'NPWP Pembeli',
value: payload.npwppembeli,
},
{ label: 'Kode Transaksi', value: getKdTransaksiText(payload.kdtransaksi) },
{
label: 'Nama Pembeli',
value: payload.namapembeli,
},
{ label: 'Kode Transaksi', value: getKdTransaksiText(payload.kdtransaksi) },
{
label: 'Detail Transaksi',
value: getDetailTransaksiText(payload.detailtransaksi),
},
{
label: 'User Perekam',
value: payload.created_by || '-',
},
{
label: 'Tanggal Rekam',
value: payload.created_at ? dayjs(payload.created_at).local().format('DD/MM/YYYY') : '-',
},
{
label: 'User Pengubah',
value: payload.updated_by || '-',
},
{
label: 'Tanggal Ubah',
value: payload.updated_at ? dayjs(payload.updated_at).local().format('DD/MM/YYYY') : '-',
},
];
setRows(formattedRows);
}, [payload]);
return (
<DialogUmum maxWidth="lg" isOpen={isOpen} onClose={onClose} title="Detail Dokumen Lain Masukan">
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
<ListDetailBuilder rows={rows} />
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakDokumenLainMasukan;
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 useDeleteDokumenLainMasukan from '../../hooks/useDeleteDokumenLainMasukan';
interface ModalDeleteDokumenLainMasukanProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
onConfirmDelete?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalDeleteDokumenLainMasukan: React.FC<ModalDeleteDokumenLainMasukanProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useDeleteDokumenLainMasukan({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmDelete?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['dlm', 'delete'] });
}
};
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 ModalDeleteDokumenLainMasukan;
import { zodResolver } from '@hookform/resolvers/zod';
import Grid from '@mui/material/Grid';
import { useQueryClient } from '@tanstack/react-query';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import type React from 'react';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import z from 'zod';
import { LoadingButton } from '@mui/lab';
import Stack from '@mui/material/Stack';
import { enqueueSnackbar } from 'notistack';
import dayjs from 'dayjs';
import usePrepopulatedDokumenLainMasukan from '../../hooks/usePrepopulatedDokumenLainMasukan';
interface ModalPrepopulatedDokumenLainMasukanProps {
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;
}
/* -------------------------------------------------------------------------- */
/* Schema */
/* -------------------------------------------------------------------------- */
const schema = z.object({
masaTahunPajak: z.string().min(1, 'Masa tahun pajak wajib diisi'),
});
type FormValues = z.infer<typeof schema>;
const ModalPrepolulatedDokumenLainMasukan: React.FC<ModalPrepopulatedDokumenLainMasukanProps> = ({
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Berhasil unduh faktur',
onConfirmUpload,
}) => {
const queryClient = useQueryClient();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const methods = useForm<FormValues>({
mode: 'all',
resolver: zodResolver(schema),
defaultValues: {
masaTahunPajak: '',
},
});
const { handleSubmit } = methods;
const { mutateAsync, isPending } = usePrepopulatedDokumenLainMasukan({
onSuccess: () => {
processSuccess();
enqueueSnackbar(successMessage, { variant: 'success' });
},
onError: (err) => {
processFail();
enqueueSnackbar(err.message || 'Gagal unduh Dokumen Lain Masukan', {
variant: 'error',
});
},
});
/* -------------------------------------------------------------------------- */
/* Submit */
/* -------------------------------------------------------------------------- */
const onSubmit = async (values: FormValues) => {
const d = dayjs(values.masaTahunPajak);
if (!d.isValid()) {
enqueueSnackbar('Masa pajak tidak valid', { variant: 'error' });
return;
}
const payload = {
tahunPajak: d.format('YYYY'),
masaPajak: d.format('MM'),
};
try {
// 🔥 WAJIB buka dulu
setIsOpenDialogProgressBar(true);
setNumberOfData(1);
await mutateAsync(payload);
await onConfirmUpload?.();
} catch {
// error sudah ditangani di hook
} finally {
queryClient.invalidateQueries({
queryKey: ['prepopulated-faktur-pm'],
});
}
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
return (
<>
<FormProvider {...methods}>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleCloseModal}
title="Unduh Faktur Prepopulated"
maxWidth="sm"
>
<Grid size={{ md: 12 }} sx={{ mt: 2 }}>
<Field.DatePicker
name="masaTahunPajak"
label="Masa Pajak"
slotProps={{ textField: { helperText: '' } }}
views={['year', 'month']}
openTo="month"
format="MM/YYYY"
maxDate={dayjs()}
/>
</Grid>
<Stack direction="row" justifyContent="end" spacing="16px" mt={3}>
<LoadingButton onClick={handleCloseModal} variant="text" size="medium">
Tutup
</LoadingButton>
<LoadingButton
onClick={handleSubmit(onSubmit)}
loading={isPending}
variant="contained"
size="medium"
>
Unduh Faktur
</LoadingButton>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalPrepolulatedDokumenLainMasukan;
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-dom';
// 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 ModalReturDokumenLainMasukan: 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 = () => {
console.log('dataSelected:', dataSelected);
console.log('selectedId:', selectedId);
if (!selectedId) return;
handleCloseModal();
clearSelection();
navigate(`/faktur/dokumen-lain/pajak-masukan/${selectedId}/${type}`);
};
return (
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan melakukan Retur Dokumen Lain Masukan ?"
description=""
actionTitle="Iya"
isOpen={isOpenDialogCancel}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={handleSubmit}
/>
);
};
export default ModalReturDokumenLainMasukan;
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 Button from '@mui/material/Button';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import { usePostCreditUncredited } from '../../hooks/usePostCreditUncredited';
// import usePostCreditUncredited from '../../hooks/usePostCreditUncredited';
interface ModalUploadCreditedProps {
dataSelected?: GridRowSelectionModel;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
statusPembeli: 'CREDITED' | 'UNCREDITED' | 'INVALID';
successMessage?: string;
onConfirmUpload?: () => Promise<void> | void;
}
/** normalize selection MUI */
const normalizeSelection = (sel?: any): (number | string)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel;
if (sel?.ids instanceof Set) return Array.from(sel.ids);
return [];
};
const ModalUploadCreditedDokumenLainMasukan: React.FC<ModalUploadCreditedProps> = ({
dataSelected,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
onConfirmUpload,
statusPembeli,
successMessage = 'Status dokumen berhasil diperbarui',
}) => {
const queryClient = useQueryClient();
const [openProgress, setOpenProgress] = useState(false);
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const { mutateAsync, isPending } = usePostCreditUncredited({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
const ids = normalizeSelection(dataSelected);
const handleSubmit = async () => {
try {
setOpenProgress(true);
setNumberOfData(ids.length);
const promises = ids.map((id) =>
mutateAsync({
id: Number(id),
statusPembeli,
})
);
await Promise.allSettled(promises);
await onConfirmUpload?.();
enqueueSnackbar(successMessage, { variant: 'success' });
} catch (err: any) {
enqueueSnackbar(err?.message || 'Gagal memproses data', { variant: 'error' });
} finally {
queryClient.invalidateQueries({ queryKey: ['dokumen-lain-masukan'] });
}
};
const handleClose = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
useEffect(() => {
setNumberOfData(ids.length);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenDialogUpload, dataSelected]);
return (
<>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleClose}
title="Apakah Anda yakin ingin mengubah status dokumen?"
maxWidth="sm"
>
<Stack spacing={2}>
<Alert severity="warning">
<Typography variant="body2">
Status dokumen akan diubah menjadi <b>{statusPembeli}</b>.
</Typography>
</Alert>
<Stack direction="row" justifyContent="flex-end" spacing={1}>
<Button variant="contained" onClick={handleClose}>
Tidak
</Button>
<Button variant="contained" loading={isPending} onClick={handleSubmit}>
Ya, Lanjutkan
</Button>
</Stack>
</Stack>
</DialogUmum>
<DialogProgressBar
isOpen={openProgress}
handleClose={() => {
handleClose();
setOpenProgress(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalUploadCreditedDokumenLainMasukan;
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 { useReturDokumenLainMasukan } from '../../hooks/useReturDokumenLainMasukan';
import useUploadDokumenLainMasukan from '../../hooks/useUploadDokumenLainKeluaran';
interface ModalUploadDokumenLainMasukanProps {
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;
singleUploadPayload?: any;
isRetur?: boolean;
}
/**
* Normalize selection -> array of ids
*/
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)[];
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 ModalUploadDokumenLainMasukan: React.FC<ModalUploadDokumenLainMasukanProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
isRetur,
}) => {
const queryClient = useQueryClient();
// progress state helpers
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);
// upload mutation (ke API upload)
const { mutateAsync: uploadMutateAsync, isPending: uploadIsPending } =
useUploadDokumenLainMasukan({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// retur mutation (dipanggil sebelum upload jika isRetur && singleUploadPayload)
// NOTE: useReturDokumenLainKeluaran expects { dlkData, onSuccess? } — jangan kirim onError di sini.
const returMutation = useReturDokumenLainMasukan({
dlkData: singleUploadPayload ?? {},
onSuccess: () => processSuccess(),
});
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([uploadMutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => uploadMutateAsync({ id: String(id) })));
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
// jika mode retur dan payload single (dari rekam view), panggil retur dulu
if (isRetur && singleUploadPayload) {
await returMutation.mutateAsync(singleUploadPayload);
}
// lanjut ke upload (bisa singleUploadPayload atau multiple selection)
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmUpload?.();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal proses data', { variant: 'error' });
} finally {
// refresh relevant cache
queryClient.invalidateQueries({ queryKey: ['dlk'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogUpload, dataSelected, setNumberOfData]);
// combine loading state: upload OR retur in-flight
const isLoading =
Boolean(uploadIsPending) ||
Boolean((returMutation as any).isLoading || (returMutation as any).isPending);
return (
<>
<FormProvider {...methods}>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleCloseModal}
title="Upload Dokumen Lain Masukan"
>
<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 data yang saya masukkan adalah benar dan saya bertanggung jawab atas kebenaran data yang saya masukkan sesuai dengan Syarat dan Ketentuan serta Kebijakan Privasi dari PajakExpress."
/>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<Button
type="button"
disabled={!isCheckedAgreement || isLoading}
onClick={onSubmit}
variant="contained"
sx={{ background: '#143B88' }}
>
{isLoading ? 'Processing...' : '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 ModalUploadDokumenLainMasukan;
import Divider from '@mui/material/Divider';
import React, { useEffect, useRef } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import { useParams } from 'react-router';
import Grid from '@mui/material/Grid';
interface InformasiPembeliProps {
dlmData?: any;
isLoading?: boolean;
isRetur?: boolean;
}
const InformasiPembeli: React.FC<InformasiPembeliProps> = ({ dlmData, isRetur }) => {
const { type } = useParams<{ id?: string; type?: 'ubah' | 'pengganti' | 'new' }>();
const { control, setValue } = useFormContext();
const isPengganti = type === 'pengganti';
const kdTransaksi = useWatch({
control,
name: 'kdTransaksi',
});
const prevKdTransaksi = useRef<string | undefined>(undefined);
const isEdit = type === 'ubah';
const canModify = !dlmData || isEdit;
const canEditnamaPenjual = isRetur || canModify;
useEffect(() => {
if (!kdTransaksi) return;
// 🔁 BERUBAH DARI IMPORT → SELAIN IMPORT
if (prevKdTransaksi.current === 'IMPORT' && kdTransaksi !== 'IMPORT') {
setValue('npwpPenjual', '', {
shouldDirty: true,
shouldValidate: true,
});
}
// ✅ MASUK KE IMPORT
if (kdTransaksi === 'IMPORT') {
setValue('npwpPenjual', '0000000000000000', {
shouldDirty: false,
shouldValidate: true,
});
}
// simpan nilai terakhir
prevKdTransaksi.current = kdTransaksi;
}, [kdTransaksi, setValue]);
useEffect(() => {
if (!isRetur) return;
if (!dlmData) return;
// NPWP dikunci & diambil dari dokumen asal
setValue('npwpPenjual', dlmData.npwppenjual ?? '', {
shouldDirty: false,
shouldValidate: false,
});
// Nama pembeli default dari dokumen asal (masih boleh diedit)
setValue('namaPenjual', dlmData.namapenjual ?? '', {
shouldDirty: false,
shouldValidate: false,
});
}, [isRetur, dlmData, setValue]);
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12 }} sx={{ mt: 3 }}>
<Divider sx={{ fontWeight: 'bold', fontSize: '1rem' }} textAlign="left">
Informasi Penjual
</Divider>
</Grid>
{/* IDENTITAS */}
{/* NPWP*/}
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text
name="npwpPenjual"
label="NPWP Penjual"
disabled={!canModify || kdTransaksi === 'IMPORT' || isPengganti || isRetur}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
maxLength: 16,
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 16);
setValue('npwpPenjual', value, { shouldDirty: true });
}}
sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
/>
</Grid>
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text
name="namaPenjual"
label="Nama Penjual"
// disabled={!canModify || isPengganti}
disabled={!canEditnamaPenjual || isPengganti || isRetur}
sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
/>
</Grid>
</Grid>
);
};
export default InformasiPembeli;
This source diff could not be displayed because it is too large. You can view the blob instead.
import React, { useEffect, useRef } from 'react';
import Grid from '@mui/material/Grid';
import Divider from '@mui/material/Divider';
import { useFormContext, useWatch } from 'react-hook-form';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
import { KD_TRANSAKSI } from '../../constant';
import { useParams } from 'react-router';
interface TotalTransaksiProps {
dlmData?: any; // data dari API
isRetur?: boolean;
}
const TotalTransaksi: React.FC<TotalTransaksiProps> = ({ dlmData, isRetur }) => {
const { control, setValue } = useFormContext<any>();
const { type } = useParams<{ id?: string; type?: 'ubah' | 'pengganti' | 'new' }>();
const isEditMode = type === 'ubah' || type === 'pengganti';
const jumlahDpp = useWatch({ control, name: 'jumlahDpp' });
const kdTransaksi = useWatch({ control, name: 'kdTransaksi' });
const maxDpp = Number(dlmData?.returMaxDpp ?? 0);
const maxPpn = Number(dlmData?.returMaxPpn ?? 0);
const maxPpnbm = Number(dlmData?.returMaxPpnbm ?? 0);
const isExport = kdTransaksi === KD_TRANSAKSI.EXPORT;
console.log(maxPpnbm);
useEffect(() => {
// 🔒 EDIT / PENGGANTI → JANGAN SENTUH NILAI API
if (isEditMode) return;
// CREATE → AUTO HITUNG
const dpp = Number(jumlahDpp || 0);
const calculatedPpn = +(dpp * 0.12).toFixed(2);
setValue('jumlahPpn', calculatedPpn, {
shouldDirty: false,
shouldTouch: false,
});
}, [isEditMode, isExport, jumlahDpp, setValue]);
const prevDppRef = useRef<number | null>(null);
useEffect(() => {
prevDppRef.current = Number(jumlahDpp || 0);
}, [jumlahDpp]);
const dpp = Number(jumlahDpp || 0);
const ppn = Number(useWatch({ control, name: 'jumlahPpn' }) || 0);
const ppnbm = Number(useWatch({ control, name: 'jumlahPpnbm' }) || 0);
// useEffect(() => {
// if (!isRetur) return;
// if (maxDpp && dpp > maxDpp) {
// setValue('jumlahDpp', maxDpp, { shouldDirty: true });
// }
// if (maxPpn && ppn > maxPpn) {
// setValue('jumlahPpn', maxPpn, { shouldDirty: true });
// }
// if (maxPpnbm && ppnbm > maxPpnbm) {
// setValue('jumlahPpnbm', maxPpnbm, { shouldDirty: true });
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [isRetur, jumlahDpp, maxDpp, maxPpn, maxPpnbm]);
useEffect(() => {
if (!isRetur) return;
if (maxDpp && dpp > maxDpp) {
setValue('jumlahDpp', maxDpp, { shouldDirty: true });
}
if (maxPpn && ppn > maxPpn) {
setValue('jumlahPpn', maxPpn, { shouldDirty: true });
}
if (maxPpnbm && ppnbm > maxPpnbm) {
setValue('jumlahPpnbm', maxPpnbm, { shouldDirty: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRetur, dpp, ppn, ppnbm, maxDpp, maxPpn, maxPpnbm]);
const formatRupiah = (val: number) => val.toLocaleString('id-ID');
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
</Divider>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="jumlahDpp"
label="Jumlah DPP (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly={false}
helperText={isRetur && maxDpp ? `Max Jumlah DPP : ${formatRupiah(maxDpp)}` : ''}
/>
</Grid>
<Grid size={{ md: 4 }}>
{/* <RHFNumeric
name="jumlahPpn"
label="Jumlah PPN (Rp)"
allowDecimalValue
decimalScale={2}
helperText={isRetur && maxPpn ? `Max Jumlah PPN : ${formatRupiah(maxPpn)}` : ''}
onValueChange={() => {
setValue('isPpnManual', true, { shouldDirty: false });
}}
/> */}
<RHFNumeric
name="jumlahPpn"
label="Jumlah PPN (Rp)"
allowDecimalValue
decimalScale={2}
helperText={isRetur && maxPpn ? `Max Jumlah PPN : ${formatRupiah(maxPpn)}` : ''}
onValueChange={(values: any) => {
if (!isRetur) return;
const value = Number(values.floatValue || 0);
if (maxPpn && value > maxPpn) {
setValue('jumlahPpn', maxPpn, { shouldDirty: true });
return;
}
setValue('jumlahPpn', value, { shouldDirty: true });
setValue('isPpnManual', true, { shouldDirty: false });
}}
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="jumlahPpnbm"
label="Jumlah PPnBM (Rp)"
allowDecimalValue
decimalScale={2}
helperText={
isRetur && maxPpnbm >= 0 ? `Max Jumlah PPnBM : ${formatRupiah(maxPpnbm)}` : ''
}
onValueChange={(values: any) => {
if (!isRetur) return;
const value = Number(values.floatValue || 0);
if (maxPpnbm && value > maxPpnbm) {
setValue('jumlahPpnbm', maxPpnbm, { shouldDirty: true });
return;
}
setValue('jumlahPpnbm', value, { shouldDirty: true });
}}
/>
</Grid>
</Grid>
);
};
export default TotalTransaksi;
import React, { useEffect, useState } from 'react';
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';
interface ModalDeleteBarangJasaProps {
/** data yang sedang dipilih untuk dihapus */
dataSelected?: GridRowSelectionModel;
/** buka / tutup modal delete */
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
/** callback setelah delete sukses */
onConfirmDelete?: () => Promise<void> | void;
/** pesan sukses */
successMessage?: string;
}
/**
* Normalisasi selection model agar selalu jadi array id
*/
const normalizeSelection = (sel?: GridRowSelectionModel): (string | number)[] => {
if (!sel) return [];
// ✅ v7 shape baru: { type: 'include', ids: Set(...) }
if (typeof sel === 'object' && 'ids' in sel && sel.ids instanceof Set) {
return Array.from(sel.ids);
}
// fallback untuk bentuk array lama
if (Array.isArray(sel)) return sel;
// fallback terakhir, kalau object key-value
if (typeof sel === 'object') {
return Object.keys(sel);
}
return [];
};
const ModalDeleteBarangJasa: React.FC<ModalDeleteBarangJasaProps> = ({
dataSelected,
isOpenDialogDelete,
setIsOpenDialogDelete,
onConfirmDelete,
successMessage = 'Data berhasil dihapus',
}) => {
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
// setIsOpenDialogProgressBar(true);
await onConfirmDelete?.();
processSuccess();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
} catch (error: any) {
processFail();
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length || 1);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin ingin 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 ModalDeleteBarangJasa;
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import HighlightOffTwoToneIcon from '@mui/icons-material/HighlightOffTwoTone';
interface ToolbarCancelProps {
selectedRows: any[];
selectedRowsData: any[];
onCancel: (ids: number[]) => void;
}
const ToolbarCancel: React.FC<ToolbarCancelProps> = ({
selectedRows,
selectedRowsData,
onCancel,
}) => {
// Logic sederhana
const isEnabled =
selectedRows.length > 0 &&
selectedRowsData.every((row: any) => row.fgStatus === 'normal' || row.fgStatus === 'amendment');
const handleClick = () => {
if (!isEnabled) return;
const ids = selectedRowsData.map((row: any) => row.id).filter((id: any) => id !== undefined);
onCancel(ids);
};
return (
<Tooltip title={isEnabled ? `Batalkan ${selectedRows.length} data` : 'Pilih data yang valid'}>
<IconButton
onClick={handleClick}
disabled={!isEnabled}
color={isEnabled ? 'error' : 'default'}
>
<HighlightOffTwoToneIcon />
</IconButton>
</Tooltip>
);
};
export default ToolbarCancel;
export type TransaksiCode = 'DELIVERY' | 'EXPORT';
export interface TransaksiCodeOption {
value: TransaksiCode;
label: string;
}
export const TRANSAKSI_CODE_OPTIONS: TransaksiCodeOption[] = [
{ value: 'DELIVERY', label: 'DELIVERY' },
{ value: 'EXPORT', label: 'EXPORT' },
];
export const getTransaksiCodeLabel = (value?: string) =>
TRANSAKSI_CODE_OPTIONS.find((opt) => opt.value === value)?.label ?? value;
export const DOKUMEN_TRANSAKSI = {
DOKUMEN_DIPERSAMAKAN: 'OSD.00601',
CUKAI_ROKOK: 'OSD.00602',
DOKUMEN_KAWASAN_BERIKAT: 'OSD.00603',
PEMBERITAHUAN_EKSPOR_BARANG: 'OSD.00604',
PEMBERITAHUAN_EKSPOR_JASA: 'OSD.00605',
PEMBERITAHUAN_IMPOR_BAYAR: 'OSD.00606',
PEMBAYARAN: 'OSD.00607',
DOKUMEN_TERTENTU: 'OSD.00608',
SKPKB_SKPKBT: 'OSD.00609',
PEMBERITAHUAN_IMPOR: 'OSD.00610',
} as const;
export type DokumenTransaksi = (typeof DOKUMEN_TRANSAKSI)[keyof typeof DOKUMEN_TRANSAKSI];
export interface DokumenTransaksiOption {
value: DokumenTransaksi;
label: string;
}
export const DOKUMEN_TRANSAKSI_OPTIONS: DokumenTransaksiOption[] = [
{
value: DOKUMEN_TRANSAKSI.DOKUMEN_DIPERSAMAKAN,
label: 'Dokumen yang Dipersamakan dengan Faktur Pajak',
},
{
value: DOKUMEN_TRANSAKSI.CUKAI_ROKOK,
label: 'Cukai Rokok',
},
{
value: DOKUMEN_TRANSAKSI.DOKUMEN_KAWASAN_BERIKAT,
label: 'Dokumen Kawasan Berikat',
},
{
value: DOKUMEN_TRANSAKSI.PEMBERITAHUAN_EKSPOR_BARANG,
label: 'Pemberitahuan Ekspor Barang (PEB)',
},
{
value: DOKUMEN_TRANSAKSI.PEMBERITAHUAN_EKSPOR_JASA,
label: 'Pemberitahuan Ekspor BKP Tidak Berwujud / Pemberitahuan Ekspor JKP',
},
{
value: DOKUMEN_TRANSAKSI.PEMBERITAHUAN_IMPOR_BAYAR,
label: 'PIB dan SSP',
},
{
value: DOKUMEN_TRANSAKSI.PEMBAYARAN,
label: 'SSP',
},
{
value: DOKUMEN_TRANSAKSI.DOKUMEN_TERTENTU,
label: 'Dokumen Tertentu',
},
{
value: DOKUMEN_TRANSAKSI.SKPKB_SKPKBT,
label: 'SKPKB/SKPKBT atas JLN',
},
{
value: DOKUMEN_TRANSAKSI.PEMBERITAHUAN_IMPOR,
label: 'Pemberitahuan Impor',
},
];
// helper aman untuk formatter / fallback
export const getDokumenTransaksiLabel = (value?: string) =>
DOKUMEN_TRANSAKSI_OPTIONS.find((opt) => opt.value === value)?.label ?? value;
export const DETAIL_TRANSAKSI = {
PENYERAHAN_NON_PEMUNGUT: 'TD.00301',
PENYERAHAN_PEMUNGUT_PEMERINTAH: 'TD.00302',
PENYERAHAN_PEMUNGUT_NON_PEMERINTAH: 'TD.00303',
DPP_LAIN: 'TD.00304',
PERSENTASE_TERTENTU: 'TD.00305',
TURIS_RESTITUSI: 'TD.00306',
PPN_TIDAK_DIPUNGUT: 'TD.00307',
PPN_DIBEBASKAN: 'TD.00308',
PENJUALAN_ASET: 'TD.00309',
TARIF_TIDAK_NORMAL: 'TD.00310',
EKSPOR_BERWUJUD: 'TD.00311',
EKSPOR_TIDAK_BERWUJUD: 'TD.00312',
EKSPOR_JASA: 'TD.00313',
IMPOR_BERWUJUD: 'TD.00314',
IMPOR_TIDAK_BERWUJUD: 'TD.00315',
};
export const DETAIL_TRANSAKSI_TEXT = {
[DETAIL_TRANSAKSI.PENYERAHAN_NON_PEMUNGUT]: 'Kepada PKP Penjual selain Pemungut PPN',
[DETAIL_TRANSAKSI.PENYERAHAN_PEMUNGUT_PEMERINTAH]: 'Kepada Pemungut PPN Instansi Pemerintah',
[DETAIL_TRANSAKSI.PENYERAHAN_PEMUNGUT_NON_PEMERINTAH]:
'Kepada Pemungut PPN selain Instansi Pemerintah',
[DETAIL_TRANSAKSI.DPP_LAIN]: 'Penyerahan yang menggunakan Dasar Pengenaan Pajak (DPP) Nilai Lain',
[DETAIL_TRANSAKSI.PERSENTASE_TERTENTU]:
'Penyerahan yang PPN-nya dipungut dengan besaran tertentu',
[DETAIL_TRANSAKSI.TURIS_RESTITUSI]:
'Kepada pemegang paspor luar negeri dalam rangka VAT Refund for Tourist',
[DETAIL_TRANSAKSI.PPN_TIDAK_DIPUNGUT]:
'Penyerahan yang mendapat fasilitas PPN dan/atau PPnBM Tidak Dipungut atau Ditanggung Pemerintah',
[DETAIL_TRANSAKSI.PPN_DIBEBASKAN]:
'Penyerahan yang mendapat fasilitas dibebaskan dari pengenaan PPN dan/atau PPnBM',
[DETAIL_TRANSAKSI.PENJUALAN_ASET]:
'Penyerahan aktiva yang menurut tujuan semula tidak untuk diperjualbelikan',
[DETAIL_TRANSAKSI.TARIF_TIDAK_NORMAL]: 'Untuk penyerahan lainnya',
[DETAIL_TRANSAKSI.EKSPOR_BERWUJUD]: 'Ekspor BKP Berwujud',
[DETAIL_TRANSAKSI.EKSPOR_TIDAK_BERWUJUD]: 'Ekspor BKP Tidak Berwujud',
[DETAIL_TRANSAKSI.EKSPOR_JASA]: 'Ekspor JKP',
[DETAIL_TRANSAKSI.IMPOR_BERWUJUD]: 'Impor BKP',
[DETAIL_TRANSAKSI.IMPOR_TIDAK_BERWUJUD]: 'Pemanfaatan BKP Tidak Berwujud dan JKP',
};
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',
};
// Transaction Types (short names)
export const KD_TRANSAKSI = {
DELIVERY_SPECIAL: 'DELIVERY_SPECIAL',
EXPORT: 'EXPORT',
DELIVERY: 'DELIVERY',
IMPORT: 'IMPORT',
PURCHASE: 'PURCHASE',
UNCREDIT: 'UNCREDIT',
};
// Transaction Type Codes
export const KD_TRANSAKSI_CODE = {
[KD_TRANSAKSI.DELIVERY]: 'OSD.00101',
[KD_TRANSAKSI.DELIVERY_SPECIAL]: 'OSD.00102',
[KD_TRANSAKSI.EXPORT]: 'OSD.00103',
[KD_TRANSAKSI.IMPORT]: 'OSD.00104',
[KD_TRANSAKSI.PURCHASE]: 'OSD.00105',
[KD_TRANSAKSI.UNCREDIT]: 'OSD.00106',
};
// Transaction Type Descriptions
export const KD_TRANSAKSI_TEXT = {
[KD_TRANSAKSI.DELIVERY]: 'Penyerahan dengan Menggunakan Dokumen Tertentu',
[KD_TRANSAKSI.DELIVERY_SPECIAL]: 'Pengiriman dengan dokumen khusus',
[KD_TRANSAKSI.EXPORT]: 'Ekspor',
[KD_TRANSAKSI.IMPORT]: 'Impor barang/pemanfaatan barang/jasa tidak berwujud',
[KD_TRANSAKSI.PURCHASE]: 'Pembelian lokal',
[KD_TRANSAKSI.UNCREDIT]: 'Pajak masukan tidak dikreditkan atau pajak masukan dengan fasilitas',
};
// Transaction Document Codes
export const DOKUMEN_TRANSAKSI = {
DOKUMEN_DIPERSAMAKAN: 'OSD.00601',
CUKAI_ROKOK: 'OSD.00602',
DOKUMEN_KAWASAN_BERIKAT: 'OSD.00603',
PEMBERITAHUAN_EKSPOR_BARANG: 'OSD.00604',
PEMBERITAHUAN_EKSPOR_JASA: 'OSD.00605',
PEMBERITAHUAN_IMPOR_BAYAR: 'OSD.00606',
PEMBAYARAN: 'OSD.00607',
DOKUMEN_TERTENTU: 'OSD.00608',
SKPKB_SKPKBT: 'OSD.00609',
PEMBERITAHUAN_IMPOR: 'OSD.00610',
};
// Transaction Document Descriptions
export const DOKUMEN_TRANSAKSI_TEXT = {
[DOKUMEN_TRANSAKSI.DOKUMEN_DIPERSAMAKAN]: 'Dokumen yang Dipersamakan dengan Faktur Pajak',
[DOKUMEN_TRANSAKSI.CUKAI_ROKOK]: 'Cukai Rokok',
[DOKUMEN_TRANSAKSI.DOKUMEN_KAWASAN_BERIKAT]: 'Dokumen Kawasan Berikat',
[DOKUMEN_TRANSAKSI.PEMBERITAHUAN_EKSPOR_BARANG]: 'Pemberitahuan Ekspor Barang (PEB)',
[DOKUMEN_TRANSAKSI.PEMBERITAHUAN_EKSPOR_JASA]:
'Pemberitahuan Ekspor BKP Tidak Berwujud/Pemberitahuan Ekspor JKP',
[DOKUMEN_TRANSAKSI.PEMBERITAHUAN_IMPOR_BAYAR]: 'PIB dan SSP',
[DOKUMEN_TRANSAKSI.PEMBAYARAN]: 'SSP',
[DOKUMEN_TRANSAKSI.DOKUMEN_TERTENTU]: 'Dokumen Tertentu',
[DOKUMEN_TRANSAKSI.SKPKB_SKPKBT]: 'SKPKB/SKPKBT atas JLN',
[DOKUMEN_TRANSAKSI.PEMBERITAHUAN_IMPOR]: 'Pemberitahuan Impor',
};
// Additional Information Document Codes
export const KETERANGAN_TAMBAHAN = {
KAWASAN_PERDAGANGAN_BEBAS: 'TD.00501',
KAWASAN_BERIKAT: 'TD.00502',
BANTUAN_LUAR_NEGERI: 'TD.00503',
AVTUR: 'TD.00504',
LAINNYA: 'TD.00505',
KONTRAKTOR_BATUBARA: 'TD.00506',
BBM_TRANSPORTASI_LAUT: 'TD.00507',
JASA_TRANSPORTASI_TERTENTU: 'TD.00508',
BARANG_TERTENTU_KEK: 'TD.00509',
BARANG_TERTENTU_STRATEGIS_SLIME_ANODE: 'TD.00510',
SARANA_TRANSPORTASI_TERTENTU: 'TD.00511',
KONTRAKTOR_MIGAS: 'TD.00512',
RUMAH_DAN_RUSUN: 'TD.00513',
};
// Additional Information Document Descriptions in Indonesian
export const KETERANGAN_TAMBAHAN_TEXT = {
[KETERANGAN_TAMBAHAN.KAWASAN_PERDAGANGAN_BEBAS]: 'Kawasan Perdagangan Bebas',
[KETERANGAN_TAMBAHAN.KAWASAN_BERIKAT]: 'Kawasan Berikat',
[KETERANGAN_TAMBAHAN.BANTUAN_LUAR_NEGERI]: 'Hibah dan Bantuan Luar Negeri',
[KETERANGAN_TAMBAHAN.AVTUR]: 'Avtur',
[KETERANGAN_TAMBAHAN.LAINNYA]: 'Lainnya',
[KETERANGAN_TAMBAHAN.KONTRAKTOR_BATUBARA]: 'Kontraktor PKP2B Generasi I',
[KETERANGAN_TAMBAHAN.BBM_TRANSPORTASI_LAUT]:
'Penyerahan BBM untuk Kapal Laut Angkutan Luar Negeri',
[KETERANGAN_TAMBAHAN.JASA_TRANSPORTASI_TERTENTU]:
'Penyerahan Jasa Kena Pajak Terkait Sarana Transportasi Tertentu',
[KETERANGAN_TAMBAHAN.BARANG_TERTENTU_KEK]: 'Penyerahan Barang Kena Pajak Tertentu di KEK',
[KETERANGAN_TAMBAHAN.BARANG_TERTENTU_STRATEGIS_SLIME_ANODE]:
'Barang Kena Pajak Tertentu Strategis berupa Slime Anode',
[KETERANGAN_TAMBAHAN.SARANA_TRANSPORTASI_TERTENTU]:
'Penyerahan Sarana Transportasi Tertentu dan/atau Jasa Terkait Sarana Transportasi Tertentu',
[KETERANGAN_TAMBAHAN.KONTRAKTOR_MIGAS]:
'Penyerahan kepada Kontraktor Migas yang Sesuai PP No. 27 Tahun 2017',
[KETERANGAN_TAMBAHAN.RUMAH_DAN_RUSUN]:
'Penyerahan Rumah Tapak dan Unit Hunian Rusun Ditanggung Pemerintah untuk Tahun Anggaran 2021',
};
// Kode Dokumen Informasi Tambahan yang Disingkat
export const SHORT_KETERANGAN_TAMBAHAN = {
BARANG_KENA_PAJAK_TERTENTU: 'TD.00501',
BARANG_KENA_PAJAK_STRATEGIS: 'TD.00502',
JASA_BANDARA: 'TD.00503',
LAINNYA: 'TD.00504',
BKP_STRATEGIS_PP_81_2015: 'TD.00505',
JASA_PELABUHAN_TRANSPORTASI_LAUT: 'TD.00506',
PENGIRIMAN_AIR_BERSIH: 'TD.00507',
};
// Deskripsi Dokumen Informasi Tambahan dalam Bahasa Indonesia
export const SHORT_KETERANGAN_TAMBAHAN_TEXT = {
[SHORT_KETERANGAN_TAMBAHAN.BARANG_KENA_PAJAK_TERTENTU]: 'Barang Kena Pajak Tertentu dan Jasa',
[SHORT_KETERANGAN_TAMBAHAN.BARANG_KENA_PAJAK_STRATEGIS]:
'Barang Kena Pajak yang Bersifat Strategis',
[SHORT_KETERANGAN_TAMBAHAN.JASA_BANDARA]: 'Jasa Bandara',
[SHORT_KETERANGAN_TAMBAHAN.LAINNYA]: 'Lainnya',
[SHORT_KETERANGAN_TAMBAHAN.BKP_STRATEGIS_PP_81_2015]:
'BKP Tertentu yang Strategis Sesuai PP Nomor 81 Tahun 2015',
[SHORT_KETERANGAN_TAMBAHAN.JASA_PELABUHAN_TRANSPORTASI_LAUT]:
'Pengiriman Jasa Pelabuhan Tertentu untuk Kegiatan Transportasi Laut Lintas Negara',
[SHORT_KETERANGAN_TAMBAHAN.PENGIRIMAN_AIR_BERSIH]: 'Pengiriman Air Bersih',
};
// Pengganti Constants
export const FG_PENGGANTI_DOKUMEN_LAIN = {
NORMAL: 'TD.00400',
PENGGANTI: 'TD.00401',
};
// Pengganti Constants
export const FG_PENGGANTI = {
YES: '1',
NO: '0',
};
// Document Code Constants
export const KODE_DOKUMEN = {
EBKPTB: 'OSD.00702',
EJKP: 'OSD.00701',
};
// Document Code Descriptions
export const KODE_DOKUMEN_TEXT = {
[KODE_DOKUMEN.EBKPTB]: 'EBKPTB untuk Ekspor BKP Tidak Berwujud',
[KODE_DOKUMEN.EJKP]: 'Pemberitahuan Ekspor Jasa',
};
export const MIN_THN_PAJAK = 2022;
export const getKdTransaksiText = (value?: string) => {
if (!value) return '-';
// ✅ CASE 1: value = 'DELIVERY'
if (KD_TRANSAKSI_TEXT[value as keyof typeof KD_TRANSAKSI_TEXT]) {
return KD_TRANSAKSI_TEXT[value as keyof typeof KD_TRANSAKSI_TEXT];
}
// ✅ CASE 2: value = 'OSD.00101'
const key = Object.keys(KD_TRANSAKSI_CODE).find(
(k) => KD_TRANSAKSI_CODE[k as keyof typeof KD_TRANSAKSI_CODE] === value
);
return key ? KD_TRANSAKSI_TEXT[key as keyof typeof KD_TRANSAKSI_TEXT] : value;
};
export const getDetailTransaksiText = (code?: string) =>
DETAIL_TRANSAKSI_TEXT[code as keyof typeof DETAIL_TRANSAKSI_TEXT] ?? code ?? '-';
export const getDokumenTransaksiText = (code?: string) =>
DOKUMEN_TRANSAKSI_TEXT[code as keyof typeof DOKUMEN_TRANSAKSI_TEXT] ?? code ?? '-';
export const getKeteranganTambahanText = (code?: string) =>
KETERANGAN_TAMBAHAN_TEXT[code as keyof typeof KETERANGAN_TAMBAHAN_TEXT] ??
SHORT_KETERANGAN_TAMBAHAN_TEXT[code as keyof typeof SHORT_KETERANGAN_TAMBAHAN_TEXT] ??
code ??
'-';
export const KODE_DOKUMEN = {
EBKPTB: 'OSD.00702',
EJKP: 'OSD.00701',
} as const;
export type KodeDokumen = (typeof KODE_DOKUMEN)[keyof typeof KODE_DOKUMEN];
export interface KodeDokumenOption {
value: KodeDokumen;
label: string;
}
export const KODE_DOKUMEN_OPTIONS: KodeDokumenOption[] = [
{
value: KODE_DOKUMEN.EBKPTB,
label: 'EBKPTB untuk Ekspor BKP Tidak Berwujud',
},
{
value: KODE_DOKUMEN.EJKP,
label: 'Pemberitahuan Ekspor Jasa',
},
];
// helper aman (formatter / renderCell / fallback)
export const getKodeDokumenLabel = (value?: string) =>
KODE_DOKUMEN_OPTIONS.find((opt) => opt.value === value)?.label ?? value;
const appRootKey = 'unifikasi';
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
faktuPK: {
all: (params: any) => [appRootKey, 'fakturPk', params],
detail: (params: any) => [appRootKey, 'dlm', 'detail', params],
uangMukaDetail: (nomor: string) => ['fakturPK', 'uangMukaDetail', nomor],
draft: [appRootKey, 'fakturPk', 'draft'],
delete: [appRootKey, 'fakturPk', 'delete'],
upload: [appRootKey, 'fakturPk', 'upload'],
cancel: [appRootKey, 'fakturPk', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'fakturPk-cetak-pdf', params],
},
};
export default queryKey;
export type Status = 'DRAFT' | 'APPROVED' | 'WAITING FOR AMENDMENT' | 'AMENDED' | 'CANCELLED';
export interface StatusOption {
value: Status;
label: string;
}
export const STATUS_OPTIONS: StatusOption[] = [
{ value: 'DRAFT', label: 'Draft' },
{ value: 'APPROVED', label: 'Normal' },
{ value: 'WAITING FOR AMENDMENT', label: 'Normal Pengganti' },
{ value: 'AMENDED', label: 'Diganti' },
{ value: 'CANCELLED', label: 'Batal' },
];
// helper (biar formatter rapi)
export const getStatusLabel = (value?: string) =>
STATUS_OPTIONS.find((opt) => opt.value === value)?.label ?? value;
export const DETAIL_TRANSAKSI = {
PENYERAHAN_NON_PEMUNGUT: 'TD.00301',
PENYERAHAN_PEMUNGUT_PEMERINTAH: 'TD.00302',
PENYERAHAN_PEMUNGUT_NON_PEMERINTAH: 'TD.00303',
DPP_LAIN: 'TD.00304',
PERSENTASE_TERTENTU: 'TD.00305',
TURIS_RESTITUSI: 'TD.00306',
PPN_TIDAK_DIPUNGUT: 'TD.00307',
PPN_DIBEBASKAN: 'TD.00308',
PENJUALAN_ASET: 'TD.00309',
TARIF_TIDAK_NORMAL: 'TD.00310',
EKSPOR_BERWUJUD: 'TD.00311',
EKSPOR_TIDAK_BERWUJUD: 'TD.00312',
EKSPOR_JASA: 'TD.00313',
IMPOR_BERWUJUD: 'TD.00314',
IMPOR_TIDAK_BERWUJUD: 'TD.00315',
} as const;
export type DetailTransaksi = (typeof DETAIL_TRANSAKSI)[keyof typeof DETAIL_TRANSAKSI];
export interface DetailTransaksiOption {
value: DetailTransaksi;
label: string;
}
export const DETAIL_TRANSAKSI_OPTIONS: DetailTransaksiOption[] = [
{
value: DETAIL_TRANSAKSI.PENYERAHAN_NON_PEMUNGUT,
label: 'Kepada PKP Penjual selain Pemungut PPN',
},
{
value: DETAIL_TRANSAKSI.PENYERAHAN_PEMUNGUT_PEMERINTAH,
label: 'Kepada Pemungut PPN Instansi Pemerintah',
},
{
value: DETAIL_TRANSAKSI.PENYERAHAN_PEMUNGUT_NON_PEMERINTAH,
label: 'Kepada Pemungut PPN selain Instansi Pemerintah',
},
{
value: DETAIL_TRANSAKSI.DPP_LAIN,
label: 'Penyerahan yang menggunakan Dasar Pengenaan Pajak (DPP) Nilai Lain',
},
{
value: DETAIL_TRANSAKSI.PERSENTASE_TERTENTU,
label: 'Penyerahan yang PPN-nya dipungut dengan besaran tertentu',
},
{
value: DETAIL_TRANSAKSI.TURIS_RESTITUSI,
label: 'Kepada pemegang paspor luar negeri dalam rangka VAT Refund for Tourist',
},
{
value: DETAIL_TRANSAKSI.PPN_TIDAK_DIPUNGUT,
label:
'Penyerahan yang mendapat fasilitas PPN dan/atau PPnBM Tidak Dipungut atau Ditanggung Pemerintah',
},
{
value: DETAIL_TRANSAKSI.PPN_DIBEBASKAN,
label: 'Penyerahan yang mendapat fasilitas dibebaskan dari pengenaan PPN dan/atau PPnBM',
},
{
value: DETAIL_TRANSAKSI.PENJUALAN_ASET,
label: 'Penyerahan aktiva yang menurut tujuan semula tidak untuk diperjualbelikan',
},
{
value: DETAIL_TRANSAKSI.TARIF_TIDAK_NORMAL,
label: 'Untuk penyerahan lainnya',
},
{
value: DETAIL_TRANSAKSI.EKSPOR_BERWUJUD,
label: 'Ekspor BKP Berwujud',
},
{
value: DETAIL_TRANSAKSI.EKSPOR_TIDAK_BERWUJUD,
label: 'Ekspor BKP Tidak Berwujud',
},
{
value: DETAIL_TRANSAKSI.EKSPOR_JASA,
label: 'Ekspor JKP',
},
{
value: DETAIL_TRANSAKSI.IMPOR_BERWUJUD,
label: 'Impor BKP',
},
{
value: DETAIL_TRANSAKSI.IMPOR_TIDAK_BERWUJUD,
label: 'Pemanfaatan BKP Tidak Berwujud dan JKP',
},
];
// helper untuk grid / formatter / fallback
export const getDetailTransaksiLabel = (value?: string) =>
DETAIL_TRANSAKSI_OPTIONS.find((opt) => opt.value === value)?.label ?? value;
This diff is collapsed.
import { useMutation } from '@tanstack/react-query';
import type { TCancelRequest, TCancelResponse } from '../types/types';
import fakturApi from '../utils/api';
const useCancelDokumenLainMasukan = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelRequest>({
mutationKey: ['cancel-dokumen-lain-masukan'],
mutationFn: (payload) => fakturApi.cancelDokumenLainMasukan(payload),
...props,
});
export default useCancelDokumenLainMasukan;
import { useMutation } from '@tanstack/react-query';
import type { TBaseResponseAPI, TDeleteRequest } from '../types/types';
import fakturApi from '../utils/api';
const useDeleteDokumenLainMasukan = (props?: any) =>
useMutation<TBaseResponseAPI<null>, Error, TDeleteRequest>({
mutationKey: ['delete-dokumen-lain-masukan'],
mutationFn: (payload) => fakturApi.deleteDokumenLainMasukan(payload),
...props,
});
export default useDeleteDokumenLainMasukan;
import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query';
import queryKey from '../constant/queryKey';
import type { TGetListDataDokumenLainMasukanResult } from '../types/types';
import fakturApi from '../utils/api';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
export type TGetDokumenLainMasukanApiWrapped = {
data: TGetListDataDokumenLainMasukanResult[];
total: number;
pageSize: number;
page: number; // 1-based
};
/**
* Format tanggal ke format dd/mm/yyyy
*/
export const formatDateToDDMMYYYY = (value?: string | null) => {
if (!value) return '';
return dayjs.utc(value).local().format('DD/MM/YYYY');
};
/**
* 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,
// ✅ ADJUST: whitelist param API (hindari ...rest mentah)
...(rest.tglAwal && { tglAwal: rest.tglAwal }),
...(rest.tglAkhir && { tglAkhir: rest.tglAkhir }),
...(rest.nomordokumen && { nomordokumen: rest.nomordokumen }),
...(rest.npwp && { npwp: rest.npwp }),
...(rest.all && { all: rest.all }),
};
};
/**
* 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 useGetDokumenLainMasukan = ({ params }: { params: any }) => {
const normalized = normalizeParams(params);
const { page, limit, advanced, sortingMode, sortingMethod, ...rest } = normalized;
const restKey = JSON.stringify(rest ?? {});
const isListRequest =
page !== undefined ||
limit !== undefined ||
advanced !== undefined ||
sortingMode ||
sortingMethod;
return useQuery<TGetDokumenLainMasukanApiWrapped>({
queryKey: isListRequest
? ['faktur-pk', page, limit, advanced, sortingMode, sortingMethod, restKey]
: ['faktur-pk', restKey],
queryFn: async () => {
const res: any = await fakturApi.getDokumenLainMasukan({ 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 TGetListDataDokumenLainMasukanResult[],
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 useGetDokumenLainMasukanById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.faktuPK.detail(id),
queryFn: async () => {
const res = await fakturApi.getDokumenLainMasukanById(id);
if (!res) throw new Error('Data tidak ditemukan');
return res;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export default useGetDokumenLainMasukan;
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-dokumen-lain-keluaran', params],
enabled: !!params, // ⭐️ ADD THIS
queryFn: async () => {
const res = await fakturApi.getGoods(params);
return res.data;
},
});
export default useGetGoods;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
import type { TBaseResponseAPI, TIdTambahanResult } from '../types/types';
const useGetIdTambahan = (props?: any) =>
useMutation<TBaseResponseAPI<TIdTambahanResult>, Error, Record<string, any> | undefined>({
mutationFn: async (params) => {
const res = await fakturApi.getIdTambahan(params);
return res;
},
...props,
});
export default useGetIdTambahan;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
import type { TKeteranganTambahanResult, TBaseResponseAPI } from '../types/types';
const useGetKeteranganTambahan = (props?: any) =>
useMutation<TBaseResponseAPI<TKeteranganTambahanResult>, Error, Record<string, any> | undefined>({
mutationFn: async (params) => {
const res = await fakturApi.getKeteranganTambahan(params);
return res;
},
...props,
});
export default useGetKeteranganTambahan;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export * from './dlm-list-view';
export * from './dokumenLainMasukanRekamView';
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